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>
This commit is contained in:
matt 2026-02-12 01:59:55 +00:00
parent 6df422ecef
commit fa62433219
3 changed files with 572 additions and 15 deletions

View file

@ -13,8 +13,8 @@ This roadmap transforms claude-devtools from a single-mode application (local XO
Decimal phases appear between their surrounding integers in numeric order. Decimal phases appear between their surrounding integers in numeric order.
- [x] **Phase 1: Provider Plumbing** - Fix SSH session parsing and subagent loading ✓ 2026-02-12 - [x] **Phase 1: Provider Plumbing** - Fix SSH session parsing and subagent loading ✓ 2026-02-12
- [ ] **Phase 2: Service Infrastructure** - ServiceContextRegistry and IPC context API - [x] **Phase 2: Service Infrastructure** - ServiceContextRegistry and IPC context API ✓ 2026-02-12
- [ ] **Phase 3: State Management** - Snapshot/restore system for instant switching - [x] **Phase 3: State Management** - Snapshot/restore system for instant switching ✓ 2026-02-12
- [ ] **Phase 4: Workspace UI** - Context switcher and connection profiles - [ ] **Phase 4: Workspace UI** - Context switcher and connection profiles
## Phase Details ## Phase Details
@ -45,9 +45,9 @@ Plans:
**Plans**: 3 plans **Plans**: 3 plans
Plans: Plans:
- [ ] 02-01-PLAN.md — ServiceContext bundle class, ServiceContextRegistry coordinator, and dispose() methods for FileWatcher/DataCache - [x] 02-01-PLAN.md — ServiceContext bundle class, ServiceContextRegistry coordinator, and dispose() methods for FileWatcher/DataCache
- [ ] 02-02-PLAN.md — Wire registry into main/index.ts and update all IPC handlers to route via registry - [x] 02-02-PLAN.md — Wire registry into main/index.ts and update all IPC handlers to route via registry
- [ ] 02-03-PLAN.md — Context management IPC channels, preload bridge, and connection profiles in ConfigManager - [x] 02-03-PLAN.md — Context management IPC channels, preload bridge, and connection profiles in ConfigManager
### Phase 3: State Management ### Phase 3: State Management
**Goal**: Context switching preserves exact UI state per workspace with instant restoration **Goal**: Context switching preserves exact UI state per workspace with instant restoration
@ -59,11 +59,10 @@ Plans:
3. Previously visited context restores instantly without refetching data 3. Previously visited context restores instantly without refetching data
4. Loading overlay prevents stale data flash during context switch 4. Loading overlay prevents stale data flash during context switch
5. Context snapshots survive app restart (stored in IndexedDB) 5. Context snapshots survive app restart (stored in IndexedDB)
**Plans**: 1-2 plans **Plans**: 1 plan
Plans: Plans:
- [ ] 03-01: Context snapshot system and contextSlice - [x] 03-01-PLAN.md — Context snapshot system: contextSlice, IndexedDB storage, overlay, validation, and store wiring ✓
- [ ] 03-02: IndexedDB persistence with expiration handling
### Phase 4: Workspace UI ### Phase 4: Workspace UI
**Goal**: Users can visually manage and switch between workspaces with clear status indicators **Goal**: Users can visually manage and switch between workspaces with clear status indicators
@ -75,11 +74,11 @@ Plans:
3. Connection status indicators clearly show connected/connecting/disconnected/error states with distinct visual treatment 3. Connection status indicators clearly show connected/connecting/disconnected/error states with distinct visual treatment
4. User can save SSH connection as a profile, then reconnect to it later without re-entering credentials 4. User can save SSH connection as a profile, then reconnect to it later without re-entering credentials
5. User can switch workspaces using keyboard shortcut (Cmd/Ctrl+K or similar) 5. User can switch workspaces using keyboard shortcut (Cmd/Ctrl+K or similar)
**Plans**: 1-2 plans **Plans**: 2 plans
Plans: Plans:
- [ ] 04-01: ContextSwitcher component and status indicators - [ ] 04-01-PLAN.md — ContextSwitcher dropdown, ConnectionStatusBadge, SidebarHeader integration, and Cmd+Shift+K shortcut
- [ ] 04-02: Connection profiles UI in settings - [ ] 04-02-PLAN.md — WorkspaceSection settings for SSH profile CRUD with auto-refresh
## Progress ## Progress
@ -89,10 +88,10 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Provider Plumbing | 1/1 | ✓ Complete | 2026-02-12 | | 1. Provider Plumbing | 1/1 | ✓ Complete | 2026-02-12 |
| 2. Service Infrastructure | 0/3 | Not started | - | | 2. Service Infrastructure | 3/3 | ✓ Complete | 2026-02-12 |
| 3. State Management | 0/1-2 | Not started | - | | 3. State Management | 1/1 | ✓ Complete | 2026-02-12 |
| 4. Workspace UI | 0/1-2 | Not started | - | | 4. Workspace UI | 0/2 | Not started | - |
--- ---
*Roadmap created: 2026-02-12* *Roadmap created: 2026-02-12*
*Last updated: 2026-02-12 after Phase 2 planning complete* *Last updated: 2026-02-12 after Phase 4 planning complete*

View file

@ -0,0 +1,302 @@
---
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: "<ContextSwitcher"
- from: "src/renderer/hooks/useKeyboardShortcuts.ts"
to: "src/renderer/store/slices/contextSlice.ts"
via: "Cmd+Shift+K triggers switchContext with next context from availableContexts"
pattern: "shiftKey.*switchContext|availableContexts"
---
<objective>
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.
</objective>
<execution_context>
@/home/bskim/.claude/get-shit-done/workflows/execute-plan.md
@/home/bskim/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ConnectionStatusBadge and ContextSwitcher components</name>
<files>
src/renderer/components/common/ConnectionStatusBadge.tsx
src/renderer/components/common/ContextSwitcher.tsx
src/renderer/store/slices/contextSlice.ts
</files>
<action>
**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:
```typescript
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).
</action>
<verify>
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.
</verify>
<done>
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.
</done>
</task>
<task type="auto">
<name>Task 2: Wire ContextSwitcher into SidebarHeader and add keyboard shortcut</name>
<files>
src/renderer/components/layout/SidebarHeader.tsx
src/renderer/hooks/useKeyboardShortcuts.ts
src/renderer/App.tsx
</files>
<action>
**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) */}
<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:
```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.
</action>
<verify>
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.
</verify>
<done>
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.
</done>
</task>
</tasks>
<verification>
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
</verification>
<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>
<output>
After completion, create `.planning/phases/04-workspace-ui/04-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,256 @@
---
phase: 04-workspace-ui
plan: 02
type: execute
wave: 2
depends_on: ["04-01"]
files_modified:
- src/renderer/components/settings/sections/WorkspaceSection.tsx
- src/renderer/components/settings/sections/index.ts
- src/renderer/components/settings/SettingsTabs.tsx
- src/renderer/components/settings/SettingsView.tsx
autonomous: true
must_haves:
truths:
- "User can see a 'Workspace' tab in settings that lists saved SSH connection profiles"
- "User can add a new SSH profile with name, host, port, username, and auth method"
- "User can edit an existing SSH profile's fields"
- "User can delete an SSH profile with confirmation"
- "Profile changes persist across app restarts (stored via ConfigManager)"
- "After saving/deleting a profile, the context switcher dropdown reflects the change"
artifacts:
- path: "src/renderer/components/settings/sections/WorkspaceSection.tsx"
provides: "CRUD UI for SSH connection profiles following NotificationTriggerSettings pattern"
min_lines: 100
- path: "src/renderer/components/settings/sections/index.ts"
provides: "Barrel export including WorkspaceSection"
- path: "src/renderer/components/settings/SettingsTabs.tsx"
provides: "New 'workspace' tab option in settings tabs"
- path: "src/renderer/components/settings/SettingsView.tsx"
provides: "WorkspaceSection rendered when workspace tab active"
key_links:
- from: "src/renderer/components/settings/sections/WorkspaceSection.tsx"
to: "window.electronAPI.config"
via: "config.get() reads profiles, config.update('ssh', ...) writes profiles"
pattern: "config\\.get|config\\.update.*ssh"
- from: "src/renderer/components/settings/sections/WorkspaceSection.tsx"
to: "window.electronAPI.context.list"
via: "Refreshes available contexts after profile save/delete"
pattern: "context\\.list|fetchAvailableContexts"
- from: "src/renderer/components/settings/SettingsView.tsx"
to: "src/renderer/components/settings/sections/WorkspaceSection.tsx"
via: "Conditionally renders WorkspaceSection when activeSection === 'workspace'"
pattern: "<WorkspaceSection"
---
<objective>
Create the SSH connection profiles settings section for managing saved workspaces.
Purpose: Users need a persistent way to save, edit, and delete SSH connection profiles so they can reconnect to previously configured remote machines without re-entering credentials. This completes the workspace management story alongside the context switcher from Plan 01.
Output: WorkspaceSection in settings with full CRUD for SSH profiles, integrated into settings tabs, with automatic context list refresh on changes.
</objective>
<execution_context>
@/home/bskim/.claude/get-shit-done/workflows/execute-plan.md
@/home/bskim/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-workspace-ui/04-RESEARCH.md
@.planning/phases/04-workspace-ui/04-01-SUMMARY.md
@src/renderer/components/settings/SettingsView.tsx
@src/renderer/components/settings/SettingsTabs.tsx
@src/renderer/components/settings/sections/ConnectionSection.tsx
@src/renderer/components/settings/sections/index.ts
@src/shared/types/api.ts
@src/main/services/infrastructure/ConfigManager.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Create WorkspaceSection settings component with SSH profile CRUD</name>
<files>
src/renderer/components/settings/sections/WorkspaceSection.tsx
</files>
<action>
Create a settings section following the pattern established by ConnectionSection and NotificationTriggerSettings.
**Imports:**
```typescript
import { useCallback, useEffect, useState } from 'react';
import { useStore } from '@renderer/store';
import { Edit2, Loader2, Plus, Save, Server, Trash2, X } from 'lucide-react';
import { SettingsSectionHeader } from '../components/SettingsSectionHeader';
import type { SshConnectionProfile, SshAuthMethod } from '@shared/types';
```
**State management:**
- `profiles: SshConnectionProfile[]` - loaded from config
- `loading: boolean` - initial load state
- `editingId: string | null` - profile being edited (null = not editing)
- `showAddForm: boolean` - new profile form visibility
- Form state: `formName`, `formHost`, `formPort`, `formUsername`, `formAuthMethod`, `formPrivateKeyPath`
**Profile loading:**
On mount, call `window.electronAPI.config.get()` and extract `config.ssh?.profiles ?? []`. Set into `profiles` state.
Create `loadProfiles` callback that refetches from config and updates state. Call after every CRUD operation.
**Add profile handler:**
- Generate ID: `crypto.randomUUID()` (available in renderer)
- Build profile object: `{ id, name: formName, host: formHost, port: parseInt(formPort) || 22, username: formUsername, authMethod: formAuthMethod, privateKeyPath: formAuthMethod === 'privateKey' ? formPrivateKeyPath : undefined }`
- Save: `await window.electronAPI.config.update('ssh', { profiles: [...profiles, newProfile] })`
- After save: `await loadProfiles()`, reset form, `setShowAddForm(false)`
- After save: `void useStore.getState().fetchAvailableContexts()` to refresh context switcher
**Edit profile handler:**
- Populate form fields from selected profile when `editingId` changes
- Save: Replace profile in array by id, `await window.electronAPI.config.update('ssh', { profiles: updatedProfiles })`
- After save: `await loadProfiles()`, `setEditingId(null)`
- After save: `void useStore.getState().fetchAvailableContexts()`
**Delete profile handler:**
- Filter profile out: `profiles.filter(p => p.id !== id)`
- Save: `await window.electronAPI.config.update('ssh', { profiles: filtered })`
- After delete: `await loadProfiles()`
- After delete: `void useStore.getState().fetchAvailableContexts()`
**UI Structure:**
```
SettingsSectionHeader title="Workspace Profiles"
Description text: "Save SSH connection profiles for quick reconnection"
{loading && Loader2 spinner}
{!loading && profiles.length === 0 && empty state message}
{profiles.map(profile => (
ProfileCard:
- If editingId === profile.id: render inline edit form
- Else: render display card with:
- Server icon + profile.name (bold)
- profile.username@profile.host:profile.port (muted text)
- Auth method badge (muted)
- Edit button (Edit2 icon)
- Delete button (Trash2 icon, confirm with window.confirm())
))}
{showAddForm ? (
Add Profile Form:
- Name input (required)
- Host input (required)
- Port input (default 22)
- Username input (required)
- Auth method select (auto/agent/privateKey/password)
- Private key path input (conditional on authMethod === 'privateKey')
- Save button + Cancel button
) : (
Add Profile button (Plus icon)
)}
```
**Styling:**
- Use `var(--color-surface-raised)` for card backgrounds
- Use `var(--color-border)` for card borders
- Use `var(--color-text)`, `var(--color-text-secondary)`, `var(--color-text-muted)` for text hierarchy
- Input styling: same as ConnectionSection (`inputClass` and `inputStyle` pattern)
- Buttons: same styling as ConnectionSection action buttons
- Cards: `rounded-md border p-4 space-y-2` with surface-raised background
</action>
<verify>
Run `pnpm typecheck` - zero errors. Verify WorkspaceSection.tsx exists and exports the component. Confirm it imports SshConnectionProfile from shared types.
</verify>
<done>
WorkspaceSection renders a list of saved SSH profiles with add/edit/delete functionality. Profile changes are persisted via ConfigManager and trigger context list refresh. Component follows existing settings patterns.
</done>
</task>
<task type="auto">
<name>Task 2: Wire WorkspaceSection into SettingsView and SettingsTabs</name>
<files>
src/renderer/components/settings/sections/index.ts
src/renderer/components/settings/SettingsTabs.tsx
src/renderer/components/settings/SettingsView.tsx
</files>
<action>
**1. Update sections/index.ts barrel export:**
Add: `export { WorkspaceSection } from './WorkspaceSection';`
**2. Update SettingsTabs.tsx:**
Add `'workspace'` to the `SettingsSection` type:
```typescript
export type SettingsSection = 'general' | 'connection' | 'workspace' | 'notifications' | 'advanced';
```
Add workspace tab to the `tabs` array, positioned after 'connection':
```typescript
{ id: 'workspace', label: 'Workspaces', icon: Server },
```
Import `Server` from lucide-react (it may already be imported - check first, do not duplicate).
The tabs array order should be: general, connection, workspace, notifications, advanced.
**3. Update SettingsView.tsx:**
Import `WorkspaceSection` from the sections barrel:
```typescript
import {
AdvancedSection,
ConnectionSection,
GeneralSection,
NotificationsSection,
WorkspaceSection,
} from './sections';
```
Add the workspace section render block in the content area, between connection and notifications:
```tsx
{activeSection === 'workspace' && <WorkspaceSection />}
```
The component takes no props (it manages its own state internally, similar to ConnectionSection).
</action>
<verify>
Run `pnpm typecheck` - zero errors. Run `pnpm test` - all tests pass. Run `pnpm build` - production build succeeds. Verify SettingsTabs includes 'workspace' option. Verify SettingsView renders WorkspaceSection when workspace tab is active.
</verify>
<done>
Settings view has a "Workspaces" tab showing SSH profile management. Tab sits between Connection and Notifications in the tab bar. The full CRUD flow works: add profile -> appears in list -> edit fields -> save -> delete with confirm. Profile changes refresh context switcher dropdown automatically.
</done>
</task>
</tasks>
<verification>
1. `pnpm typecheck` passes with zero errors
2. `pnpm test` passes with no regressions
3. `pnpm build` succeeds
4. WorkspaceSection.tsx exists with CRUD operations for SSH profiles
5. SettingsTabs.tsx includes 'workspace' in SettingsSection type
6. SettingsView.tsx renders WorkspaceSection when workspace tab is active
7. sections/index.ts exports WorkspaceSection
8. Profile add/edit/delete calls config.update('ssh', ...) and fetchAvailableContexts()
</verification>
<success_criteria>
- "Workspaces" tab visible in settings between Connection and Notifications
- Empty state shown when no profiles saved
- User can add SSH profile with name, host, port, username, auth method
- User can inline-edit existing profile fields
- User can delete profile with confirmation dialog
- Profile changes persist via ConfigManager (survive app restart)
- After any profile change, context switcher dropdown refreshes automatically
</success_criteria>
<output>
After completion, create `.planning/phases/04-workspace-ui/04-02-SUMMARY.md`
</output>