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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,8 @@ import {
RefreshCw,
Github,
Loader2,
Maximize2,
Minimize2,
} from 'lucide-react';
import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
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 type { Issue } from '@/lib/api';
import { cn } from '@/lib/utils';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
// Issue types
type IssueType = 'bug' | 'feature' | 'improvement' | 'other';
@@ -286,6 +289,10 @@ export function IssueHubPage() {
const [isNewIssueOpen, setIsNewIssueOpen] = 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
const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues();
// Queue data
@@ -392,17 +399,36 @@ export function IssueHubPage() {
};
return (
<div className="space-y-6">
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header and action buttons on same row */}
<div className="flex items-center justify-between">
<IssueHubHeader currentTab={currentTab} />
{/* Action buttons - dynamic based on current tab */}
{renderActionButtons() && (
<div className="flex gap-2">
{renderActionButtons()}
</div>
)}
<div className="flex items-center gap-2">
{/* Action buttons - dynamic based on current tab */}
{renderActionButtons()}
{/* 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>
<IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} />

View File

@@ -45,7 +45,10 @@ import {
Link2,
ShieldCheck,
Settings2,
Maximize2,
Minimize2,
} from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button';
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 { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
type SortField = 'date' | 'name' | 'tasks';
@@ -1209,6 +1213,8 @@ export function LiteTasksPage() {
const [searchQuery, setSearchQuery] = React.useState('');
const [sortField, setSortField] = React.useState<SortField>('date');
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Filter and sort sessions
const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => {
@@ -1525,7 +1531,7 @@ export function LiteTasksPage() {
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
return (
<div className="space-y-6">
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
@@ -1542,6 +1548,18 @@ export function LiteTasksPage() {
</p>
</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>
{/* Tabs */}

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,10 @@ import {
Folder,
User,
AlertCircle,
Maximize2,
Minimize2,
} from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
@@ -128,6 +131,10 @@ export function SkillsManagerPage() {
const [isDetailLoading, setIsDetailLoading] = useState(false);
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
const {
skills,
categories,
@@ -233,7 +240,7 @@ export function SkillsManagerPage() {
}, []);
return (
<div className="space-y-6">
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
{/* Page Header */}
<div className="flex flex-col 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 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}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}

View File

@@ -4,8 +4,10 @@
// Main page for team execution - list/detail dual view with tabbed detail
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 { cn } from '@/lib/utils';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { useTeamStore } from '@/stores/teamStore';
import type { TeamDetailTab } from '@/stores/teamStore';
@@ -33,6 +35,8 @@ export function TeamPage() {
setDetailTab,
backToList,
} = useTeamStore();
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Data hooks (only active in detail mode)
const { messages, total: messageTotal } = useTeamMessages(
@@ -67,16 +71,30 @@ export function TeamPage() {
// Detail view
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 */}
<TeamHeader
selectedTeam={selectedTeam}
onBack={backToList}
members={members}
totalMessages={totalMessages}
autoRefresh={autoRefresh}
onToggleAutoRefresh={toggleAutoRefresh}
/>
<div className="flex items-center justify-between">
<TeamHeader
selectedTeam={selectedTeam}
onBack={backToList}
members={members}
totalMessages={totalMessages}
autoRefresh={autoRefresh}
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) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">

View File

@@ -7,7 +7,7 @@
// Right sidebar: FileSidebarPanel (file tree, resizable)
// Top: DashboardToolbar with panel toggles and layout presets
// 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 { useIntl } from 'react-intl';
@@ -24,6 +24,7 @@ import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector';
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
// ========== Main Page Component ==========
@@ -32,10 +33,13 @@ export function TerminalDashboardPage() {
const [activePanel, setActivePanel] = useState<PanelId | null>(null);
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
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) => {
setActivePanel((prev) => (prev === panelId ? null : panelId));
}, []);
@@ -44,17 +48,8 @@ export function TerminalDashboardPage() {
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 (
<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>
{/* Global toolbar */}
<DashboardToolbar
@@ -64,15 +59,15 @@ export function TerminalDashboardPage() {
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
isSessionSidebarOpen={isSessionSidebarOpen}
onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)}
isFullscreen={isFullscreen}
onToggleFullscreen={toggleFullscreen}
isFullscreen={isImmersiveMode}
onToggleFullscreen={toggleImmersiveMode}
/>
{/* Main content with three-column layout */}
<div className="flex-1 min-h-0">
<Allotment className="h-full">
{/* Session sidebar (conditional) */}
{showSessionSidebar && (
{/* Session sidebar (controlled by local state, not immersive mode) */}
{isSessionSidebarOpen && (
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
<div className="h-full flex flex-col border-r border-border">
<div className="flex-1 min-h-0 overflow-y-auto">
@@ -90,8 +85,8 @@ export function TerminalDashboardPage() {
<TerminalGrid />
</Allotment.Pane>
{/* File sidebar (conditional, default 280px) */}
{showFileSidebar && (
{/* File sidebar (controlled by local state, not immersive mode) */}
{isFileSidebarOpen && (
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
<FileSidebarPanel
rootPath={projectPath ?? '/'}
@@ -103,40 +98,36 @@ export function TerminalDashboardPage() {
</Allotment>
</div>
{/* Floating panels (conditional, overlay) */}
{showFloatingPanels && (
<>
<FloatingPanel
isOpen={activePanel === 'issues'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
side="left"
width={380}
>
<IssuePanel />
</FloatingPanel>
{/* Floating panels (always available) */}
<FloatingPanel
isOpen={activePanel === 'issues'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
side="left"
width={380}
>
<IssuePanel />
</FloatingPanel>
<FloatingPanel
isOpen={activePanel === 'queue'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
side="right"
width={400}
>
<QueuePanel />
</FloatingPanel>
<FloatingPanel
isOpen={activePanel === 'queue'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
side="right"
width={400}
>
<QueuePanel />
</FloatingPanel>
<FloatingPanel
isOpen={activePanel === 'inspector'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
side="right"
width={360}
>
<InspectorContent />
</FloatingPanel>
</>
)}
<FloatingPanel
isOpen={activePanel === 'inspector'}
onClose={closePanel}
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
side="right"
width={360}
>
<InspectorContent />
</FloatingPanel>
</AssociationHighlightProvider>
</div>
);

View File

@@ -17,6 +17,8 @@ import {
Library,
Play,
Activity,
Maximize2,
Minimize2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -24,6 +26,7 @@ import { Input } from '@/components/ui/Input';
import { useFlowStore, toast } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore';
import { useExecuteFlow } from '@/hooks/useFlows';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import type { Flow } from '@/types/flow';
interface FlowToolbarProps {
@@ -37,6 +40,10 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const [flowName, setFlowName] = useState('');
const [isSaving, setIsSaving] = useState(false);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
// Flow store
const currentFlow = useFlowStore((state) => state.currentFlow);
const isModified = useFlowStore((state) => state.isModified);
@@ -363,6 +370,22 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
)}
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
</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>
);

View File

@@ -5,8 +5,9 @@
import { useEffect, useState, useCallback } from 'react';
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 { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { useExecutionStore } from '@/stores/executionStore';
import { Button } from '@/components/ui/Button';
import { FlowCanvas } from './FlowCanvas';

View File

@@ -236,6 +236,9 @@ const initialState = {
themeSlots: [DEFAULT_SLOT] as ThemeSlot[],
activeSlotId: 'default' as ThemeSlotId,
deletedSlotBuffer: null as ThemeSlot | null,
// Immersive fullscreen mode (hides app shell chrome)
isImmersiveMode: false,
};
export const useAppStore = create<AppStore>()(
@@ -670,6 +673,16 @@ export const useAppStore = create<AppStore>()(
}
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',
@@ -807,3 +820,4 @@ export const selectError = (state: AppStore) => state.error;
export const selectThemeSlots = (state: AppStore) => state.themeSlots;
export const selectActiveSlotId = (state: AppStore) => state.activeSlotId;
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[];
activeSlotId: ThemeSlotId;
deletedSlotBuffer: ThemeSlot | null;
// Immersive mode (fullscreen)
isImmersiveMode: boolean;
}
export interface AppActions {
@@ -170,6 +173,10 @@ export interface AppActions {
updateBackgroundEffect: <K extends keyof BackgroundEffects>(key: K, value: BackgroundEffects[K]) => void;
setBackgroundMode: (mode: BackgroundMode) => void;
setBackgroundImage: (url: string | null, attribution: UnsplashAttribution | null) => void;
// Immersive mode actions
setImmersiveMode: (enabled: boolean) => void;
toggleImmersiveMode: () => void;
}
export type AppStore = AppState & AppActions;