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>
256 lines
11 KiB
Markdown
256 lines
11 KiB
Markdown
---
|
|
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>
|