diff --git a/ccw/frontend/src/components/layout/AppShell.tsx b/ccw/frontend/src/components/layout/AppShell.tsx index d47830ef..b579a0b0 100644 --- a/ccw/frontend/src/components/layout/AppShell.tsx +++ b/ccw/frontend/src/components/layout/AppShell.tsx @@ -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 ( -
+
{/* Background image layer (z-index: -3 to -2) */} - {/* Header - fixed at top */} -
+ {/* Header - fixed at top (hidden in immersive mode) */} + {!isImmersiveMode && ( +
+ )} {/* Main layout - sidebar + content */} -
- {/* Sidebar - collapsed by default */} - +
+ {/* Sidebar - collapsed by default (hidden in immersive mode) */} + {!isImmersiveMode && ( + + )} {/* Main content area */} {children} diff --git a/ccw/frontend/src/pages/CliViewerPage.tsx b/ccw/frontend/src/pages/CliViewerPage.tsx index 8c861bbb..b23480e8 100644 --- a/ccw/frontend/src/pages/CliViewerPage.tsx +++ b/ccw/frontend/src/pages/CliViewerPage.tsx @@ -370,7 +370,7 @@ export function CliViewerPage() { const CurrentLayoutIcon = currentLayoutOption.icon; return ( -
+
{/* ======================================== */} {/* Toolbar */} {/* ======================================== */} diff --git a/ccw/frontend/src/pages/CommandsManagerPage.tsx b/ccw/frontend/src/pages/CommandsManagerPage.tsx index 5c3123b5..efc47260 100644 --- a/ccw/frontend/src/pages/CommandsManagerPage.tsx +++ b/ccw/frontend/src/pages/CommandsManagerPage.tsx @@ -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 ( -
+
{/* Page Header */}
@@ -109,10 +116,24 @@ export function CommandsManagerPage() { {formatMessage({ id: 'commands.description' })}

- +
+ + +
{/* Error alert */} diff --git a/ccw/frontend/src/pages/HistoryPage.tsx b/ccw/frontend/src/pages/HistoryPage.tsx index 7cd3b0b1..9f05e4e2 100644 --- a/ccw/frontend/src/pages/HistoryPage.tsx +++ b/ccw/frontend/src/pages/HistoryPage.tsx @@ -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(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 ( -
+
{/* Header */}
@@ -146,6 +151,18 @@ export function HistoryPage() {

+ +
diff --git a/ccw/frontend/src/pages/LiteTasksPage.tsx b/ccw/frontend/src/pages/LiteTasksPage.tsx index ef77761d..d22b23b0 100644 --- a/ccw/frontend/src/pages/LiteTasksPage.tsx +++ b/ccw/frontend/src/pages/LiteTasksPage.tsx @@ -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('date'); const [sortOrder, setSortOrder] = React.useState('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 ( -
+
{/* Header */}
@@ -1542,6 +1548,18 @@ export function LiteTasksPage() {

+
{/* Tabs */} diff --git a/ccw/frontend/src/pages/MemoryPage.tsx b/ccw/frontend/src/pages/MemoryPage.tsx index c44de8e3..7a689a72 100644 --- a/ccw/frontend/src/pages/MemoryPage.tsx +++ b/ccw/frontend/src/pages/MemoryPage.tsx @@ -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 ( -
+
{/* Page Header */}
@@ -797,6 +802,18 @@ export function MemoryPage() {

+ {isUnifiedTab && ( +
diff --git a/ccw/frontend/src/pages/SkillsManagerPage.tsx b/ccw/frontend/src/pages/SkillsManagerPage.tsx index ce16e65d..9ebfed9c 100644 --- a/ccw/frontend/src/pages/SkillsManagerPage.tsx +++ b/ccw/frontend/src/pages/SkillsManagerPage.tsx @@ -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 ( -
+
{/* Page Header */}
@@ -256,6 +263,18 @@ export function SkillsManagerPage() {
+ +
{/* Overview: Pipeline + Members (always visible) */}
diff --git a/ccw/frontend/src/pages/TerminalDashboardPage.tsx b/ccw/frontend/src/pages/TerminalDashboardPage.tsx index ba98affd..622a9144 100644 --- a/ccw/frontend/src/pages/TerminalDashboardPage.tsx +++ b/ccw/frontend/src/pages/TerminalDashboardPage.tsx @@ -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(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 ( -
+
{/* Global toolbar */} setIsFileSidebarOpen((prev) => !prev)} isSessionSidebarOpen={isSessionSidebarOpen} onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)} - isFullscreen={isFullscreen} - onToggleFullscreen={toggleFullscreen} + isFullscreen={isImmersiveMode} + onToggleFullscreen={toggleImmersiveMode} /> {/* Main content with three-column layout */}
- {/* Session sidebar (conditional) */} - {showSessionSidebar && ( + {/* Session sidebar (controlled by local state, not immersive mode) */} + {isSessionSidebarOpen && (
@@ -90,8 +85,8 @@ export function TerminalDashboardPage() { - {/* File sidebar (conditional, default 280px) */} - {showFileSidebar && ( + {/* File sidebar (controlled by local state, not immersive mode) */} + {isFileSidebarOpen && (
- {/* Floating panels (conditional, overlay) */} - {showFloatingPanels && ( - <> - - - + {/* Floating panels (always available) */} + + + - - - + + + - - - - - )} + + +
); diff --git a/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx b/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx index d14f4392..6ded5b67 100644 --- a/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx +++ b/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx @@ -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' })} + +
+ + {/* Fullscreen Toggle */} +
); diff --git a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx index c15e1be4..006964f6 100644 --- a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx +++ b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx @@ -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'; diff --git a/ccw/frontend/src/stores/appStore.ts b/ccw/frontend/src/stores/appStore.ts index c77b05cc..04eff97c 100644 --- a/ccw/frontend/src/stores/appStore.ts +++ b/ccw/frontend/src/stores/appStore.ts @@ -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()( @@ -670,6 +673,16 @@ export const useAppStore = create()( } 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; diff --git a/ccw/frontend/src/types/store.ts b/ccw/frontend/src/types/store.ts index f43f0c8f..d09ba29a 100644 --- a/ccw/frontend/src/types/store.ts +++ b/ccw/frontend/src/types/store.ts @@ -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: (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;