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>
13 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-workspace-ui | 01 | execute | 1 |
|
true |
|
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[](importContextInfofrom@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
connectionStateandconnectedHostfrom store viauseStore- Determine if this specific SSH context matches the connected host:
contextId === 'ssh-' + connectedHost - If match: use
connectionStatevalue - If no match: treat as
'disconnected'
- Determine if this specific SSH context matches the connected host:
- 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:
ConnectionStatusBadgefor active context- Display label:
'Local'forcontextId === 'local', strip'ssh-'prefix for SSH (e.g.,ssh-192.168.1.10->192.168.1.10) ChevronDownicon (rotate-180 when open)disabled={isContextSwitching}andopacity-50when switchingstyle={{ 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)
ConnectionStatusBadgeicon- Label text (Local or host name)
Checkicon ifctx.id === activeContextId- onClick:
switchContext(ctx.id)thensetIsOpen(false)
- Fixed backdrop overlay
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.
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.availableContextsactiveContextId: s.activeContextIdswitchContext: s.switchContextisContextSwitching: 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.
<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>