--- phase: 04-workspace-ui plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/renderer/components/common/ContextSwitcher.tsx - src/renderer/components/common/ConnectionStatusBadge.tsx - src/renderer/components/layout/SidebarHeader.tsx - src/renderer/hooks/useKeyboardShortcuts.ts - src/renderer/store/slices/contextSlice.ts - src/renderer/App.tsx autonomous: true must_haves: truths: - "User sees context switcher in SidebarHeader Row 1 listing Local + all connected SSH workspaces" - "Each workspace item shows connection status icon (connected/connecting/disconnected/error with distinct colors)" - "User can click a workspace in the dropdown to switch contexts (triggers switchContext from contextSlice)" - "User can press Cmd/Ctrl+Shift+K to cycle to the next context" - "Context switcher is disabled while isContextSwitching is true (prevents race condition)" - "Available contexts refresh on mount and when SSH status changes" artifacts: - path: "src/renderer/components/common/ContextSwitcher.tsx" provides: "Dropdown listing local + SSH contexts with status badges, switch-on-click" min_lines: 60 - path: "src/renderer/components/common/ConnectionStatusBadge.tsx" provides: "Icon component rendering 4 connection states with distinct visual treatment" min_lines: 25 - path: "src/renderer/components/layout/SidebarHeader.tsx" provides: "Modified Row 1 with ContextSwitcher before project name" - path: "src/renderer/hooks/useKeyboardShortcuts.ts" provides: "Cmd+Shift+K shortcut for context cycling" - path: "src/renderer/store/slices/contextSlice.ts" provides: "availableContexts state + fetchAvailableContexts action" key_links: - from: "src/renderer/components/common/ContextSwitcher.tsx" to: "src/renderer/store/slices/contextSlice.ts" via: "useStore consuming availableContexts, activeContextId, switchContext, isContextSwitching" pattern: "useStore.*availableContexts|switchContext" - from: "src/renderer/components/common/ConnectionStatusBadge.tsx" to: "src/renderer/store/slices/connectionSlice.ts" via: "useStore consuming connectionState for SSH contexts" pattern: "connectionState" - from: "src/renderer/components/layout/SidebarHeader.tsx" to: "src/renderer/components/common/ContextSwitcher.tsx" via: "ContextSwitcher rendered in Row 1" pattern: " Create the context switcher UI and keyboard shortcuts for workspace switching. Purpose: Users need a visible, always-accessible way to see which workspace (local or SSH) they are in, switch between workspaces, and understand connection status at a glance. This is the primary user-facing UI for the multi-context system built in Phases 1-3. Output: ContextSwitcher dropdown in SidebarHeader, ConnectionStatusBadge icons, Cmd+Shift+K shortcut, and availableContexts state in contextSlice. @/home/bskim/.claude/get-shit-done/workflows/execute-plan.md @/home/bskim/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-workspace-ui/04-RESEARCH.md @.planning/phases/03-state-management/03-01-SUMMARY.md @src/renderer/components/layout/SidebarHeader.tsx @src/renderer/hooks/useKeyboardShortcuts.ts @src/renderer/store/slices/contextSlice.ts @src/renderer/store/slices/connectionSlice.ts @src/renderer/App.tsx @src/renderer/components/settings/sections/ConnectionSection.tsx @src/preload/index.ts @src/shared/types/api.ts Task 1: Create ConnectionStatusBadge and ContextSwitcher components src/renderer/components/common/ConnectionStatusBadge.tsx src/renderer/components/common/ContextSwitcher.tsx src/renderer/store/slices/contextSlice.ts **1. Add availableContexts state to contextSlice.ts:** Add to `ContextSlice` interface: - `availableContexts: ContextInfo[]` (import `ContextInfo` from `@shared/types/api`) - `fetchAvailableContexts: () => Promise` Add to slice creator initial state: - `availableContexts: [{ id: 'local', type: 'local' as const }]` Implement `fetchAvailableContexts`: - Call `window.electronAPI.context.list()` - `set({ availableContexts: result })` - Wrap in try/catch, on error fallback to `[{ id: 'local', type: 'local' }]` Also call `fetchAvailableContexts()` inside `initializeContextSystem()` after setting `contextSnapshotsReady: true`. **2. Create ConnectionStatusBadge.tsx:** Small icon component that renders connection state visually: ```typescript import { Loader2, Monitor, Wifi, WifiOff } from 'lucide-react'; import { useStore } from '@renderer/store'; ``` Props: `{ contextId: string; className?: string }` Logic: - If `contextId === 'local'`: render `` - If SSH: read `connectionState` and `connectedHost` from store via `useStore` - Determine if this specific SSH context matches the connected host: `contextId === 'ssh-' + connectedHost` - If match: use `connectionState` value - If no match: treat as `'disconnected'` - Render by state: - `'connected'`: `` - `'connecting'`: `` - `'disconnected'`: `` - `'error'`: `` Apply `className` prop to wrapper if provided. **3. Create ContextSwitcher.tsx:** Follow the SidebarHeader dropdown pattern exactly (useRef, outside click, escape key, `inset-x-4` dropdown positioning). Props: none (reads everything from store). Store selectors (via `useShallow`): - `activeContextId`, `isContextSwitching`, `availableContexts`, `switchContext` State: - `isOpen: boolean` (dropdown visibility) - `dropdownRef: useRef` Effects: - Close on outside click (same pattern as SidebarHeader line 266-283) - Close on Escape key (same pattern as SidebarHeader line 286-295) Render: - Trigger button: - `ConnectionStatusBadge` for active context - Display label: `'Local'` for `contextId === 'local'`, strip `'ssh-'` prefix for SSH (e.g., `ssh-192.168.1.10` -> `192.168.1.10`) - `ChevronDown` icon (rotate-180 when open) - `disabled={isContextSwitching}` and `opacity-50` when switching - `style={{ WebkitAppRegion: 'no-drag' }}` (sits in drag region) - Dropdown panel (when `isOpen && !isContextSwitching`): - Fixed backdrop overlay `className="fixed inset-0 z-10"` (same as SidebarHeader) - Panel: `absolute inset-x-4 top-full z-20 mt-1 max-h-[250px] overflow-y-auto rounded-lg py-1 shadow-xl` - Background: `var(--color-surface-sidebar)`, border: `var(--color-border)` - Header label: "Switch Workspace" (same style as SidebarHeader "Switch Repository") - For each context in `availableContexts`: - Button with hover state (follow ProjectDropdownItem pattern) - `ConnectionStatusBadge` icon - Label text (Local or host name) - `Check` icon if `ctx.id === activeContextId` - onClick: `switchContext(ctx.id)` then `setIsOpen(false)` Use theme CSS variables throughout (no hardcoded colors except green-400/red-400 for status). Run `pnpm typecheck` - zero errors. Verify ContextSwitcher.tsx, ConnectionStatusBadge.tsx, and updated contextSlice.ts exist with correct exports. Confirm `availableContexts` is in the ContextSlice interface. ContextSwitcher renders a dropdown listing Local + SSH contexts with status badges. ConnectionStatusBadge shows 4 distinct states. contextSlice tracks availableContexts fetched from context.list() IPC. All three files type-check cleanly. Task 2: Wire ContextSwitcher into SidebarHeader and add keyboard shortcut src/renderer/components/layout/SidebarHeader.tsx src/renderer/hooks/useKeyboardShortcuts.ts src/renderer/App.tsx **1. Modify SidebarHeader.tsx - Add ContextSwitcher to Row 1:** Import `ContextSwitcher` from `'../common/ContextSwitcher'`. In the Row 1 div (the one with `height: HEADER_ROW1_HEIGHT`, `paddingLeft: var(--macos-traffic-light-padding-left)`), add `ContextSwitcher` as the FIRST child before the existing project name button: ```tsx {/* ROW 1: Project Identity (Title Bar / Drag Region) */}
{/* NEW: Context Switcher - workspace indicator */} {/* Existing: Project name dropdown button */} {/* Existing: Collapse sidebar button */}
``` The ContextSwitcher already has `WebkitAppRegion: 'no-drag'` on its button, and the Row 1 div is the drag region, so it integrates naturally. Add a small visual separator between ContextSwitcher and project name. Use a `
` with `className="mx-1 h-4 w-px"` and `style={{ backgroundColor: 'var(--color-border)' }}` between them. This gives a subtle vertical line separating "workspace" from "project". Ensure the Row 1 layout uses `gap-2` or appropriate spacing so the new element fits without overflow. The existing `justify-between` may need adjustment - change to `gap-2` and keep the collapse button at the right with `ml-auto`. **2. Modify useKeyboardShortcuts.ts - Add Cmd+Shift+K:** Add to useShallow selector: - `availableContexts: s.availableContexts` - `activeContextId: s.activeContextId` - `switchContext: s.switchContext` - `isContextSwitching: s.isContextSwitching` IMPORTANT: The Cmd+Shift+K check MUST come BEFORE the existing Cmd+K check (line 209). Since both match `event.key === 'k'`, the shiftKey variant must be tested first: ```typescript // Cmd+Shift+K: Cycle to next workspace context if (event.key === 'k' && event.shiftKey) { event.preventDefault(); if (!isContextSwitching && availableContexts.length > 1) { const currentIndex = availableContexts.findIndex(c => c.id === activeContextId); const nextIndex = (currentIndex + 1) % availableContexts.length; void switchContext(availableContexts[nextIndex].id); } return; } // Cmd+K: Open command palette for global search (existing) if (event.key === 'k') { ... } ``` Add `availableContexts`, `activeContextId`, `switchContext`, `isContextSwitching` to the useEffect dependency array. **3. Modify App.tsx - Refresh contexts on SSH status change:** Add a useEffect that listens for SSH status changes and refreshes the available contexts list: ```typescript // Refresh available contexts when SSH connection state changes useEffect(() => { if (!window.electronAPI.ssh?.onStatus) return; const cleanup = window.electronAPI.ssh.onStatus(() => { void useStore.getState().fetchAvailableContexts(); }); return cleanup; }, []); ``` This ensures the ContextSwitcher dropdown always reflects current SSH connections. Place this after the existing context system initialization effect. Run `pnpm typecheck` - zero errors. Run `pnpm test` - all tests pass. Run `pnpm build` - production build succeeds. Verify SidebarHeader renders ContextSwitcher in Row 1. Verify useKeyboardShortcuts handles Cmd+Shift+K before Cmd+K. SidebarHeader Row 1 shows [ContextSwitcher | separator | ProjectName | CollapseBtn]. Cmd+Shift+K cycles through available contexts. SSH status changes trigger context list refresh. No regressions in existing tests or type checking. 1. `pnpm typecheck` passes with zero errors 2. `pnpm test` passes with no regressions 3. `pnpm build` succeeds 4. ContextSwitcher.tsx exports a component that renders a dropdown 5. ConnectionStatusBadge.tsx exports a component with 4 visual states 6. SidebarHeader.tsx imports and renders ContextSwitcher in Row 1 7. useKeyboardShortcuts.ts checks Cmd+Shift+K BEFORE Cmd+K 8. contextSlice.ts has availableContexts state and fetchAvailableContexts action 9. App.tsx has SSH status listener that refreshes available contexts - Context switcher visible in sidebar header showing active workspace with status icon - Dropdown lists Local + all connected SSH workspaces - Connection status icons: Monitor (local), Wifi/green (connected), Spinner (connecting), WifiOff/muted (disconnected), WifiOff/red (error) - Clicking a workspace triggers context switch with overlay - Cmd+Shift+K cycles through available workspaces - Switcher disabled during active context switch - Available contexts refresh when SSH status changes After completion, create `.planning/phases/04-workspace-ui/04-01-SUMMARY.md`