feat: add global immersive/fullscreen mode support across pages

- Add isImmersiveMode state to appStore for global fullscreen management
- Update AppShell to hide Header and Sidebar when immersive mode is active
- Add fullscreen toggle button to workflow and knowledge pages:
  - TerminalDashboardPage, IssueHubPage, SessionsPage, LiteTasksPage
  - HistoryPage, TeamPage, MemoryPage, SkillsManagerPage
  - CommandsManagerPage, RulesManagerPage, CliViewerPage
  - OrchestratorPage (via FlowToolbar)
- Preserve padding in fullscreen mode for better visual appearance
This commit is contained in:
catlog22
2026-02-16 13:07:35 +08:00
parent cffeece220
commit 111b0f6809
16 changed files with 315 additions and 96 deletions

View File

@@ -2,6 +2,7 @@
// AppShell Component // AppShell Component
// ======================================== // ========================================
// Root layout component combining Header, Sidebar, and MainContent // Root layout component combining Header, Sidebar, and MainContent
// Supports immersive mode to hide chrome for fullscreen experiences
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -16,6 +17,7 @@ import { AskQuestionDialog, A2UIPopupCard } from '@/components/a2ui';
import { BackgroundImage } from '@/components/shared/BackgroundImage'; import { BackgroundImage } from '@/components/shared/BackgroundImage';
import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores'; import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores';
import { useWorkflowStore } from '@/stores/workflowStore'; import { useWorkflowStore } from '@/stores/workflowStore';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { useWebSocketNotifications, useWebSocket } from '@/hooks'; import { useWebSocketNotifications, useWebSocket } from '@/hooks';
export interface AppShellProps { export interface AppShellProps {
@@ -40,6 +42,9 @@ export function AppShell({
const projectPath = useWorkflowStore((state) => state.projectPath); const projectPath = useWorkflowStore((state) => state.projectPath);
const location = useLocation(); const location = useLocation();
// Immersive mode (fullscreen) - hide chrome
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
// Workspace initialization logic (URL > localStorage) // Workspace initialization logic (URL > localStorage)
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false); const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
@@ -157,32 +162,36 @@ export function AppShell({
}, [setCurrentPopupCard]); }, [setCurrentPopupCard]);
return ( return (
<div className="flex flex-col min-h-screen bg-background"> <div className={cn("flex flex-col min-h-screen bg-background", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Background image layer (z-index: -3 to -2) */} {/* Background image layer (z-index: -3 to -2) */}
<BackgroundImage /> <BackgroundImage />
{/* Header - fixed at top */} {/* Header - fixed at top (hidden in immersive mode) */}
{!isImmersiveMode && (
<Header <Header
onRefresh={onRefresh} onRefresh={onRefresh}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
onCliMonitorClick={handleCliMonitorClick} onCliMonitorClick={handleCliMonitorClick}
/> />
)}
{/* Main layout - sidebar + content */} {/* Main layout - sidebar + content */}
<div className="flex flex-1 overflow-hidden"> <div className={cn("flex flex-1 overflow-hidden", isImmersiveMode && "h-full")}>
{/* Sidebar - collapsed by default */} {/* Sidebar - collapsed by default (hidden in immersive mode) */}
{!isImmersiveMode && (
<Sidebar <Sidebar
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
onCollapsedChange={handleCollapsedChange} onCollapsedChange={handleCollapsedChange}
mobileOpen={mobileOpen} mobileOpen={mobileOpen}
onMobileClose={handleMobileClose} onMobileClose={handleMobileClose}
/> />
)}
{/* Main content area */} {/* Main content area */}
<MainContent <MainContent
className={cn( className={cn(
'app-shell-content transition-all duration-300', 'app-shell-content transition-all duration-300',
sidebarCollapsed ? 'md:ml-16' : 'md:ml-64' isImmersiveMode ? 'ml-0' : sidebarCollapsed ? 'md:ml-16' : 'md:ml-64'
)} )}
> >
{children} {children}

View File

@@ -370,7 +370,7 @@ export function CliViewerPage() {
const CurrentLayoutIcon = currentLayoutOption.icon; const CurrentLayoutIcon = currentLayoutOption.icon;
return ( return (
<div className="h-full flex flex-col -m-4 md:-m-6"> <div className="h-full flex flex-col">
{/* ======================================== */} {/* ======================================== */}
{/* Toolbar */} {/* Toolbar */}
{/* ======================================== */} {/* ======================================== */}

View File

@@ -16,7 +16,10 @@ import {
Folder, Folder,
User, User,
AlertCircle, AlertCircle,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@@ -40,6 +43,10 @@ export function CommandsManagerPage() {
// Search state // Search state
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const { const {
commands, commands,
groupedCommands, groupedCommands,
@@ -96,7 +103,7 @@ export function CommandsManagerPage() {
}; };
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Page Header */} {/* Page Header */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@@ -109,11 +116,25 @@ export function CommandsManagerPage() {
{formatMessage({ id: 'commands.description' })} {formatMessage({ id: 'commands.description' })}
</p> </p>
</div> </div>
<div className="flex items-center gap-2">
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}> <Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} /> <RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })} {formatMessage({ id: 'common.actions.refresh' })}
</Button> </Button>
</div> </div>
</div>
{/* Error alert */} {/* Error alert */}
{error && ( {error && (

View File

@@ -13,7 +13,10 @@ import {
AlertTriangle, AlertTriangle,
Search, Search,
X, X,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useHistory } from '@/hooks/useHistory'; import { useHistory } from '@/hooks/useHistory';
import { ConversationCard } from '@/components/shared/ConversationCard'; import { ConversationCard } from '@/components/shared/ConversationCard';
@@ -53,6 +56,8 @@ export function HistoryPage() {
const [isPanelOpen, setIsPanelOpen] = React.useState(false); const [isPanelOpen, setIsPanelOpen] = React.useState(false);
const [nativeExecutionId, setNativeExecutionId] = React.useState<string | null>(null); const [nativeExecutionId, setNativeExecutionId] = React.useState<string | null>(null);
const [isNativePanelOpen, setIsNativePanelOpen] = React.useState(false); const [isNativePanelOpen, setIsNativePanelOpen] = React.useState(false);
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const { const {
executions, executions,
@@ -134,7 +139,7 @@ export function HistoryPage() {
}; };
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header */} {/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
@@ -146,6 +151,18 @@ export function HistoryPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@@ -11,6 +11,8 @@ import {
RefreshCw, RefreshCw,
Github, Github,
Loader2, Loader2,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader'; import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs'; import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs';
@@ -29,6 +31,7 @@ import { useIssues, useIssueMutations, useIssueQueue } from '@/hooks';
import { pullIssuesFromGitHub, uploadAttachments } from '@/lib/api'; import { pullIssuesFromGitHub, uploadAttachments } from '@/lib/api';
import type { Issue } from '@/lib/api'; import type { Issue } from '@/lib/api';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
// Issue types // Issue types
type IssueType = 'bug' | 'feature' | 'improvement' | 'other'; type IssueType = 'bug' | 'feature' | 'improvement' | 'other';
@@ -286,6 +289,10 @@ export function IssueHubPage() {
const [isNewIssueOpen, setIsNewIssueOpen] = useState(false); const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
const [isGithubSyncing, setIsGithubSyncing] = useState(false); const [isGithubSyncing, setIsGithubSyncing] = useState(false);
// Immersive mode (fullscreen) - hide app chrome
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Issues data // Issues data
const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues(); const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues();
// Queue data // Queue data
@@ -392,17 +399,36 @@ export function IssueHubPage() {
}; };
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header and action buttons on same row */} {/* Header and action buttons on same row */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<IssueHubHeader currentTab={currentTab} /> <IssueHubHeader currentTab={currentTab} />
<div className="flex items-center gap-2">
{/* Action buttons - dynamic based on current tab */} {/* Action buttons - dynamic based on current tab */}
{renderActionButtons() && (
<div className="flex gap-2">
{renderActionButtons()} {renderActionButtons()}
</div>
{/* Fullscreen toggle */}
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)} )}
title={isImmersiveMode
? formatMessage({ id: 'issueHub.exitFullscreen', defaultMessage: 'Exit Fullscreen' })
: formatMessage({ id: 'issueHub.fullscreen', defaultMessage: 'Fullscreen' })
}
>
{isImmersiveMode ? (
<Minimize2 className="w-4 h-4" />
) : (
<Maximize2 className="w-4 h-4" />
)}
</button>
</div>
</div> </div>
<IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} /> <IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} />

View File

@@ -45,7 +45,10 @@ import {
Link2, Link2,
ShieldCheck, ShieldCheck,
Settings2, Settings2,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { useLiteTasks } from '@/hooks/useLiteTasks'; import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
@@ -55,6 +58,7 @@ import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api'; import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api';
import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent'; import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan'; type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
type SortField = 'date' | 'name' | 'tasks'; type SortField = 'date' | 'name' | 'tasks';
@@ -1209,6 +1213,8 @@ export function LiteTasksPage() {
const [searchQuery, setSearchQuery] = React.useState(''); const [searchQuery, setSearchQuery] = React.useState('');
const [sortField, setSortField] = React.useState<SortField>('date'); const [sortField, setSortField] = React.useState<SortField>('date');
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc'); const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Filter and sort sessions // Filter and sort sessions
const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => { const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => {
@@ -1525,7 +1531,7 @@ export function LiteTasksPage() {
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length; const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -1542,6 +1548,18 @@ export function LiteTasksPage() {
</p> </p>
</div> </div>
</div> </div>
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
</div> </div>
{/* Tabs */} {/* Tabs */}

View File

@@ -28,7 +28,10 @@ import {
Terminal, Terminal,
GitBranch, GitBranch,
Hash, Hash,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@@ -624,6 +627,8 @@ export function MemoryPage() {
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories'); const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories');
const [unifiedQuery, setUnifiedQuery] = useState(''); const [unifiedQuery, setUnifiedQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState(''); const [selectedCategory, setSelectedCategory] = useState('');
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const isUnifiedTab = currentTab === 'unifiedSearch'; const isUnifiedTab = currentTab === 'unifiedSearch';
@@ -784,7 +789,7 @@ export function MemoryPage() {
const activeError = isUnifiedTab ? unifiedError : error; const activeError = isUnifiedTab ? unifiedError : error;
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Page Header */} {/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
@@ -797,6 +802,18 @@ export function MemoryPage() {
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
{isUnifiedTab && ( {isUnifiedTab && (
<Button <Button
variant="outline" variant="outline"

View File

@@ -16,7 +16,10 @@ import {
Folder, Folder,
User, User,
Globe, Globe,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { import {
useRules, useRules,
useCreateRule, useCreateRule,
@@ -64,6 +67,10 @@ export function RulesManagerPage() {
const [searchQuery, setSearchQuery] = React.useState(''); const [searchQuery, setSearchQuery] = React.useState('');
const [categoryFilter, setCategoryFilter] = React.useState<string[]>([]); const [categoryFilter, setCategoryFilter] = React.useState<string[]>([]);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Dialog state // Dialog state
const [createDialogOpen, setCreateDialogOpen] = React.useState(false); const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
const [editDialogOpen, setEditDialogOpen] = React.useState(false); const [editDialogOpen, setEditDialogOpen] = React.useState(false);
@@ -193,7 +200,7 @@ export function RulesManagerPage() {
categoryFilter.length > 0 || searchQuery.length > 0; categoryFilter.length > 0 || searchQuery.length > 0;
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header */} {/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
@@ -203,6 +210,18 @@ export function RulesManagerPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@@ -13,6 +13,8 @@ import {
AlertCircle, AlertCircle,
FolderKanban, FolderKanban,
X, X,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { import {
useSessions, useSessions,
@@ -43,6 +45,7 @@ import {
import { TabsNavigation } from '@/components/ui/TabsNavigation'; import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { SessionMetadata } from '@/types/store'; import type { SessionMetadata } from '@/types/store';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
type LocationFilter = 'all' | 'active' | 'archived'; type LocationFilter = 'all' | 'active' | 'archived';
@@ -71,6 +74,10 @@ export function SessionsPage() {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [sessionToDelete, setSessionToDelete] = React.useState<string | null>(null); const [sessionToDelete, setSessionToDelete] = React.useState<string | null>(null);
// Immersive mode (fullscreen)
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Build filter object // Build filter object
const filter: SessionsFilter = React.useMemo( const filter: SessionsFilter = React.useMemo(
() => ({ () => ({
@@ -149,7 +156,7 @@ export function SessionsPage() {
const hasActiveFilters = statusFilter.length > 0 || searchQuery.length > 0; const hasActiveFilters = statusFilter.length > 0 || searchQuery.length > 0;
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header */} {/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
@@ -168,6 +175,18 @@ export function SessionsPage() {
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} /> <RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })} {formatMessage({ id: 'common.actions.refresh' })}
</Button> </Button>
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? formatMessage({ id: 'common.exitFullscreen', defaultMessage: 'Exit Fullscreen' }) : formatMessage({ id: 'common.fullscreen', defaultMessage: 'Fullscreen' })}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
</div> </div>
</div> </div>

View File

@@ -21,7 +21,10 @@ import {
Folder, Folder,
User, User,
AlertCircle, AlertCircle,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@@ -128,6 +131,10 @@ export function SkillsManagerPage() {
const [isDetailLoading, setIsDetailLoading] = useState(false); const [isDetailLoading, setIsDetailLoading] = useState(false);
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false); const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const { const {
skills, skills,
categories, categories,
@@ -233,7 +240,7 @@ export function SkillsManagerPage() {
}, []); }, []);
return ( return (
<div className="space-y-6"> <div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Page Header */} {/* Page Header */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@@ -256,6 +263,18 @@ export function SkillsManagerPage() {
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}> <Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} /> <RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })} {formatMessage({ id: 'common.actions.refresh' })}

View File

@@ -4,8 +4,10 @@
// Main page for team execution - list/detail dual view with tabbed detail // Main page for team execution - list/detail dual view with tabbed detail
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Package, MessageSquare } from 'lucide-react'; import { Package, MessageSquare, Maximize2, Minimize2 } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/Card'; import { Card, CardContent } from '@/components/ui/Card';
import { cn } from '@/lib/utils';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation'; import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { useTeamStore } from '@/stores/teamStore'; import { useTeamStore } from '@/stores/teamStore';
import type { TeamDetailTab } from '@/stores/teamStore'; import type { TeamDetailTab } from '@/stores/teamStore';
@@ -33,6 +35,8 @@ export function TeamPage() {
setDetailTab, setDetailTab,
backToList, backToList,
} = useTeamStore(); } = useTeamStore();
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Data hooks (only active in detail mode) // Data hooks (only active in detail mode)
const { messages, total: messageTotal } = useTeamMessages( const { messages, total: messageTotal } = useTeamMessages(
@@ -67,8 +71,9 @@ export function TeamPage() {
// Detail view // Detail view
return ( return (
<div className="p-6 space-y-6"> <div className={cn("p-6 space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Detail Header: back button + team name + stats + controls */} {/* Detail Header: back button + team name + stats + controls */}
<div className="flex items-center justify-between">
<TeamHeader <TeamHeader
selectedTeam={selectedTeam} selectedTeam={selectedTeam}
onBack={backToList} onBack={backToList}
@@ -77,6 +82,19 @@ export function TeamPage() {
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
onToggleAutoRefresh={toggleAutoRefresh} onToggleAutoRefresh={toggleAutoRefresh}
/> />
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
</div>
{/* Overview: Pipeline + Members (always visible) */} {/* Overview: Pipeline + Members (always visible) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">

View File

@@ -7,7 +7,7 @@
// Right sidebar: FileSidebarPanel (file tree, resizable) // Right sidebar: FileSidebarPanel (file tree, resizable)
// Top: DashboardToolbar with panel toggles and layout presets // Top: DashboardToolbar with panel toggles and layout presets
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive) // Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
// Fullscreen mode: Hides all sidebars for maximum terminal space // Fullscreen mode: Uses global isImmersiveMode to hide app chrome (Header + Sidebar)
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -24,6 +24,7 @@ import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector'; import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector';
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel'; import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
// ========== Main Page Component ========== // ========== Main Page Component ==========
@@ -32,10 +33,13 @@ export function TerminalDashboardPage() {
const [activePanel, setActivePanel] = useState<PanelId | null>(null); const [activePanel, setActivePanel] = useState<PanelId | null>(null);
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true); const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true); const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
const projectPath = useWorkflowStore(selectProjectPath); const projectPath = useWorkflowStore(selectProjectPath);
// Use global immersive mode state (only affects AppShell chrome)
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const togglePanel = useCallback((panelId: PanelId) => { const togglePanel = useCallback((panelId: PanelId) => {
setActivePanel((prev) => (prev === panelId ? null : panelId)); setActivePanel((prev) => (prev === panelId ? null : panelId));
}, []); }, []);
@@ -44,17 +48,8 @@ export function TerminalDashboardPage() {
setActivePanel(null); setActivePanel(null);
}, []); }, []);
const toggleFullscreen = useCallback(() => {
setIsFullscreen((prev) => !prev);
}, []);
// In fullscreen mode, hide all sidebars and panels
const showSessionSidebar = isSessionSidebarOpen && !isFullscreen;
const showFileSidebar = isFileSidebarOpen && !isFullscreen;
const showFloatingPanels = !isFullscreen;
return ( return (
<div className={`flex flex-col overflow-hidden ${isFullscreen ? 'h-screen -m-0' : 'h-[calc(100vh-56px)] -m-4 md:-m-6'}`}> <div className={`flex flex-col overflow-hidden ${isImmersiveMode ? 'h-screen' : 'h-[calc(100vh-56px)]'}`}>
<AssociationHighlightProvider> <AssociationHighlightProvider>
{/* Global toolbar */} {/* Global toolbar */}
<DashboardToolbar <DashboardToolbar
@@ -64,15 +59,15 @@ export function TerminalDashboardPage() {
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)} onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
isSessionSidebarOpen={isSessionSidebarOpen} isSessionSidebarOpen={isSessionSidebarOpen}
onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)} onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)}
isFullscreen={isFullscreen} isFullscreen={isImmersiveMode}
onToggleFullscreen={toggleFullscreen} onToggleFullscreen={toggleImmersiveMode}
/> />
{/* Main content with three-column layout */} {/* Main content with three-column layout */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<Allotment className="h-full"> <Allotment className="h-full">
{/* Session sidebar (conditional) */} {/* Session sidebar (controlled by local state, not immersive mode) */}
{showSessionSidebar && ( {isSessionSidebarOpen && (
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}> <Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
<div className="h-full flex flex-col border-r border-border"> <div className="h-full flex flex-col border-r border-border">
<div className="flex-1 min-h-0 overflow-y-auto"> <div className="flex-1 min-h-0 overflow-y-auto">
@@ -90,8 +85,8 @@ export function TerminalDashboardPage() {
<TerminalGrid /> <TerminalGrid />
</Allotment.Pane> </Allotment.Pane>
{/* File sidebar (conditional, default 280px) */} {/* File sidebar (controlled by local state, not immersive mode) */}
{showFileSidebar && ( {isFileSidebarOpen && (
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}> <Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
<FileSidebarPanel <FileSidebarPanel
rootPath={projectPath ?? '/'} rootPath={projectPath ?? '/'}
@@ -103,9 +98,7 @@ export function TerminalDashboardPage() {
</Allotment> </Allotment>
</div> </div>
{/* Floating panels (conditional, overlay) */} {/* Floating panels (always available) */}
{showFloatingPanels && (
<>
<FloatingPanel <FloatingPanel
isOpen={activePanel === 'issues'} isOpen={activePanel === 'issues'}
onClose={closePanel} onClose={closePanel}
@@ -135,8 +128,6 @@ export function TerminalDashboardPage() {
> >
<InspectorContent /> <InspectorContent />
</FloatingPanel> </FloatingPanel>
</>
)}
</AssociationHighlightProvider> </AssociationHighlightProvider>
</div> </div>
); );

View File

@@ -17,6 +17,8 @@ import {
Library, Library,
Play, Play,
Activity, Activity,
Maximize2,
Minimize2,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -24,6 +26,7 @@ import { Input } from '@/components/ui/Input';
import { useFlowStore, toast } from '@/stores'; import { useFlowStore, toast } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore'; import { useExecutionStore } from '@/stores/executionStore';
import { useExecuteFlow } from '@/hooks/useFlows'; import { useExecuteFlow } from '@/hooks/useFlows';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import type { Flow } from '@/types/flow'; import type { Flow } from '@/types/flow';
interface FlowToolbarProps { interface FlowToolbarProps {
@@ -37,6 +40,10 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const [flowName, setFlowName] = useState(''); const [flowName, setFlowName] = useState('');
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Flow store // Flow store
const currentFlow = useFlowStore((state) => state.currentFlow); const currentFlow = useFlowStore((state) => state.currentFlow);
const isModified = useFlowStore((state) => state.isModified); const isModified = useFlowStore((state) => state.isModified);
@@ -363,6 +370,22 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
)} )}
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })} {formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
</Button> </Button>
<div className="w-px h-6 bg-border" />
{/* Fullscreen Toggle */}
<button
onClick={toggleImmersiveMode}
className={cn(
'p-2 rounded-md transition-colors',
isImmersiveMode
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
</div> </div>
</div> </div>
); );

View File

@@ -5,8 +5,9 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { ChevronRight } from 'lucide-react'; import { ChevronRight, Maximize2, Minimize2 } from 'lucide-react';
import { useFlowStore } from '@/stores'; import { useFlowStore } from '@/stores';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { useExecutionStore } from '@/stores/executionStore'; import { useExecutionStore } from '@/stores/executionStore';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { FlowCanvas } from './FlowCanvas'; import { FlowCanvas } from './FlowCanvas';

View File

@@ -236,6 +236,9 @@ const initialState = {
themeSlots: [DEFAULT_SLOT] as ThemeSlot[], themeSlots: [DEFAULT_SLOT] as ThemeSlot[],
activeSlotId: 'default' as ThemeSlotId, activeSlotId: 'default' as ThemeSlotId,
deletedSlotBuffer: null as ThemeSlot | null, deletedSlotBuffer: null as ThemeSlot | null,
// Immersive fullscreen mode (hides app shell chrome)
isImmersiveMode: false,
}; };
export const useAppStore = create<AppStore>()( export const useAppStore = create<AppStore>()(
@@ -670,6 +673,16 @@ export const useAppStore = create<AppStore>()(
} }
get().setBackgroundConfig(updated); get().setBackgroundConfig(updated);
}, },
// ========== Immersive Mode Actions ==========
setImmersiveMode: (enabled: boolean) => {
set({ isImmersiveMode: enabled }, false, 'setImmersiveMode');
},
toggleImmersiveMode: () => {
set((state) => ({ isImmersiveMode: !state.isImmersiveMode }), false, 'toggleImmersiveMode');
},
}), }),
{ {
name: 'ccw-app-store', name: 'ccw-app-store',
@@ -807,3 +820,4 @@ export const selectError = (state: AppStore) => state.error;
export const selectThemeSlots = (state: AppStore) => state.themeSlots; export const selectThemeSlots = (state: AppStore) => state.themeSlots;
export const selectActiveSlotId = (state: AppStore) => state.activeSlotId; export const selectActiveSlotId = (state: AppStore) => state.activeSlotId;
export const selectDeletedSlotBuffer = (state: AppStore) => state.deletedSlotBuffer; export const selectDeletedSlotBuffer = (state: AppStore) => state.deletedSlotBuffer;
export const selectIsImmersiveMode = (state: AppStore) => state.isImmersiveMode;

View File

@@ -117,6 +117,9 @@ export interface AppState {
themeSlots: ThemeSlot[]; themeSlots: ThemeSlot[];
activeSlotId: ThemeSlotId; activeSlotId: ThemeSlotId;
deletedSlotBuffer: ThemeSlot | null; deletedSlotBuffer: ThemeSlot | null;
// Immersive mode (fullscreen)
isImmersiveMode: boolean;
} }
export interface AppActions { export interface AppActions {
@@ -170,6 +173,10 @@ export interface AppActions {
updateBackgroundEffect: <K extends keyof BackgroundEffects>(key: K, value: BackgroundEffects[K]) => void; updateBackgroundEffect: <K extends keyof BackgroundEffects>(key: K, value: BackgroundEffects[K]) => void;
setBackgroundMode: (mode: BackgroundMode) => void; setBackgroundMode: (mode: BackgroundMode) => void;
setBackgroundImage: (url: string | null, attribution: UnsplashAttribution | null) => void; setBackgroundImage: (url: string | null, attribution: UnsplashAttribution | null) => void;
// Immersive mode actions
setImmersiveMode: (enabled: boolean) => void;
toggleImmersiveMode: () => void;
} }
export type AppStore = AppState & AppActions; export type AppStore = AppState & AppActions;