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:
parent
6df422ecef
commit
fa62433219
3 changed files with 572 additions and 15 deletions
|
|
@ -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.
|
||||
|
||||
- [x] **Phase 1: Provider Plumbing** - Fix SSH session parsing and subagent loading ✓ 2026-02-12
|
||||
- [ ] **Phase 2: Service Infrastructure** - ServiceContextRegistry and IPC context API
|
||||
- [ ] **Phase 3: State Management** - Snapshot/restore system for instant switching
|
||||
- [x] **Phase 2: Service Infrastructure** - ServiceContextRegistry and IPC context API ✓ 2026-02-12
|
||||
- [x] **Phase 3: State Management** - Snapshot/restore system for instant switching ✓ 2026-02-12
|
||||
- [ ] **Phase 4: Workspace UI** - Context switcher and connection profiles
|
||||
|
||||
## Phase Details
|
||||
|
|
@ -45,9 +45,9 @@ Plans:
|
|||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 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
|
||||
- [ ] 02-03-PLAN.md — Context management IPC channels, preload bridge, and connection profiles in ConfigManager
|
||||
- [x] 02-01-PLAN.md — ServiceContext bundle class, ServiceContextRegistry coordinator, and dispose() methods for FileWatcher/DataCache ✓
|
||||
- [x] 02-02-PLAN.md — Wire registry into main/index.ts and update all IPC handlers to route via registry ✓
|
||||
- [x] 02-03-PLAN.md — Context management IPC channels, preload bridge, and connection profiles in ConfigManager ✓
|
||||
|
||||
### Phase 3: State Management
|
||||
**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
|
||||
4. Loading overlay prevents stale data flash during context switch
|
||||
5. Context snapshots survive app restart (stored in IndexedDB)
|
||||
**Plans**: 1-2 plans
|
||||
**Plans**: 1 plan
|
||||
|
||||
Plans:
|
||||
- [ ] 03-01: Context snapshot system and contextSlice
|
||||
- [ ] 03-02: IndexedDB persistence with expiration handling
|
||||
- [x] 03-01-PLAN.md — Context snapshot system: contextSlice, IndexedDB storage, overlay, validation, and store wiring ✓
|
||||
|
||||
### Phase 4: Workspace UI
|
||||
**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
|
||||
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)
|
||||
**Plans**: 1-2 plans
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 04-01: ContextSwitcher component and status indicators
|
||||
- [ ] 04-02: Connection profiles UI in settings
|
||||
- [ ] 04-01-PLAN.md — ContextSwitcher dropdown, ConnectionStatusBadge, SidebarHeader integration, and Cmd+Shift+K shortcut
|
||||
- [ ] 04-02-PLAN.md — WorkspaceSection settings for SSH profile CRUD with auto-refresh
|
||||
|
||||
## Progress
|
||||
|
||||
|
|
@ -89,10 +88,10 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
|
|||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Provider Plumbing | 1/1 | ✓ Complete | 2026-02-12 |
|
||||
| 2. Service Infrastructure | 0/3 | Not started | - |
|
||||
| 3. State Management | 0/1-2 | Not started | - |
|
||||
| 4. Workspace UI | 0/1-2 | Not started | - |
|
||||
| 2. Service Infrastructure | 3/3 | ✓ Complete | 2026-02-12 |
|
||||
| 3. State Management | 1/1 | ✓ Complete | 2026-02-12 |
|
||||
| 4. Workspace UI | 0/2 | Not started | - |
|
||||
|
||||
---
|
||||
*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*
|
||||
|
|
|
|||
302
.planning/phases/04-workspace-ui/04-01-PLAN.md
Normal file
302
.planning/phases/04-workspace-ui/04-01-PLAN.md
Normal 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>
|
||||
256
.planning/phases/04-workspace-ui/04-02-PLAN.md
Normal file
256
.planning/phases/04-workspace-ui/04-02-PLAN.md
Normal 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>
|
||||
Loading…
Reference in a new issue