agent-ecosystem/.planning/phases/04-workspace-ui/04-01-PLAN.md
matt fa62433219 docs(04): create workspace UI phase plans
Plan 01: ContextSwitcher dropdown, ConnectionStatusBadge, SidebarHeader
integration, keyboard shortcut (Cmd+Shift+K), and availableContexts state.
Plan 02: WorkspaceSection settings for SSH profile CRUD with auto-refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:59:55 +00:00

13 KiB

phase plan type wave depends_on files_modified autonomous must_haves
04-workspace-ui 01 execute 1
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
true
truths artifacts key_links
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
path provides min_lines
src/renderer/components/common/ContextSwitcher.tsx Dropdown listing local + SSH contexts with status badges, switch-on-click 60
path provides min_lines
src/renderer/components/common/ConnectionStatusBadge.tsx Icon component rendering 4 connection states with distinct visual treatment 25
path provides
src/renderer/components/layout/SidebarHeader.tsx Modified Row 1 with ContextSwitcher before project name
path provides
src/renderer/hooks/useKeyboardShortcuts.ts Cmd+Shift+K shortcut for context cycling
path provides
src/renderer/store/slices/contextSlice.ts availableContexts state + fetchAvailableContexts action
from to via pattern
src/renderer/components/common/ContextSwitcher.tsx src/renderer/store/slices/contextSlice.ts useStore consuming availableContexts, activeContextId, switchContext, isContextSwitching useStore.*availableContexts|switchContext
from to via pattern
src/renderer/components/common/ConnectionStatusBadge.tsx src/renderer/store/slices/connectionSlice.ts useStore consuming connectionState for SSH contexts connectionState
from to via pattern
src/renderer/components/layout/SidebarHeader.tsx src/renderer/components/common/ContextSwitcher.tsx ContextSwitcher rendered in Row 1 <ContextSwitcher
from to via pattern
src/renderer/hooks/useKeyboardShortcuts.ts src/renderer/store/slices/contextSlice.ts Cmd+Shift+K triggers switchContext with next context from availableContexts shiftKey.*switchContext|availableContexts
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.

<execution_context> @/home/bskim/.claude/get-shit-done/workflows/execute-plan.md @/home/bskim/.claude/get-shit-done/templates/summary.md </execution_context>

@.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<void>

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:

import { Loader2, Monitor, Wifi, WifiOff } from 'lucide-react';
import { useStore } from '@renderer/store';

Props: { contextId: string; className?: string }

Logic:

  • If contextId === 'local': render <Monitor className="size-3.5 text-text-muted" />
  • 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': <Wifi className="size-3.5 text-green-400" />
    • 'connecting': <Loader2 className="size-3.5 animate-spin text-text-muted" />
    • 'disconnected': <WifiOff className="size-3.5 text-text-muted" />
    • 'error': <WifiOff className="size-3.5 text-red-400" />

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<HTMLDivElement>

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:

{/* ROW 1: Project Identity (Title Bar / Drag Region) */}
<div ref={projectDropdownRef} className="relative flex select-none items-center justify-between pr-2" ...>
  {/* NEW: Context Switcher - workspace indicator */}
  <ContextSwitcher />

  {/* Existing: Project name dropdown button */}
  <button onClick={() => setIsProjectDropdownOpen(...)} ...>
    ...
  </button>

  {/* Existing: Collapse sidebar button */}
  <button onClick={toggleSidebar} ...>
    ...
  </button>
</div>

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 <div> 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:

// 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:

// 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/04-workspace-ui/04-01-SUMMARY.md`