feat(planning): add project planning documents and configuration for SSH multi-context workspaces
- Introduced new planning files including PROJECT.md, REQUIREMENTS.md, ROADMAP.md, and STATE.md to outline the vision and requirements for SSH multi-context workspaces. - Added ARCHITECTURE.md and CONCERNS.md to detail the codebase structure and address technical debt, known bugs, and security considerations. - Created CONVENTIONS.md to establish coding standards and practices for the project. - Updated .gitignore to exclude demo files and added configuration for planning tools. This commit lays the groundwork for enhancing SSH functionality and user experience in managing multiple workspaces.
This commit is contained in:
parent
c12b3295e9
commit
ae4833e310
22 changed files with 5395 additions and 20 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -45,3 +45,4 @@ temp/
|
|||
|
||||
|
||||
eslint-fix/
|
||||
demo/*
|
||||
72
.planning/PROJECT.md
Normal file
72
.planning/PROJECT.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# SSH Multi-Context Workspaces
|
||||
|
||||
## What This Is
|
||||
|
||||
A workspace-based multi-context system for claude-devtools that treats local and SSH connections as independent, switchable screens. Local mode is always alive. Each SSH connection is an independent workspace with its own projects, sessions, tabs, notifications, and state — freely switchable with instant state preservation. Like having multiple monitors you flip between.
|
||||
|
||||
## Core Value
|
||||
|
||||
Users can seamlessly switch between local and any number of SSH workspaces without losing state, and SSH sessions actually load their conversation history.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
|
||||
- SSH connection establishment and SFTP file system provider — existing
|
||||
- Session discovery over SSH (sidebar lists remote sessions) — existing
|
||||
- SSH connection settings UI — existing
|
||||
- SSH auto-reconnect and last-connection persistence — existing
|
||||
|
||||
### Active
|
||||
|
||||
- [ ] SSH sessions display full conversation history (fix provider plumbing bug)
|
||||
- [ ] SSH subagent drill-down loads correctly over SFTP
|
||||
- [ ] Multiple service contexts coexist (local always alive, SSH contexts independent)
|
||||
- [ ] Context switching preserves full state (tabs, sidebar, notifications, selections)
|
||||
- [ ] Workspace switcher in sidebar for switching between contexts
|
||||
- [ ] Status bar indicator showing active workspace and connection status
|
||||
- [ ] SSH file watchers stay alive in background while connected
|
||||
- [ ] Loading overlay during context switch (prevent stale data flash)
|
||||
- [ ] Switching to local restores all previous local state exactly
|
||||
- [ ] Connection profiles saved in settings for quick reconnection
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- SFTP performance optimization — investigating separately after workspace switching works
|
||||
- SSH key management UI — use system SSH keys
|
||||
- Multiple simultaneous SSH sessions visible side-by-side — workspace model is one-at-a-time switching
|
||||
- Mobile/responsive layout for SSH features — desktop only
|
||||
|
||||
## Context
|
||||
|
||||
The existing SSH integration (commits 4b56186, 921420b, ad4e75b) established SFTP-based file system access and connection management. However, several critical issues surfaced:
|
||||
|
||||
1. **"No conversation history" bug**: `SessionParser`, `SubagentResolver`, and `SubagentDetailBuilder` don't pass the SSH `FileSystemProvider` to `parseJsonlFile()`, so JSONL parsing falls back to local filesystem and finds nothing.
|
||||
|
||||
2. **Stale sidebar during transitions**: Fire-and-forget fetches with no loading state means old local data is visible while SSH data loads.
|
||||
|
||||
3. **Destructive mode switch**: Connecting SSH destroys local services; disconnecting destroys SSH state. No way to switch back without re-fetching everything.
|
||||
|
||||
Reference design document: `~/.claude/plans/mighty-soaring-moore.md` — contains detailed implementation notes for all 4 phases including specific file changes, line numbers, and code patterns.
|
||||
|
||||
Brownfield codebase map: `.planning/codebase/` — 7 documents covering stack, architecture, structure, conventions, testing, integrations, and concerns.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Tech stack**: Electron 28.x, React 18.x, TypeScript 5.x, Zustand 4.x — no new frameworks
|
||||
- **Architecture**: Must use existing IPC bridge pattern (main process services + preload API + renderer store)
|
||||
- **Backward compatibility**: Local-only mode must work identically to current behavior
|
||||
- **State isolation**: Each workspace must have fully independent Zustand state slices
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Rationale | Outcome |
|
||||
|----------|-----------|---------|
|
||||
| ServiceContextRegistry in main process | Centralizes context lifecycle, avoids scattered service variable management | -- Pending |
|
||||
| Snapshot/restore for Zustand state | Instant switching without refetching; preserves exact user state | -- Pending |
|
||||
| Workspace indicators in sidebar + status bar | Sidebar for active switching, status bar for passive awareness (VS Code model) | -- Pending |
|
||||
| SSH watchers stay alive in background | Users expect real-time updates even for non-active workspaces; disconnect is explicit | -- Pending |
|
||||
| Performance optimization deferred | Focus on correctness and UX first, investigate SFTP latency separately | -- Pending |
|
||||
|
||||
---
|
||||
*Last updated: 2026-02-12 after initialization*
|
||||
114
.planning/REQUIREMENTS.md
Normal file
114
.planning/REQUIREMENTS.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Requirements: SSH Multi-Context Workspaces
|
||||
|
||||
**Defined:** 2026-02-12
|
||||
**Core Value:** Users can seamlessly switch between local and SSH workspaces without losing state, and SSH sessions actually load their conversation history.
|
||||
|
||||
## v1 Requirements
|
||||
|
||||
Requirements for initial release. Each maps to roadmap phases.
|
||||
|
||||
### Provider Plumbing
|
||||
|
||||
- [ ] **PROV-01**: SSH sessions display full conversation history (fix FileSystemProvider plumbing through SessionParser)
|
||||
- [ ] **PROV-02**: SSH subagent drill-down loads correctly over SFTP (fix SubagentResolver and SubagentDetailBuilder provider plumbing)
|
||||
|
||||
### Service Context Management
|
||||
|
||||
- [ ] **SCTX-01**: ServiceContextRegistry manages local + N SSH service contexts with getActive/switch/create/destroy lifecycle
|
||||
- [ ] **SCTX-02**: Local service context is always alive and never destroyed on SSH connect/disconnect
|
||||
- [ ] **SCTX-03**: Each SSH context has independent services (ProjectScanner, SessionParser, SubagentResolver, ChunkBuilder, DataCache, FileWatcher)
|
||||
- [ ] **SCTX-04**: All services implement dispose() for proper cleanup (EventEmitter listeners, timers, SSH connections)
|
||||
- [ ] **SCTX-05**: IPC requests are stamped with context ID to prevent race conditions during rapid switching
|
||||
|
||||
### IPC Context API
|
||||
|
||||
- [ ] **IPC-01**: Context management IPC channels exist (context:list, context:switch, context:connect-ssh, context:disconnect-ssh, context:destroy)
|
||||
- [ ] **IPC-02**: Preload exposes context API to renderer (window.electronAPI.context)
|
||||
- [ ] **IPC-03**: File watcher events are scoped to active context only
|
||||
|
||||
### State Snapshot/Restore
|
||||
|
||||
- [ ] **SNAP-01**: Full Zustand state snapshot captured on context switch (projects, sessions, tabs, pane layout, selections, notifications)
|
||||
- [ ] **SNAP-02**: Switching to a previously visited context restores exact state instantly (no refetch)
|
||||
- [ ] **SNAP-03**: Switching to a never-visited context shows empty/skeleton state (not stale local data)
|
||||
- [ ] **SNAP-04**: Loading overlay displayed during context switch to prevent stale data flash
|
||||
- [ ] **SNAP-05**: Context snapshots persist to IndexedDB (survive app restart)
|
||||
|
||||
### Workspace UI
|
||||
|
||||
- [ ] **WSUI-01**: Context switcher dropdown in sidebar lists Local + connected SSH workspaces
|
||||
- [ ] **WSUI-02**: Status bar indicator shows active workspace name and connection status
|
||||
- [ ] **WSUI-03**: Connection status indicators display connected/connecting/disconnected/error states
|
||||
- [ ] **WSUI-04**: SSH connection profiles can be saved, edited, and deleted in settings
|
||||
- [ ] **WSUI-05**: Keyboard shortcuts available for switching between workspaces
|
||||
|
||||
## v2 Requirements
|
||||
|
||||
Deferred to future release. Tracked but not in current roadmap.
|
||||
|
||||
### Enhanced UX
|
||||
|
||||
- **WSUI-06**: Color-coded workspace indicators per SSH connection
|
||||
- **WSUI-07**: Fuzzy search in workspace switcher for quick filtering
|
||||
- **WSUI-08**: Auto-reconnect with exponential backoff on SSH disconnect
|
||||
- **WSUI-09**: Workspace health metrics (latency, connection uptime)
|
||||
|
||||
### Advanced Capabilities
|
||||
|
||||
- **ADV-01**: Parallel workspace windows (view two workspaces simultaneously)
|
||||
- **ADV-02**: Cross-context session comparison (local vs remote side-by-side)
|
||||
- **ADV-03**: Workspace groups (organize related SSH connections)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| SFTP performance optimization | Investigating separately after workspace switching works |
|
||||
| SSH key management UI | Use system SSH keys, not app-managed |
|
||||
| Multi-window side-by-side workspaces | v2+ complexity; current model is one-at-a-time switching |
|
||||
| Mobile/responsive layout | Desktop-only app |
|
||||
| BroadcastChannel multi-window sync | Single window for v1; multi-window is v2+ |
|
||||
| Offline-first workspace caching | Would require conflict resolution strategy; defer |
|
||||
|
||||
## Traceability
|
||||
|
||||
Which phases cover which requirements. Updated during roadmap creation.
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| PROV-01 | Phase 1 | Pending |
|
||||
| PROV-02 | Phase 1 | Pending |
|
||||
| SCTX-01 | Phase 2 | Pending |
|
||||
| SCTX-02 | Phase 2 | Pending |
|
||||
| SCTX-03 | Phase 2 | Pending |
|
||||
| SCTX-04 | Phase 2 | Pending |
|
||||
| SCTX-05 | Phase 2 | Pending |
|
||||
| IPC-01 | Phase 2 | Pending |
|
||||
| IPC-02 | Phase 2 | Pending |
|
||||
| IPC-03 | Phase 2 | Pending |
|
||||
| SNAP-01 | Phase 3 | Pending |
|
||||
| SNAP-02 | Phase 3 | Pending |
|
||||
| SNAP-03 | Phase 3 | Pending |
|
||||
| SNAP-04 | Phase 3 | Pending |
|
||||
| SNAP-05 | Phase 3 | Pending |
|
||||
| WSUI-01 | Phase 4 | Pending |
|
||||
| WSUI-02 | Phase 4 | Pending |
|
||||
| WSUI-03 | Phase 4 | Pending |
|
||||
| WSUI-04 | Phase 4 | Pending |
|
||||
| WSUI-05 | Phase 4 | Pending |
|
||||
|
||||
**Coverage:**
|
||||
- v1 requirements: 20 total
|
||||
- Mapped to phases: 20
|
||||
- Unmapped: 0
|
||||
|
||||
**Validation:**
|
||||
- Phase 1: 2 requirements (PROV-01, PROV-02)
|
||||
- Phase 2: 8 requirements (SCTX-01 through SCTX-05, IPC-01 through IPC-03)
|
||||
- Phase 3: 5 requirements (SNAP-01 through SNAP-05)
|
||||
- Phase 4: 5 requirements (WSUI-01 through WSUI-05)
|
||||
- Total: 20 requirements mapped
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-02-12*
|
||||
*Last updated: 2026-02-12 after roadmap creation (traceability complete)*
|
||||
98
.planning/ROADMAP.md
Normal file
98
.planning/ROADMAP.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Roadmap: SSH Multi-Context Workspaces
|
||||
|
||||
## Overview
|
||||
|
||||
This roadmap transforms claude-devtools from a single-mode application (local XOR SSH) into a true multi-context workspace system where local mode is always alive and each SSH connection is an independent, switchable workspace with full state preservation. Phase 1 fixes the critical "no conversation history" bug by plumbing FileSystemProvider through all parsing services. Phase 2 establishes ServiceContextRegistry infrastructure to manage multiple independent service contexts. Phase 3 implements snapshot-based state management for instant context switching. Phase 4 delivers the workspace switcher UI and connection profiles.
|
||||
|
||||
## Phases
|
||||
|
||||
**Phase Numbering:**
|
||||
- Integer phases (1, 2, 3, 4): Planned milestone work
|
||||
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||
|
||||
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
|
||||
- [ ] **Phase 4: Workspace UI** - Context switcher and connection profiles
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Provider Plumbing
|
||||
**Goal**: SSH sessions display full conversation history and subagent drill-down works correctly
|
||||
**Depends on**: Nothing (first phase)
|
||||
**Requirements**: PROV-01, PROV-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can open an SSH session and see full conversation history (not "No conversation history" message)
|
||||
2. User can drill down into subagents within SSH sessions and view their execution details
|
||||
3. JSONL file parsing uses SSH FileSystemProvider when in SSH mode (not falling back to local filesystem)
|
||||
**Plans**: 1 plan
|
||||
|
||||
Plans:
|
||||
- [x] 01-01-PLAN.md — Thread FileSystemProvider through parsing stack (SessionParser, SubagentResolver, SubagentDetailBuilder) ✓
|
||||
|
||||
### Phase 2: Service Infrastructure
|
||||
**Goal**: Multiple service contexts coexist with proper lifecycle management and IPC routing
|
||||
**Depends on**: Phase 1
|
||||
**Requirements**: SCTX-01, SCTX-02, SCTX-03, SCTX-04, SCTX-05, IPC-01, IPC-02, IPC-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can connect to SSH without destroying local service context (local projects/sessions remain available)
|
||||
2. Multiple SSH connections can exist simultaneously with independent service instances
|
||||
3. Switching between contexts routes IPC requests to the correct service context
|
||||
4. Disconnecting SSH and reconnecting later restores the same SSH context (not recreated from scratch)
|
||||
5. File watcher events only fire for the active context (no cross-context pollution)
|
||||
**Plans**: 2-3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 02-01: ServiceContextRegistry with context lifecycle management
|
||||
- [ ] 02-02: IPC context API and preload bridge
|
||||
- [ ] 02-03: Connection profiles in ConfigManager
|
||||
|
||||
### Phase 3: State Management
|
||||
**Goal**: Context switching preserves exact UI state per workspace with instant restoration
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: SNAP-01, SNAP-02, SNAP-03, SNAP-04, SNAP-05
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can switch from local to SSH and back, returning to exact same state (open tabs, selected project, scroll position, sidebar selections)
|
||||
2. First-time switch to new SSH context shows empty state (not stale local data)
|
||||
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:
|
||||
- [ ] 03-01: Context snapshot system and contextSlice
|
||||
- [ ] 03-02: IndexedDB persistence with expiration handling
|
||||
|
||||
### Phase 4: Workspace UI
|
||||
**Goal**: Users can visually manage and switch between workspaces with clear status indicators
|
||||
**Depends on**: Phase 3
|
||||
**Requirements**: WSUI-01, WSUI-02, WSUI-03, WSUI-04, WSUI-05
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User sees context switcher in sidebar listing Local + all SSH workspaces
|
||||
2. Status bar shows active workspace name and connection status at all times
|
||||
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:
|
||||
- [ ] 04-01: ContextSwitcher component and status indicators
|
||||
- [ ] 04-02: Connection profiles UI in settings
|
||||
|
||||
## Progress
|
||||
|
||||
**Execution Order:**
|
||||
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/2-3 | Not started | - |
|
||||
| 3. State Management | 0/1-2 | Not started | - |
|
||||
| 4. Workspace UI | 0/1-2 | Not started | - |
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-02-12*
|
||||
*Last updated: 2026-02-12 after Phase 1 execution complete*
|
||||
82
.planning/STATE.md
Normal file
82
.planning/STATE.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .planning/PROJECT.md (updated 2026-02-12)
|
||||
|
||||
**Core value:** Users can seamlessly switch between local and SSH workspaces without losing state, and SSH sessions actually load their conversation history.
|
||||
**Current focus:** Phase 1 complete — ready for Phase 2
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 1 of 4 (Provider Plumbing) — COMPLETE
|
||||
Plan: All plans complete
|
||||
Status: Phase 1 complete
|
||||
Last activity: 2026-02-12 - Phase 1 execution complete (1/1 plans)
|
||||
|
||||
Progress: [██░░░░░░░░] 25% (1/4 phases)
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
- Total plans completed: 1
|
||||
- Average duration: 4 min
|
||||
- Total execution time: 0.07 hours
|
||||
|
||||
**By Phase:**
|
||||
|
||||
| Phase | Plans | Total | Avg/Plan |
|
||||
|-------|-------|-------|----------|
|
||||
| 01 Provider Plumbing | 1 | 4 min | 4 min |
|
||||
|
||||
**Recent Trend:**
|
||||
- Last 5 plans: 4
|
||||
- Trend: Just started
|
||||
|
||||
*Updated after each plan completion*
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
|
||||
Decisions are logged in PROJECT.md Key Decisions table.
|
||||
Recent decisions affecting current work:
|
||||
|
||||
- ServiceContextRegistry in main process (centralizes context lifecycle)
|
||||
- Snapshot/restore for Zustand state (instant switching without refetching)
|
||||
- Workspace indicators in sidebar + status bar (VS Code model)
|
||||
- SSH watchers stay alive in background (real-time updates for all workspaces)
|
||||
- Added getFileSystemProvider() getter to ProjectScanner for consistent provider access (01-01)
|
||||
- Threaded provider through all parseJsonlFile() call sites instead of relying on optional parameter fallback (01-01)
|
||||
- Refactored SubagentDetailBuilder to accept fsProvider and projectsDir as explicit parameters (01-01)
|
||||
|
||||
### Pending Todos
|
||||
|
||||
None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
**Phase 1:**
|
||||
- ✓ RESOLVED: SessionParser, SubagentResolver, and SubagentDetailBuilder now receive FileSystemProvider correctly (01-01)
|
||||
- Need to test SSH session loading and subagent drill-down thoroughly before proceeding to infrastructure changes (deferred to end-to-end testing)
|
||||
|
||||
**Phase 2:**
|
||||
- ServiceContextRegistry pattern is novel for this codebase (no existing examples) - may need proof-of-concept validation
|
||||
- EventEmitter listener cleanup must be bulletproof - memory leaks from orphaned listeners can consume 50-100MB per switch
|
||||
|
||||
**Phase 3:**
|
||||
- Snapshot expiration strategy uses 5-minute TTL heuristic - may need tuning based on actual user switching patterns
|
||||
- Must validate restored tabs against current context (projectIds may not exist in different context)
|
||||
|
||||
**Phase 4:**
|
||||
- Context switcher placement in sidebar needs to fit with existing SidebarHeader without disrupting current layout
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-02-12
|
||||
Stopped at: Phase 1 execution complete — ready for Phase 2 planning
|
||||
Resume file: None
|
||||
|
||||
---
|
||||
*Created: 2026-02-12*
|
||||
*Last updated: 2026-02-12 after Phase 1 execution complete*
|
||||
180
.planning/codebase/ARCHITECTURE.md
Normal file
180
.planning/codebase/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Architecture
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Pattern Overview
|
||||
|
||||
**Overall:** Three-process Electron architecture with domain-driven service layer and unidirectional data flow
|
||||
|
||||
**Key Characteristics:**
|
||||
- Electron main process (Node.js) handles file system access, JSONL parsing, and business logic via domain-organized services
|
||||
- Renderer process (React) manages UI state through Zustand slices with per-tab isolation
|
||||
- Preload script provides secure IPC bridge via contextBridge
|
||||
- Data flows: JSONL files → Services (parse/analyze) → IPC → Zustand store → React components
|
||||
- Service layer organized into 5 domains: analysis, discovery, error, infrastructure, parsing
|
||||
|
||||
## Layers
|
||||
|
||||
**Main Process (Node.js):**
|
||||
- Purpose: File system access, session parsing, chunk building, business logic
|
||||
- Location: `src/main/`
|
||||
- Contains: Entry point (`index.ts`), services (domain-organized), IPC handlers, types, utilities
|
||||
- Depends on: Electron APIs, Node.js file system, shared types
|
||||
- Used by: Renderer process via IPC
|
||||
|
||||
**Preload Bridge:**
|
||||
- Purpose: Secure IPC communication layer between main and renderer
|
||||
- Location: `src/preload/`
|
||||
- Contains: ElectronAPI implementation (`index.ts`), IPC channel constants
|
||||
- Depends on: Electron contextBridge, IPC channel definitions
|
||||
- Used by: Renderer process via `window.electronAPI`
|
||||
|
||||
**Renderer Process (React):**
|
||||
- Purpose: UI rendering, state management, user interaction
|
||||
- Location: `src/renderer/`
|
||||
- Contains: React components, Zustand store, hooks, utilities, contexts
|
||||
- Depends on: React, Zustand, ElectronAPI, shared types
|
||||
- Used by: End user
|
||||
|
||||
**Shared Code:**
|
||||
- Purpose: Cross-process types, pure utilities, constants
|
||||
- Location: `src/shared/`
|
||||
- Contains: Type definitions, token formatting, model parsing, logger
|
||||
- Depends on: Nothing (pure TypeScript)
|
||||
- Used by: Main, renderer, preload processes
|
||||
|
||||
**Service Domains (Main Process):**
|
||||
- Purpose: Business logic organized by responsibility
|
||||
- Location: `src/main/services/{domain}/`
|
||||
- Contains: 5 domains with specialized services
|
||||
- Depends on: Node.js APIs, shared types, utilities
|
||||
- Used by: IPC handlers, main process lifecycle
|
||||
|
||||
## Data Flow
|
||||
|
||||
**Session Loading Flow:**
|
||||
|
||||
1. User selects project/session in sidebar (renderer)
|
||||
2. Renderer calls `window.electronAPI.getSessionDetail(projectId, sessionId)`
|
||||
3. IPC handler in `src/main/ipc/sessions.ts` receives request
|
||||
4. Handler checks `DataCache` (LRU cache, 50 entries, 10min TTL)
|
||||
5. On cache miss: `SessionParser` reads JSONL file from `~/.claude/projects/{encoded-path}/{sessionId}.jsonl`
|
||||
6. `SessionParser` parses messages, extracts metadata, calculates metrics
|
||||
7. `SubagentResolver` finds subagent files in `{sessionId}/subagents/`, parses them, links to Task calls
|
||||
8. `ChunkBuilder` orchestrates chunk building: classifies messages (user/AI/system/noise), groups into chunks, attaches subagents
|
||||
9. Result cached in `DataCache` and returned via IPC
|
||||
10. Renderer receives data, updates Zustand store (`sessionDetailSlice`)
|
||||
11. React components re-render with new data
|
||||
|
||||
**Real-time Update Flow:**
|
||||
|
||||
1. `FileWatcher` detects change in session JSONL file (100ms debounce)
|
||||
2. FileWatcher emits 'file-change' event with `{ type, path, projectId, sessionId, isSubagent }`
|
||||
3. Main process forwards event to renderer via `mainWindow.webContents.send('file-change', event)`
|
||||
4. Renderer's `initializeNotificationListeners()` receives event
|
||||
5. Store action `refreshSessionInPlace()` called (debounced 150ms)
|
||||
6. Cache invalidated, session re-parsed
|
||||
7. Store updated without changing `selectedSessionId` (no flicker)
|
||||
8. Components re-render with updated data
|
||||
|
||||
**State Management:**
|
||||
- Zustand store with 14 slices (project, session, sessionDetail, subagent, conversation, tab, tabUI, pane, ui, notification, config, repository, connection, update)
|
||||
- Each slice follows pattern: `{ data, selectedId, loading, error }`
|
||||
- Per-tab UI state isolated in `tabUISlice` using tabId as key
|
||||
- IPC event listeners registered once in `App.tsx`, update store directly
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
**Chunk (Visualization Unit):**
|
||||
- Purpose: Independent timeline visualization unit for chat display
|
||||
- Examples: `UserChunk`, `AIChunk`, `SystemChunk`, `CompactChunk`
|
||||
- Pattern: Discriminated union on `type` field, each with timestamp, duration, metrics (tokens, cost, tools)
|
||||
- Built by: `ChunkBuilder` orchestrating `ChunkFactory`, `MessageClassifier`, `ProcessLinker`
|
||||
|
||||
**Process (Subagent):**
|
||||
- Purpose: Represents spawned subagent execution with timing and metrics
|
||||
- Examples: Task tool subagents, teammate messages in team coordination
|
||||
- Pattern: Contains `id`, `name`, `startTime`, `endTime`, `metrics`, `isParallel`, optional `team` metadata
|
||||
- Built by: `SubagentResolver` parsing subagent JSONL files, linking to Task calls, detecting parallel execution
|
||||
|
||||
**Service (Business Logic):**
|
||||
- Purpose: Domain-specific business logic with single responsibility
|
||||
- Examples: `ChunkBuilder` (analysis), `ProjectScanner` (discovery), `SessionParser` (parsing), `NotificationManager` (infrastructure), `ErrorDetector` (error)
|
||||
- Pattern: Class-based, injected dependencies via constructor, exported from domain barrel
|
||||
- Location: `src/main/services/{domain}/`
|
||||
|
||||
**Zustand Slice (State Domain):**
|
||||
- Purpose: Domain-specific state management with actions
|
||||
- Examples: `sessionSlice`, `tabSlice`, `notificationSlice`
|
||||
- Pattern: Factory function returning slice with data, actions, selectors
|
||||
- Combined in: `src/renderer/store/index.ts` via `create<AppState>()`
|
||||
|
||||
**IPC Handler (Communication):**
|
||||
- Purpose: Request/response handlers for renderer-to-main communication
|
||||
- Examples: `get-projects`, `get-session-detail`, `notifications:get`
|
||||
- Pattern: Domain-organized modules with initialize/register/remove functions
|
||||
- Location: `src/main/ipc/{domain}.ts`
|
||||
|
||||
## Entry Points
|
||||
|
||||
**Main Process Entry:**
|
||||
- Location: `src/main/index.ts` (381 lines)
|
||||
- Triggers: `app.whenReady()` Electron event
|
||||
- Responsibilities: Initialize services (ProjectScanner, SessionParser, SubagentResolver, ChunkBuilder, DataCache, FileWatcher, NotificationManager, UpdaterService, SshConnectionManager), register IPC handlers, create BrowserWindow, start file watcher, apply configuration
|
||||
|
||||
**Renderer Entry:**
|
||||
- Location: `src/renderer/main.tsx` (12 lines)
|
||||
- Triggers: Page load after Electron window created
|
||||
- Responsibilities: Render React app (`<App />`), mount to #root div
|
||||
|
||||
**Preload Entry:**
|
||||
- Location: `src/preload/index.ts` (369 lines)
|
||||
- Triggers: Before renderer loads (Electron lifecycle)
|
||||
- Responsibilities: Expose ElectronAPI via contextBridge, wrap IPC calls with type-safe interface, provide event listener setup/cleanup
|
||||
|
||||
**React App Component:**
|
||||
- Location: `src/renderer/App.tsx`
|
||||
- Triggers: React render
|
||||
- Responsibilities: Initialize theme, dismiss splash screen, register IPC event listeners (`initializeNotificationListeners()`), render layout (`<TabbedLayout />`)
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Strategy:** Layer-specific error boundaries with graceful degradation
|
||||
|
||||
**Patterns:**
|
||||
- Main process: Try/catch in services and IPC handlers, log errors with `createLogger()`, return null or empty arrays on failure
|
||||
- Renderer: React `<ErrorBoundary>` component catches render errors, shows fallback UI
|
||||
- IPC: Config handlers return `{ success: boolean, data?, error? }` wrapper, other handlers return null on failure
|
||||
- Store actions: Catch async errors, set `error` state, display in UI
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
**Logging:** `createLogger(namespace)` from `@shared/utils/logger`, used throughout main and renderer
|
||||
|
||||
**Validation:**
|
||||
- Path validation in `src/main/ipc/validation.ts` via `validatePath()`, `validateMentions()`
|
||||
- Config validation in `src/main/ipc/configValidation.ts` for user input sanitization
|
||||
- Type guards in `src/main/ipc/guards.ts` for IPC argument validation
|
||||
|
||||
**Authentication:** Not applicable (local desktop app accessing user's file system)
|
||||
|
||||
**Caching:**
|
||||
- `DataCache` service (LRU, 50 entries, 10min TTL) for parsed session data
|
||||
- Cache invalidation on file changes detected by `FileWatcher`
|
||||
- Automatic cleanup every 5 minutes
|
||||
|
||||
**Performance:**
|
||||
- Virtual scrolling for session lists and message lists (`@tanstack/react-virtual`)
|
||||
- Debounced file watching (100ms) to batch rapid changes
|
||||
- Session refresh debouncing (150ms) to prevent redundant IPC calls
|
||||
- LRU cache to avoid re-parsing large JSONL files
|
||||
|
||||
**Configuration:**
|
||||
- `ConfigManager` service manages `~/.claude-devtools/config.json`
|
||||
- Accessed via `configManager.getConfig()`, `configManager.updateConfig()`
|
||||
- IPC handlers in `src/main/ipc/config.ts` for renderer access
|
||||
- Settings UI in `src/renderer/components/settings/`
|
||||
|
||||
---
|
||||
|
||||
*Architecture analysis: 2026-02-12*
|
||||
309
.planning/codebase/CONCERNS.md
Normal file
309
.planning/codebase/CONCERNS.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# Codebase Concerns
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Tech Debt
|
||||
|
||||
**Large, complex files needing refactoring:**
|
||||
- Issue: Several files exceed 700-1100 lines with complex business logic
|
||||
- Files:
|
||||
- `src/renderer/utils/contextTracker.ts` (1099 lines)
|
||||
- `src/main/services/discovery/ProjectScanner.ts` (827 lines)
|
||||
- `src/main/services/infrastructure/FileWatcher.ts` (798 lines)
|
||||
- `src/renderer/components/chat/ChatHistory.tsx` (745 lines)
|
||||
- `src/renderer/store/slices/tabSlice.ts` (740 lines)
|
||||
- `src/main/services/infrastructure/ConfigManager.ts` (701 lines)
|
||||
- `src/renderer/utils/groupTransformer.ts` (700 lines)
|
||||
- `src/main/services/parsing/GitIdentityResolver.ts` (674 lines)
|
||||
- `src/renderer/store/slices/sessionDetailSlice.ts` (658 lines)
|
||||
- `src/main/services/infrastructure/NotificationManager.ts` (657 lines)
|
||||
- `src/renderer/utils/claudeMdTracker.ts` (656 lines)
|
||||
- `src/main/ipc/config.ts` (628 lines)
|
||||
- Impact: Difficult to maintain, test, and debug; high cognitive load for changes
|
||||
- Fix approach: Extract into smaller modules with single responsibilities; split contextTracker into separate concerns (injection detection, stats computation, phase tracking); break ConfigManager into domain-specific config handlers; split ChatHistory view logic from virtualization/scroll management
|
||||
|
||||
**Type safety gaps with `as any` and `as unknown` casts:**
|
||||
- Issue: Type assertions bypass TypeScript's type checking
|
||||
- Files:
|
||||
- `src/main/services/infrastructure/SshConnectionManager.ts:171` - `sftp as any` for SSH provider
|
||||
- `src/main/services/infrastructure/SshFileSystemProvider.ts:44` - `data as unknown as string`
|
||||
- `src/main/services/infrastructure/NotificationManager.ts:170` - `JSON.parse(data) as unknown`
|
||||
- `src/main/services/discovery/ProjectScanner.ts:659` - `JSON.parse(content) as unknown`
|
||||
- `src/main/services/analysis/ToolResultExtractor.ts:140` - `content as unknown[]`
|
||||
- `src/main/services/analysis/SemanticStepExtractor.ts:169` - `msg.toolUseResult as unknown`
|
||||
- `src/main/utils/jsonl.ts:450` - `entry as unknown as Record<string, unknown>`
|
||||
- Test files: Extensive use in `test/main/services/infrastructure/FileWatcher.test.ts` for mocking
|
||||
- Impact: Potential runtime type errors; bypasses compiler safety net
|
||||
- Fix approach: Create proper type guards and validation functions; use Zod schemas for JSON parsing; properly type SSH2/SFTP library interfaces instead of `any`
|
||||
|
||||
**Empty catch blocks silently swallowing errors:**
|
||||
- Issue: 213 try/catch occurrences across 62 files, many with minimal error handling
|
||||
- Files: All IPC handlers, store slices, service files
|
||||
- Impact: Silent failures make debugging difficult; errors may go unnoticed
|
||||
- Fix approach: Add structured logging to all catch blocks; implement error telemetry; ensure all errors bubble up or are explicitly handled with user feedback
|
||||
|
||||
**SSH connection type safety issues:**
|
||||
- Issue: SSH2 library types not properly integrated, forcing `any` casts
|
||||
- Files: `src/main/services/infrastructure/SshConnectionManager.ts`, `src/main/services/infrastructure/SshFileSystemProvider.ts`
|
||||
- Impact: Runtime errors in SSH operations won't be caught at compile time
|
||||
- Fix approach: Create proper TypeScript interfaces for SSH2/SFTP types; use branded types or runtime validation
|
||||
|
||||
**Disabled ESLint rules indicate code smells:**
|
||||
- Issue: 30+ eslint-disable comments suggest underlying design issues
|
||||
- Files:
|
||||
- `src/renderer/components/chat/ChatHistory.tsx:417,425` - Direct DOM mutation for search highlighting
|
||||
- `src/renderer/components/chat/AIChatGroup.tsx:195` - Manual memoization instead of React Compiler
|
||||
- `src/renderer/hooks/useAutoScrollBottom.ts:144,178,204,247` - Complex effect dependencies
|
||||
- `src/renderer/components/sidebar/DateGroupedSessions.tsx:157` - TanStack Virtual API limitation
|
||||
- `src/renderer/utils/groupTransformer.ts:57` - Regex flagged as potentially unsafe
|
||||
- Impact: Code may be fragile or have performance issues
|
||||
- Fix approach: Refactor direct DOM mutations into proper React state management; simplify effect dependencies; document why ESLint rules must be disabled
|
||||
|
||||
## Known Bugs
|
||||
|
||||
**None explicitly documented in code:**
|
||||
- Symptoms: No TODO/FIXME/HACK/BUG comments found in source
|
||||
- Trigger: N/A
|
||||
- Workaround: N/A
|
||||
|
||||
## Security Considerations
|
||||
|
||||
**IPC input validation present but could be stricter:**
|
||||
- Risk: Malformed IPC inputs could cause crashes or unexpected behavior
|
||||
- Files: `src/main/ipc/guards.ts`, all IPC handlers in `src/main/ipc/`
|
||||
- Current mitigation: Validation guards with max length checks (128-512 chars), pattern validation, coercion for numeric inputs
|
||||
- Recommendations:
|
||||
- Add rate limiting on IPC calls to prevent DOS
|
||||
- Validate file paths more strictly to prevent directory traversal
|
||||
- Add schema validation for complex config objects (use Zod)
|
||||
|
||||
**Content sanitization exists but not comprehensive:**
|
||||
- Risk: User-provided content from JSONL files could contain malicious content
|
||||
- Files: `src/shared/utils/contentSanitizer.ts` - sanitizes display content, removes control characters
|
||||
- Current mitigation: Basic content sanitization with regex pattern removal
|
||||
- Recommendations:
|
||||
- Add HTML escaping for any user content rendered in DOM
|
||||
- Validate markdown content for XSS vectors
|
||||
- Implement Content Security Policy headers
|
||||
|
||||
**File system access is broad:**
|
||||
- Risk: App has access to entire user home directory via `~/.claude/`
|
||||
- Files: `src/main/services/infrastructure/LocalFileSystemProvider.ts`, `src/main/services/infrastructure/SshFileSystemProvider.ts`
|
||||
- Current mitigation: Access limited to specific Claude directories; path validation in IPC handlers
|
||||
- Recommendations:
|
||||
- Add explicit allow-list of accessible directories
|
||||
- Log all file system operations for audit trail
|
||||
- Implement file size limits to prevent memory exhaustion
|
||||
|
||||
**SSH connection security:**
|
||||
- Risk: SSH credentials and private keys handled in memory
|
||||
- Files: `src/main/services/infrastructure/SshConnectionManager.ts`, `src/main/services/infrastructure/SshConfigParser.ts`
|
||||
- Current mitigation: Supports SSH agent, private key files; no plain password storage
|
||||
- Recommendations:
|
||||
- Ensure private keys are not logged
|
||||
- Add connection timeout and retry limits
|
||||
- Validate SSH host keys to prevent MITM attacks
|
||||
- Document that passwords should use SSH agent, not inline
|
||||
|
||||
**Regex injection prevention:**
|
||||
- Risk: User-provided regex patterns in notification triggers could cause ReDoS
|
||||
- Files: `src/main/utils/regexValidation.ts`, `src/main/services/error/TriggerMatcher.ts`
|
||||
- Current mitigation: Regex validation with complexity limits, documented as preventing ReDoS
|
||||
- Recommendations: Continue validating all user regex; consider timeout mechanism for regex execution
|
||||
|
||||
## Performance Bottlenecks
|
||||
|
||||
**Large JSONL file parsing on every load:**
|
||||
- Problem: Session files can be large (10k+ lines), parsed synchronously line-by-line
|
||||
- Files: `src/main/utils/jsonl.ts:50-80` - `parseJsonlFile()`, `src/main/services/parsing/SessionParser.ts`
|
||||
- Cause: Streaming reads mitigate memory issues but CPU-bound parsing still blocks
|
||||
- Improvement path:
|
||||
- Implement incremental parsing (read only viewport range initially)
|
||||
- Add progress indicators for large files
|
||||
- Cache parsed results more aggressively (current: 50 entries, 10min TTL)
|
||||
- Consider worker threads for parsing if files exceed threshold
|
||||
|
||||
**DataCache could be more aggressive:**
|
||||
- Problem: Cache size limited to 50 entries with 10min TTL; may evict frequently accessed sessions
|
||||
- Files: `src/main/services/infrastructure/DataCache.ts:34`
|
||||
- Cause: Conservative cache settings to limit memory usage
|
||||
- Improvement path:
|
||||
- Make cache size configurable based on available memory
|
||||
- Implement smarter eviction (frequency-based, not just LRU)
|
||||
- Add cache warming for recently accessed projects
|
||||
- Monitor cache hit rate and adjust limits
|
||||
|
||||
**Virtual scrolling threshold may be too high:**
|
||||
- Problem: ChatHistory uses virtualization only after 120 items
|
||||
- Files: `src/renderer/components/chat/ChatHistory.tsx:33` - `VIRTUALIZATION_THRESHOLD = 120`
|
||||
- Cause: Balance between performance and simplicity
|
||||
- Improvement path: Lower threshold to 50 or make dynamic based on item complexity
|
||||
|
||||
**File watcher polling overhead in SSH mode:**
|
||||
- Problem: SSH mode polls every 5 seconds instead of using native file watching
|
||||
- Files: `src/main/services/infrastructure/FileWatcher.ts:76` - `SSH_POLL_INTERVAL_MS = 5000`
|
||||
- Cause: SFTP doesn't support native file watching
|
||||
- Improvement path:
|
||||
- Increase polling interval for inactive sessions
|
||||
- Only poll actively viewed sessions
|
||||
- Implement exponential backoff when no changes detected
|
||||
|
||||
**React re-renders from array state updates:**
|
||||
- Problem: Components use `useState` with arrays, triggering re-renders on mutation
|
||||
- Files: Found in `src/renderer/components/search/CommandPalette.tsx`, `src/renderer/components/settings/NotificationTriggerSettings/hooks/useAddTriggerFormState.ts`
|
||||
- Cause: React's immutability model
|
||||
- Improvement path: Use immer for state updates; move to Zustand for complex state; add React.memo() strategically
|
||||
|
||||
**Context tracking computation on every turn:**
|
||||
- Problem: `contextTracker.ts` (1099 lines) recomputes context stats for entire session
|
||||
- Files: `src/renderer/utils/contextTracker.ts`
|
||||
- Cause: Comprehensive tracking across 6 categories for all messages
|
||||
- Improvement path:
|
||||
- Memoize computation results
|
||||
- Compute incrementally as messages arrive
|
||||
- Move computation to web worker for large sessions
|
||||
|
||||
## Fragile Areas
|
||||
|
||||
**Search highlighting with direct DOM manipulation:**
|
||||
- Files: `src/renderer/components/chat/ChatHistory.tsx:417-425`
|
||||
- Why fragile: Directly mutates DOM outside React; tightly coupled to DOM structure
|
||||
- Safe modification: Changes to chat item rendering may break highlighting; modify `searchHighlightUtils.ts` in parallel
|
||||
- Test coverage: No automated tests for search highlighting
|
||||
|
||||
**File watcher concurrency handling:**
|
||||
- Files: `src/main/services/infrastructure/FileWatcher.ts:79-82` - concurrency guards with `processingInProgress`, `pendingReprocess` Sets
|
||||
- Why fragile: Complex state machine with polling, debouncing, catch-up scans, and concurrency guards; edge cases in reconnection
|
||||
- Safe modification: Test thoroughly with concurrent file changes; validate that reprocessing queue works correctly
|
||||
- Test coverage: Partial coverage in `test/main/services/infrastructure/FileWatcher.test.ts`
|
||||
|
||||
**Tab state synchronization across panes:**
|
||||
- Files: `src/renderer/store/slices/tabSlice.ts:88-96` - `syncFromLayout()`, pane helpers
|
||||
- Why fragile: Complex facade pattern synchronizing root state with focused pane; multi-level updates
|
||||
- Safe modification: Changes to pane layout must maintain backward compatibility with openTabs/activeTabId
|
||||
- Test coverage: Basic tests in `test/renderer/store/tabSlice.test.ts`
|
||||
|
||||
**Subagent resolution with parallel execution detection:**
|
||||
- Files: `src/main/services/discovery/SubagentResolver.ts` (547 lines)
|
||||
- Why fragile: Detects parallel execution by analyzing timing; enriches team metadata with color assignments
|
||||
- Safe modification: Changes to team detection logic could break teammate display; timing heuristics may need tuning
|
||||
- Test coverage: No automated tests for SubagentResolver
|
||||
|
||||
**Auto-scroll with search navigation:**
|
||||
- Files: `src/renderer/hooks/useAutoScrollBottom.ts:144,178,204,247` - complex effect dependencies
|
||||
- Why fragile: Multiple setTimeout/RAF coordination; interacts with virtualization, search, and user scroll
|
||||
- Safe modification: Changes to scroll behavior should be tested with all interaction modes (search, navigation, user scroll)
|
||||
- Test coverage: Basic tests in `test/renderer/hooks/useAutoScrollBottom.test.ts`
|
||||
|
||||
## Scaling Limits
|
||||
|
||||
**In-memory session storage:**
|
||||
- Current capacity: Limited by DataCache (50 sessions) and Zustand store (unbounded arrays)
|
||||
- Limit: Projects with 1000+ sessions may cause memory issues
|
||||
- Scaling path:
|
||||
- Implement pagination for session lists (already exists via `getSessionsPaginated`)
|
||||
- Add session unloading when tabs close
|
||||
- Consider IndexedDB for session metadata caching in renderer
|
||||
|
||||
**Context injection tracking:**
|
||||
- Current capacity: All injections tracked for entire session in memory
|
||||
- Limit: Very long sessions (1000+ turns) will accumulate large context stats
|
||||
- Scaling path:
|
||||
- Compute stats on-demand instead of upfront
|
||||
- Store only aggregates, not individual injections
|
||||
- Implement rolling window (last N turns)
|
||||
|
||||
**Notification storage:**
|
||||
- Current capacity: Max 100 notifications in `~/.claude/claude-devtools-notifications.json`
|
||||
- Limit: Hard cap prevents unbounded growth
|
||||
- Scaling path: Already well-bounded; consider adding pagination UI if 100 is insufficient
|
||||
|
||||
**Virtual scrolling limitations:**
|
||||
- Current capacity: Handles 1000+ items but estimated height may cause jumps
|
||||
- Limit: Dynamic height items (collapsed/expanded) can cause scroll jank
|
||||
- Scaling path: Use measured heights instead of estimates; implement "scroll anchoring"
|
||||
|
||||
## Dependencies at Risk
|
||||
|
||||
**Electron version updates:**
|
||||
- Risk: Currently on Electron 40.3.0; major version updates may break IPC contracts
|
||||
- Impact: File watching, native notifications, window management
|
||||
- Migration plan: Test thoroughly on Electron beta releases; monitor breaking changes in Electron docs
|
||||
|
||||
**SSH2 library type coverage:**
|
||||
- Risk: `ssh2@1.17.0` has incomplete TypeScript types, requiring `as any` casts
|
||||
- Impact: SSH functionality breaks at runtime, not compile time
|
||||
- Migration plan: Contribute types to DefinitelyTyped or switch to better-typed SSH library
|
||||
|
||||
**React 18 concurrent rendering:**
|
||||
- Risk: Some components may not be concurrent-safe (direct DOM mutations)
|
||||
- Impact: Search highlighting, scroll behavior may have race conditions
|
||||
- Migration plan: Audit all DOM mutations; move to proper React state; enable strict mode
|
||||
|
||||
**Zustand store performance:**
|
||||
- Risk: Large state trees with deep subscriptions cause re-render cascades
|
||||
- Impact: Noticeable lag on large sessions
|
||||
- Migration plan: Use `useShallow` more broadly; implement selector memoization; consider Jotai for derived state
|
||||
|
||||
## Missing Critical Features
|
||||
|
||||
**Offline SSH support:**
|
||||
- Problem: SSH connections disconnect without graceful degradation
|
||||
- Blocks: Viewing remote sessions when host unreachable
|
||||
- Priority: Medium - add cached mode for read-only access
|
||||
|
||||
**Session export/archive:**
|
||||
- Problem: No way to export session data for backup or sharing
|
||||
- Blocks: Long-term archival, data portability
|
||||
- Priority: Low - users can manually copy JSONL files
|
||||
|
||||
**Undo/redo for configuration:**
|
||||
- Problem: No way to revert config changes or notification trigger edits
|
||||
- Blocks: Safe experimentation with triggers
|
||||
- Priority: Low - manual backup of `~/.claude/config.json` works
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
**Service layer severely undertested:**
|
||||
- What's not tested: 38 of 44 service files lack tests (86% untested)
|
||||
- Files:
|
||||
- All infrastructure services except FileWatcher: `ConfigManager.ts`, `NotificationManager.ts`, `SshConnectionManager.ts`, `DataCache.ts` (DataCache has no tests despite complex LRU logic)
|
||||
- All discovery services except ProjectScanner, SessionSearcher: `SubagentResolver.ts`, `SubagentLocator.ts`, `WorktreeGrouper.ts`, `SessionContentFilter.ts`
|
||||
- All analysis services: `ChunkBuilder.ts` (tested), but `SubagentDetailBuilder.ts`, `SemanticStepExtractor.ts`, `ToolExecutionBuilder.ts` untested
|
||||
- All error services: `ErrorDetector.ts`, `ErrorTriggerChecker.ts`, `ErrorMessageBuilder.ts`
|
||||
- All parsing services except MessageClassifier, SessionParser: `GitIdentityResolver.ts`, `ClaudeMdReader.ts`
|
||||
- Risk: Complex business logic could break unnoticed; refactoring is risky
|
||||
- Priority: High - focus on: DataCache LRU eviction, NotificationManager throttling, SubagentResolver parallel detection, ErrorDetector token counting
|
||||
|
||||
**React components completely untested:**
|
||||
- What's not tested: 126 component files, 0 component tests
|
||||
- Files: All of `src/renderer/components/` (chat, sidebar, settings, dashboard, layout)
|
||||
- Risk: UI regressions go unnoticed; interaction bugs not caught
|
||||
- Priority: Medium - focus on critical paths: ChatHistory rendering, search functionality, tab management
|
||||
|
||||
**IPC handlers have minimal coverage:**
|
||||
- What's not tested: Most IPC handlers in `src/main/ipc/` lack integration tests
|
||||
- Files: `config.ts` (628 lines, 0% coverage), `sessions.ts`, `notifications.ts`, `ssh.ts`
|
||||
- Risk: Handler crashes or incorrect responses not caught
|
||||
- Priority: Medium - guards.ts has 53% coverage; add handler-level tests
|
||||
|
||||
**Store slices partially tested:**
|
||||
- What's not tested: 6 of 12 slices tested; missing: `projectSlice`, `repositorySlice`, `sessionDetailSlice`, `subagentSlice`, `conversationSlice`, `configSlice`
|
||||
- Files: `test/renderer/store/` has tests for `notificationSlice`, `paneSlice`, `sessionSlice`, `tabSlice`, `tabUISlice`
|
||||
- Risk: State mutations may have side effects; action creators could have bugs
|
||||
- Priority: Medium - focus on complex slices: sessionDetailSlice, conversationSlice
|
||||
|
||||
**Context tracking logic untested:**
|
||||
- What's not tested: `contextTracker.ts` (1099 lines) has no tests
|
||||
- Files: `src/renderer/utils/contextTracker.ts`, `src/renderer/utils/claudeMdTracker.ts`
|
||||
- Risk: Context stats computation could be wrong; token counting inaccurate
|
||||
- Priority: High - this is a core feature; add comprehensive tests for all 6 injection categories
|
||||
|
||||
**Utility functions have good coverage:**
|
||||
- What's tested: `jsonl.ts`, `pathDecoder.ts`, `pathValidation.ts`, `regexValidation.ts`, `tokenizer.ts`, formatters, date grouping
|
||||
- Coverage: Core parsing and utility logic is well-tested
|
||||
- Priority: Low - maintain current coverage
|
||||
|
||||
---
|
||||
|
||||
*Concerns audit: 2026-02-12*
|
||||
389
.planning/codebase/CONVENTIONS.md
Normal file
389
.planning/codebase/CONVENTIONS.md
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
# Coding Conventions
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Naming Patterns
|
||||
|
||||
**Files:**
|
||||
- Services/Components: PascalCase - `ChunkBuilder.ts`, `ProjectScanner.ts`, `AIChatGroup.tsx`
|
||||
- Utilities: camelCase - `pathDecoder.ts`, `tokenizer.ts`, `formatters.ts`
|
||||
- Type definitions: camelCase - `messages.ts`, `chunks.ts`, `data.ts`
|
||||
- Test files: `*.test.ts` - mirrors source file name
|
||||
|
||||
**Functions:**
|
||||
- Standard functions: camelCase - `encodePath`, `extractProjectName`, `formatDuration`
|
||||
- Type guards: `isXxx` - `isParsedRealUserMessage`, `isAIChunk`, `isValidEncodedPath`
|
||||
- Builder functions: `buildXxx` - `buildChunks`, `buildUserChunk`, `buildSessionPath`
|
||||
- Getter functions: `getXxx` - `getProjectsBasePath`, `getExpandedDisplayItemIds`
|
||||
- React components: Arrow functions with PascalCase names
|
||||
|
||||
**Variables:**
|
||||
- Standard: camelCase - `chunkBuilder`, `sessionId`, `projectPath`
|
||||
- Constants: UPPER_SNAKE_CASE - `EMPTY_METRICS`, `HARD_NOISE_TAGS`, `CONFIG_GET`
|
||||
- React components: PascalCase - `AIChatGroup`, `TokenUsageDisplay`
|
||||
- Unused parameters: Leading underscore - `_event`, `_arg`
|
||||
|
||||
**Types:**
|
||||
- Interfaces/Types: PascalCase - `ParsedMessage`, `EnhancedChunk`, `SessionMetrics`
|
||||
- Interfaces: NO "I" prefix (modern convention) - `ToolCall`, not `IToolCall`
|
||||
- Enum members: PascalCase or UPPER_CASE - `TriggerColor`, `EMPTY_STDOUT`
|
||||
|
||||
## Code Style
|
||||
|
||||
**Formatting:**
|
||||
- Tool: Prettier 3.8.1
|
||||
- Config: `/Users/bskim/claude-devtools/.prettierrc.json`
|
||||
- Key settings:
|
||||
- Semi: true (always use semicolons)
|
||||
- Single quotes: true
|
||||
- Tab width: 2 spaces
|
||||
- Trailing comma: es5
|
||||
- Print width: 100 characters
|
||||
- Arrow parens: always
|
||||
- End of line: lf
|
||||
- Bracket spacing: true
|
||||
- Bracket same line: false
|
||||
- Tailwind plugin: `prettier-plugin-tailwindcss` (auto-sorts classes)
|
||||
|
||||
**Linting:**
|
||||
- Tool: ESLint 9.39.2 with typescript-eslint
|
||||
- Config: `/Users/bskim/claude-devtools/eslint.config.js`
|
||||
- Key rules:
|
||||
- TypeScript strict mode enabled
|
||||
- Type-aware linting via `projectService`
|
||||
- React + Hooks + A11y rules
|
||||
- Security plugin for AI-generated code
|
||||
- SonarJS for code quality
|
||||
- Module boundaries enforced (main/renderer/preload/shared separation)
|
||||
- Import sorting via `simple-import-sort`
|
||||
- No default exports (prefer named exports)
|
||||
- Explicit function return types (warn)
|
||||
- Explicit module boundary types (warn)
|
||||
|
||||
## Import Organization
|
||||
|
||||
**Order (enforced by `simple-import-sort`):**
|
||||
1. Side effect imports (CSS, styles)
|
||||
2. Node.js builtins (`node:fs`, `node:path`)
|
||||
3. React packages (`react`, `react-dom`)
|
||||
4. External packages (`@?\\w`)
|
||||
5. Internal aliases (`@/`)
|
||||
6. Parent imports (`../`)
|
||||
7. Same-folder imports (`./`)
|
||||
8. Type imports (last)
|
||||
|
||||
**Path Aliases:**
|
||||
- `@main/*` → `src/main/*`
|
||||
- `@renderer/*` → `src/renderer/*`
|
||||
- `@shared/*` → `src/shared/*`
|
||||
- `@preload/*` → `src/preload/*`
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ChunkBuilder, ProjectScanner } from '@main/services';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { formatDuration } from '@shared/utils/formatters';
|
||||
|
||||
import { DisplayItemList } from './DisplayItemList';
|
||||
|
||||
import type { EnhancedChunk } from '@main/types';
|
||||
```
|
||||
|
||||
**Import Restrictions:**
|
||||
- No deep relative imports (`../../../`) - use path aliases
|
||||
- No circular dependencies (max depth: 3)
|
||||
- Module boundaries enforced:
|
||||
- Renderer → renderer + shared only
|
||||
- Main → main + shared only
|
||||
- Preload → preload + shared only
|
||||
- Shared → shared + main (for type re-exports)
|
||||
|
||||
## Type Conventions
|
||||
|
||||
**Type Imports:**
|
||||
Use `type` modifier for type-only imports:
|
||||
```typescript
|
||||
import { type EnhancedChunk, type ParsedMessage } from '@main/types';
|
||||
```
|
||||
|
||||
**Type Exports:**
|
||||
```typescript
|
||||
export type { Session, SessionDetail } from './types';
|
||||
```
|
||||
|
||||
**Function Return Types:**
|
||||
Always specify for exported functions (warn-level enforcement):
|
||||
```typescript
|
||||
export function buildChunks(messages: ParsedMessage[]): EnhancedChunk[] {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Type Guards:**
|
||||
Use discriminated union pattern with type predicates:
|
||||
```typescript
|
||||
export function isParsedRealUserMessage(msg: ParsedMessage): boolean {
|
||||
return msg.type === 'user' && !msg.isMeta && typeof msg.content === 'string';
|
||||
}
|
||||
|
||||
export function isUserChunk(chunk: Chunk | EnhancedChunk): chunk is UserChunk {
|
||||
return chunk.type === 'user';
|
||||
}
|
||||
```
|
||||
|
||||
**Barrel Exports:**
|
||||
Services use barrel exports via `index.ts`:
|
||||
```typescript
|
||||
// src/main/services/index.ts
|
||||
export * from './analysis';
|
||||
export * from './discovery';
|
||||
export * from './error';
|
||||
export * from './infrastructure';
|
||||
export * from './parsing';
|
||||
|
||||
// Usage:
|
||||
import { ChunkBuilder, ProjectScanner } from '@main/services';
|
||||
```
|
||||
|
||||
Renderer utils/hooks/types do NOT have barrel exports - import directly from files.
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Main Process:**
|
||||
```typescript
|
||||
try {
|
||||
const result = await somethingRisky();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Operation failed:', error);
|
||||
return safeDefault;
|
||||
}
|
||||
```
|
||||
|
||||
**Renderer:**
|
||||
```typescript
|
||||
// Store error state
|
||||
set({
|
||||
sessionsError: error instanceof Error ? error.message : 'Operation failed',
|
||||
sessionsLoading: false,
|
||||
});
|
||||
```
|
||||
|
||||
**IPC Handlers:**
|
||||
```typescript
|
||||
// Validate parameters
|
||||
if (!isValidProjectId(projectId)) {
|
||||
throw new Error(`Invalid project ID: ${projectId}`);
|
||||
}
|
||||
|
||||
// Return safe defaults on error
|
||||
catch (error) {
|
||||
logger.error('Handler failed:', error);
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
**Framework:** Custom logger via `@shared/utils/logger`
|
||||
|
||||
**Pattern:**
|
||||
```typescript
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
const logger = createLogger('Service:ChunkBuilder');
|
||||
|
||||
logger.info('Building chunks', { messageCount: messages.length });
|
||||
logger.warn('Missing subagent', { taskId });
|
||||
logger.error('Failed to parse', error);
|
||||
```
|
||||
|
||||
**Console Usage:**
|
||||
- Main process: `console.log/error` allowed (logging is expected)
|
||||
- Renderer: Avoid console - use logger or store error state
|
||||
- Tests: Console mocked in setup, errors/warnings cause test failures
|
||||
|
||||
## Comments
|
||||
|
||||
**When to Comment:**
|
||||
- Complex business logic (chunk building, semantic step extraction)
|
||||
- Non-obvious type guards (message classification rules)
|
||||
- Workarounds or technical debt
|
||||
- Public API documentation
|
||||
|
||||
**JSDoc/TSDoc:**
|
||||
Used extensively for exported functions and types:
|
||||
```typescript
|
||||
/**
|
||||
* Encodes an absolute path into Claude Code's directory naming format.
|
||||
* Replaces all path separators (/ and \) with dashes.
|
||||
*
|
||||
* @param absolutePath - The absolute path to encode (e.g., "/Users/username/projectname")
|
||||
* @returns The encoded directory name (e.g., "-Users-username-projectname")
|
||||
*/
|
||||
export function encodePath(absolutePath: string): string {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Section Headers:**
|
||||
Used to organize large files:
|
||||
```typescript
|
||||
// =============================================================================
|
||||
// Chunk Building
|
||||
// =============================================================================
|
||||
```
|
||||
|
||||
## Function Design
|
||||
|
||||
**Size:** Keep focused and composable. Services orchestrate specialized modules.
|
||||
|
||||
**Parameters:**
|
||||
- Use explicit types, no `any` (except in tests)
|
||||
- Optional parameters last
|
||||
- Use destructuring for options objects
|
||||
|
||||
**Return Values:**
|
||||
- Always specify return type for exported functions
|
||||
- Prefer explicit returns over implicit
|
||||
- Return safe defaults instead of throwing (where appropriate)
|
||||
|
||||
**React Components:**
|
||||
```typescript
|
||||
// Prefer arrow functions for components
|
||||
export const AIChatGroup: React.FC<AIChatGroupProps> = ({ aiGroup, userGroup }) => {
|
||||
// Component implementation
|
||||
};
|
||||
```
|
||||
|
||||
**Component Props:**
|
||||
```typescript
|
||||
interface AIChatGroupProps {
|
||||
aiGroup: AIGroup;
|
||||
userGroup?: UserGroup;
|
||||
onNavigate?: (messageId: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## Module Design
|
||||
|
||||
**Exports:**
|
||||
- Prefer named exports over default exports
|
||||
- Barrel exports for service domains
|
||||
- Direct exports for renderer utils/hooks
|
||||
|
||||
**File Organization:**
|
||||
```typescript
|
||||
// Imports
|
||||
import { ... } from '...';
|
||||
|
||||
// Types/Interfaces
|
||||
export interface MyInterface { ... }
|
||||
|
||||
// Constants
|
||||
export const MY_CONSTANT = ...;
|
||||
|
||||
// Functions
|
||||
export function myFunction() { ... }
|
||||
|
||||
// Classes
|
||||
export class MyClass { ... }
|
||||
```
|
||||
|
||||
**Services Pattern:**
|
||||
```typescript
|
||||
export class ChunkBuilder {
|
||||
buildChunks(messages: ParsedMessage[]): EnhancedChunk[] {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
buildSessionDetail(session: Session): SessionDetail {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## React Conventions
|
||||
|
||||
**Component Structure:**
|
||||
```typescript
|
||||
// Imports
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
// Types
|
||||
interface ComponentProps { ... }
|
||||
|
||||
// Component
|
||||
export const Component: React.FC<ComponentProps> = ({ prop1, prop2 }) => {
|
||||
// Hooks first
|
||||
const store = useStore();
|
||||
const [state, setState] = useState();
|
||||
|
||||
// Memoized values
|
||||
const computed = useMemo(() => ..., [deps]);
|
||||
|
||||
// Callbacks
|
||||
const handleClick = useCallback(() => ..., [deps]);
|
||||
|
||||
// Render
|
||||
return <div>...</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**Hooks Rules:**
|
||||
- Always use exhaustive deps (error-level enforcement)
|
||||
- Custom hooks start with `use`: `useTabUI`, `useAutoScrollBottom`
|
||||
- Keep hooks focused and composable
|
||||
|
||||
**State Management (Zustand):**
|
||||
```typescript
|
||||
// Slice pattern
|
||||
export interface SessionSlice {
|
||||
// State
|
||||
sessions: Session[];
|
||||
selectedSessionId: string | null;
|
||||
sessionsLoading: boolean;
|
||||
sessionsError: string | null;
|
||||
|
||||
// Actions
|
||||
fetchSessions: (projectId: string) => Promise<void>;
|
||||
selectSession: (id: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
**Use theme-aware classes:**
|
||||
```tsx
|
||||
<div className="bg-surface text-text border-border">
|
||||
<div className="bg-surface-raised text-text-secondary">
|
||||
```
|
||||
|
||||
**Auto-sorted by Prettier plugin:**
|
||||
Classes automatically ordered by Prettier's Tailwind plugin.
|
||||
|
||||
**Custom CSS variables:**
|
||||
Defined in `src/renderer/index.css` - use via Tailwind classes:
|
||||
- `--color-surface` → `bg-surface`
|
||||
- `--color-text` → `text-text`
|
||||
- `--color-border` → `border-border`
|
||||
|
||||
## Security
|
||||
|
||||
**Enabled Rules:**
|
||||
- `security/detect-eval-with-expression`: error
|
||||
- `security/detect-child-process`: warn
|
||||
- File system access: allowed (desktop app requirement)
|
||||
- Dynamic patterns: allowed (intentional in this app)
|
||||
|
||||
**Parameter Mutation:**
|
||||
Prevented via `no-param-reassign` except for:
|
||||
- `draft` (Immer patterns)
|
||||
- `acc` (reduce accumulators)
|
||||
- `state` (Zustand)
|
||||
|
||||
---
|
||||
|
||||
*Convention analysis: 2026-02-12*
|
||||
153
.planning/codebase/INTEGRATIONS.md
Normal file
153
.planning/codebase/INTEGRATIONS.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# External Integrations
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## APIs & External Services
|
||||
|
||||
**SSH/SFTP:**
|
||||
- ssh2 - Direct SSH client implementation for remote session access
|
||||
- SDK/Client: `ssh2` package (Node.js native)
|
||||
- Auth: SSH agent, password, or private key authentication
|
||||
- Implementation: `src/main/services/infrastructure/SshConnectionManager.ts`
|
||||
- Config parsing: `src/main/services/infrastructure/SshConfigParser.ts` (reads `~/.ssh/config`)
|
||||
- File system: `src/main/services/infrastructure/SshFileSystemProvider.ts` (SFTP-based)
|
||||
- Purpose: Read Claude Code session files from remote machines
|
||||
|
||||
**GitHub Releases:**
|
||||
- electron-updater - Auto-update via GitHub release assets
|
||||
- SDK/Client: `electron-updater` package
|
||||
- Configuration: `electron-builder.yml` publish section
|
||||
- Implementation: `src/main/services/infrastructure/UpdaterService.ts`
|
||||
- Update flow: Check → Download → Install (user-confirmed)
|
||||
- Purpose: Application auto-updates
|
||||
|
||||
## Data Storage
|
||||
|
||||
**Databases:**
|
||||
- None - File-based storage only
|
||||
|
||||
**File Storage:**
|
||||
- Local filesystem only
|
||||
- Session data: `~/.claude/projects/{encoded-path}/*.jsonl` (read-only)
|
||||
- App config: `~/.claude/claude-devtools-config.json` (read-write)
|
||||
- Notifications: `~/.claude/claude-devtools-notifications.json` (read-write)
|
||||
- Remote access: SFTP via ssh2 (optional)
|
||||
|
||||
**Caching:**
|
||||
- In-memory LRU cache - `src/main/services/infrastructure/DataCache.ts`
|
||||
- Max entries: 50 sessions
|
||||
- TTL: 10 minutes
|
||||
- Auto-cleanup: Every 5 minutes
|
||||
- Disable flag: `CLAUDE_CONTEXT_DISABLE_CACHE=1`
|
||||
|
||||
## Authentication & Identity
|
||||
|
||||
**Auth Provider:**
|
||||
- SSH authentication (remote access only)
|
||||
- Methods: SSH agent, password, private key
|
||||
- Agent discovery: `SSH_AUTH_SOCK` env var, launchctl (macOS), known socket paths
|
||||
- 1Password SSH agent: Supported via `~/Library/Group Containers/2BUA8C4S2C.com.1password/agent.sock`
|
||||
- Default keys: `~/.ssh/id_ed25519`, `~/.ssh/id_rsa`, `~/.ssh/id_ecdsa`
|
||||
- Config: `~/.ssh/config` parsing for host aliases, identity files
|
||||
|
||||
**Git Identity:**
|
||||
- Git identity resolution - `src/main/services/parsing/GitIdentityResolver.ts`
|
||||
- Reads `.git/config` for user.name/user.email
|
||||
- Purpose: Display commit author information in UI
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
**Error Tracking:**
|
||||
- None - Local logging only via `@shared/utils/logger.ts`
|
||||
|
||||
**Logs:**
|
||||
- Console output (main process)
|
||||
- Electron DevTools console (renderer process)
|
||||
- Log prefix format: `[Domain:Service]` (e.g., `[Infrastructure:SshConnectionManager]`)
|
||||
|
||||
## CI/CD & Deployment
|
||||
|
||||
**Hosting:**
|
||||
- GitHub - Source repository and release hosting
|
||||
|
||||
**CI Pipeline:**
|
||||
- GitHub Actions
|
||||
- Workflow: `.github/workflows/ci.yml` - Lint, typecheck, test, build
|
||||
- Workflow: `.github/workflows/release.yml` - Build and publish releases
|
||||
|
||||
**Build Output:**
|
||||
- Local: `dist-electron/` (compiled code), `out/` (packaged app)
|
||||
- Distribution: `release/` directory
|
||||
- macOS: DMG, ZIP
|
||||
- Windows: NSIS installer
|
||||
- ASAR: Enabled (app code packaged into single archive)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
**Required env vars:**
|
||||
- None for local mode
|
||||
|
||||
**Optional env vars:**
|
||||
- `CLAUDE_CONTEXT_DISABLE_CACHE=1` - Disable in-memory cache
|
||||
- `NODE_ENV=development` - Development mode
|
||||
- `APPLE_TEAM_ID` - macOS code signing team ID (build-time only)
|
||||
- `SSH_AUTH_SOCK` - SSH agent socket path (runtime, optional)
|
||||
|
||||
**Secrets location:**
|
||||
- SSH keys: `~/.ssh/` directory
|
||||
- SSH agent: External (OS keychain, 1Password)
|
||||
- App config: `~/.claude/claude-devtools-config.json` (user preferences, no secrets)
|
||||
|
||||
## Webhooks & Callbacks
|
||||
|
||||
**Incoming:**
|
||||
- None - Desktop application with no server component
|
||||
|
||||
**Outgoing:**
|
||||
- None - No webhook emissions
|
||||
|
||||
## IPC Communication
|
||||
|
||||
**Electron IPC:**
|
||||
- Pattern: Main ↔ Preload ↔ Renderer communication
|
||||
- Bridge: `contextBridge` in `src/preload/index.ts` exposes `window.electronAPI`
|
||||
- Channels: Defined in `src/preload/constants/ipcChannels.ts`
|
||||
- Domains:
|
||||
- Sessions: Project/session listing, search, detail views
|
||||
- Repository: Git worktree grouping
|
||||
- Validation: Path and mention validation
|
||||
- CLAUDE.md: Configuration file reading
|
||||
- Config: App settings management
|
||||
- Notifications: Error notifications and triggers
|
||||
- Utilities: Shell operations, file watching
|
||||
- SSH: Remote connection management
|
||||
- Updater: Update lifecycle events
|
||||
|
||||
**Real-time Updates:**
|
||||
- File watching: `chokidar`-based file system monitoring
|
||||
- Debounce: 100ms
|
||||
- Events: `file-change`, `todo-change`
|
||||
- Implementation: `src/main/services/infrastructure/FileWatcher.ts`
|
||||
- Supports both local and SSH file systems
|
||||
|
||||
## External File Access
|
||||
|
||||
**Session Files:**
|
||||
- Location: `~/.claude/projects/{encoded-path}/*.jsonl`
|
||||
- Format: JSONL (JSON Lines) with Claude Code conversation data
|
||||
- Access: Read-only
|
||||
- Parser: `src/main/services/parsing/SessionParser.ts`
|
||||
|
||||
**Configuration Files:**
|
||||
- `~/.claude/CLAUDE.md` - Global Claude Code configuration
|
||||
- `{project-root}/CLAUDE.md` - Project-specific configuration
|
||||
- `{directory}/CLAUDE.md` - Directory-specific configuration
|
||||
- Reader: `src/main/services/parsing/ClaudeMdReader.ts`
|
||||
|
||||
**Git Worktrees:**
|
||||
- Reads `.git/config` for worktree detection
|
||||
- Grouper: `src/main/services/discovery/WorktreeGrouper.ts`
|
||||
|
||||
---
|
||||
|
||||
*Integration audit: 2026-02-12*
|
||||
129
.planning/codebase/STACK.md
Normal file
129
.planning/codebase/STACK.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Technology Stack
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Languages
|
||||
|
||||
**Primary:**
|
||||
- TypeScript 5.9.3 - All application code (main, preload, renderer)
|
||||
|
||||
**Secondary:**
|
||||
- JavaScript - Configuration files (eslint.config.js, postcss.config.cjs, tailwind.config.js)
|
||||
|
||||
## Runtime
|
||||
|
||||
**Environment:**
|
||||
- Node.js 25.2.1+ (ES2020/ES2022 target)
|
||||
- Electron 40.3.0 (Chromium-based desktop runtime)
|
||||
|
||||
**Package Manager:**
|
||||
- pnpm 10.25.0 (enforced via packageManager field)
|
||||
- Lockfile: pnpm-lock.yaml (present)
|
||||
|
||||
## Frameworks
|
||||
|
||||
**Core:**
|
||||
- Electron 40.3.0 - Desktop app framework (three-process architecture)
|
||||
- React 18.3.1 - UI framework for renderer process
|
||||
- React DOM 18.3.1 - React renderer
|
||||
- Zustand 4.5.0 - State management
|
||||
|
||||
**Testing:**
|
||||
- Vitest 3.1.4 - Test runner with happy-dom environment
|
||||
- @vitest/coverage-v8 3.1.4 - Code coverage
|
||||
- happy-dom 17.6.3 - Browser environment simulation
|
||||
|
||||
**Build/Dev:**
|
||||
- electron-vite 2.3.0 - Build tool (Vite-based for Electron)
|
||||
- Vite 5.4.2 - Development server and bundler
|
||||
- electron-builder 24.13.3 - Packaging and distribution
|
||||
- tsx 4.21.0 - TypeScript execution for test scripts
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
**Critical:**
|
||||
- ssh2 1.17.0 - SSH/SFTP connectivity for remote session access
|
||||
- ssh-config 5.0.4 - SSH config file parsing
|
||||
- electron-updater 6.7.3 - Auto-update functionality via GitHub releases
|
||||
|
||||
**Infrastructure:**
|
||||
- @tanstack/react-virtual 3.10.8 - Virtual scrolling for large session lists
|
||||
- @dnd-kit/core 6.3.1 + @dnd-kit/sortable 10.0.0 - Drag-and-drop for UI
|
||||
- date-fns 3.6.0 - Date formatting and manipulation
|
||||
- lucide-react 0.562.0 - Icon library
|
||||
|
||||
**Markdown/Content:**
|
||||
- react-markdown 10.1.0 - Markdown rendering
|
||||
- remark-gfm 4.0.1 - GitHub Flavored Markdown support
|
||||
- unified 11.0.5 - Text processing pipeline
|
||||
- remark-parse 11.0.0 - Markdown parser
|
||||
- mdast-util-to-hast 13.2.1 - Markdown to HTML AST conversion
|
||||
|
||||
**Styling:**
|
||||
- Tailwind CSS 3.4.1 - Utility-first CSS framework
|
||||
- @tailwindcss/typography 0.5.19 - Typography plugin
|
||||
- autoprefixer 10.4.17 - CSS vendor prefixing
|
||||
- PostCSS 8.4.35 - CSS processing
|
||||
|
||||
**Code Quality:**
|
||||
- ESLint 9.39.2 - Linting
|
||||
- typescript-eslint 8.54.0 - TypeScript ESLint rules
|
||||
- Prettier 3.8.1 - Code formatting
|
||||
- prettier-plugin-tailwindcss 0.7.2 - Tailwind class sorting
|
||||
- knip 5.82.1 - Unused code detection
|
||||
|
||||
**ESLint Plugins:**
|
||||
- eslint-plugin-react 7.37.5 + eslint-plugin-react-hooks 7.0.1 - React rules
|
||||
- eslint-plugin-jsx-a11y 6.10.2 - Accessibility rules
|
||||
- eslint-plugin-tailwindcss 3.18.2 - Tailwind CSS rules
|
||||
- eslint-plugin-boundaries 5.3.1 - Enforce Electron architecture boundaries
|
||||
- eslint-plugin-security 3.0.1 - Security vulnerability detection
|
||||
- eslint-plugin-sonarjs 3.0.6 - Code quality and bug detection
|
||||
- eslint-plugin-simple-import-sort 12.1.1 - Import sorting
|
||||
- eslint-plugin-import 2.32.0 - Import validation
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment:**
|
||||
- No .env files - Desktop app reads from `~/.claude/` directories
|
||||
- Local data: `~/.claude/projects/{encoded-path}/*.jsonl` - Session files
|
||||
- Config: `~/.claude/claude-devtools-config.json` - App configuration
|
||||
- Notifications: `~/.claude/claude-devtools-notifications.json` - Notification history
|
||||
- Remote access: SSH/SFTP to remote `~/.claude/projects/` (optional)
|
||||
|
||||
**Build:**
|
||||
- `electron.vite.config.ts` - Electron-specific Vite configuration with path aliases
|
||||
- `tsconfig.json` - Main TypeScript config (renderer + shared)
|
||||
- `tsconfig.node.json` - Main/preload process TypeScript config
|
||||
- `tsconfig.test.json` - Test TypeScript config
|
||||
- `vitest.config.ts` - Main test configuration
|
||||
- `vitest.critical.config.ts` - Critical path coverage configuration
|
||||
- `electron-builder.yml` - Build and packaging configuration
|
||||
|
||||
**Code Style:**
|
||||
- `eslint.config.js` - Flat ESLint config with process-specific rules
|
||||
- `.prettierrc.json` - Prettier formatting rules
|
||||
- `tailwind.config.js` - Tailwind CSS customization with CSS variable theme
|
||||
|
||||
**Path Aliases:**
|
||||
- `@main/*` → `src/main/*` (main process)
|
||||
- `@renderer/*` → `src/renderer/*` (renderer process)
|
||||
- `@shared/*` → `src/shared/*` (cross-process utilities)
|
||||
- `@preload/*` → `src/preload/*` (preload bridge)
|
||||
|
||||
## Platform Requirements
|
||||
|
||||
**Development:**
|
||||
- Node.js 25+ (specified in package.json engines would go here if present)
|
||||
- pnpm 10.25.0+
|
||||
- macOS or Windows (Linux support via Electron but not tested)
|
||||
|
||||
**Production:**
|
||||
- macOS: DMG and ZIP distribution
|
||||
- Windows: NSIS installer
|
||||
- Code signing: macOS notarization via Apple Team ID (env.APPLE_TEAM_ID)
|
||||
- Auto-updates: GitHub releases via electron-updater
|
||||
|
||||
---
|
||||
|
||||
*Stack analysis: 2026-02-12*
|
||||
292
.planning/codebase/STRUCTURE.md
Normal file
292
.planning/codebase/STRUCTURE.md
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# Codebase Structure
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
claude-devtools/
|
||||
├── src/
|
||||
│ ├── main/ # Electron main process (Node.js)
|
||||
│ │ ├── constants/ # Message tags, worktree patterns
|
||||
│ │ ├── ipc/ # IPC handlers by domain
|
||||
│ │ ├── services/ # Business logic services
|
||||
│ │ │ ├── analysis/ # Chunk building, semantic steps
|
||||
│ │ │ ├── discovery/ # Project/session scanning
|
||||
│ │ │ ├── error/ # Error detection, triggers
|
||||
│ │ │ ├── infrastructure/ # Cache, config, file watching
|
||||
│ │ │ └── parsing/ # JSONL parsing, message classification
|
||||
│ │ ├── types/ # Main process type definitions
|
||||
│ │ ├── utils/ # Main utilities (jsonl, pathDecoder)
|
||||
│ │ └── index.ts # Main process entry point
|
||||
│ ├── preload/ # Electron preload (IPC bridge)
|
||||
│ │ ├── constants/ # IPC channel names
|
||||
│ │ └── index.ts # ElectronAPI implementation
|
||||
│ ├── renderer/ # React application
|
||||
│ │ ├── components/ # UI components by feature
|
||||
│ │ │ ├── chat/ # Session message display
|
||||
│ │ │ ├── common/ # Shared UI primitives
|
||||
│ │ │ ├── dashboard/ # Overview pages
|
||||
│ │ │ ├── layout/ # App shell, sidebars
|
||||
│ │ │ ├── notifications/ # Notification UI
|
||||
│ │ │ ├── search/ # Search UI
|
||||
│ │ │ ├── settings/ # Settings pages
|
||||
│ │ │ └── sidebar/ # Navigation
|
||||
│ │ ├── constants/ # CSS variables, layout, colors
|
||||
│ │ ├── contexts/ # React contexts (TabUIContext)
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── store/ # Zustand state management
|
||||
│ │ │ ├── slices/ # Domain slices
|
||||
│ │ │ └── utils/ # Store helpers
|
||||
│ │ ├── types/ # Renderer type definitions
|
||||
│ │ ├── utils/ # Renderer utilities
|
||||
│ │ ├── App.tsx # React root component
|
||||
│ │ ├── main.tsx # React entry point
|
||||
│ │ └── index.css # Global styles, theme
|
||||
│ └── shared/ # Cross-process code
|
||||
│ ├── constants/ # Cache, window, colors
|
||||
│ ├── types/ # Shared type definitions
|
||||
│ └── utils/ # Pure utilities
|
||||
├── test/ # Vitest tests
|
||||
│ ├── main/ # Main process tests
|
||||
│ │ ├── ipc/ # IPC handler tests
|
||||
│ │ ├── services/ # Service tests
|
||||
│ │ └── utils/ # Utility tests
|
||||
│ ├── renderer/ # Renderer tests
|
||||
│ │ ├── hooks/ # Hook tests
|
||||
│ │ ├── store/ # Store tests
|
||||
│ │ └── utils/ # Utility tests
|
||||
│ └── shared/ # Shared code tests
|
||||
├── resources/ # App icons, assets
|
||||
├── dist/ # Build output (renderer)
|
||||
├── dist-electron/ # Build output (main, preload)
|
||||
├── .claude/ # Claude Code configuration
|
||||
│ ├── plans/ # Feature plans
|
||||
│ └── rules/ # Project rules
|
||||
├── .planning/ # GSD planning documents
|
||||
│ └── codebase/ # Codebase analysis docs
|
||||
├── package.json # Dependencies, scripts
|
||||
├── tsconfig.json # TypeScript config (base)
|
||||
├── tsconfig.node.json # TypeScript config (main)
|
||||
├── vite.config.ts # Vite build config
|
||||
└── vitest.config.ts # Vitest test config
|
||||
```
|
||||
|
||||
## Directory Purposes
|
||||
|
||||
**src/main/**
|
||||
- Purpose: Electron main process - file system access, JSONL parsing, business logic
|
||||
- Contains: Entry point, services (5 domains), IPC handlers (10 domains), types, utilities
|
||||
- Key files: `index.ts` (app lifecycle), `services/index.ts` (barrel export), `ipc/handlers.ts` (IPC setup)
|
||||
|
||||
**src/main/services/analysis/**
|
||||
- Purpose: Chunk building and session analysis
|
||||
- Contains: ChunkBuilder, ChunkFactory, ConversationGroupBuilder, ProcessLinker, SemanticStepExtractor, SemanticStepGrouper, SubagentDetailBuilder, ToolExecutionBuilder, ToolResultExtractor, ToolSummaryFormatter
|
||||
- Key files: `ChunkBuilder.ts` (orchestrator), `ChunkFactory.ts` (chunk creation), `SemanticStepExtractor.ts` (step extraction)
|
||||
|
||||
**src/main/services/discovery/**
|
||||
- Purpose: Project/session scanning, subagent resolution
|
||||
- Contains: ProjectScanner, ProjectPathResolver, SessionSearcher, SessionContentFilter, SubagentLocator, SubagentResolver, SubprojectRegistry, WorktreeGrouper
|
||||
- Key files: `ProjectScanner.ts` (file system scanning), `SubagentResolver.ts` (subagent linking)
|
||||
|
||||
**src/main/services/infrastructure/**
|
||||
- Purpose: Core application infrastructure
|
||||
- Contains: DataCache, FileWatcher, ConfigManager, NotificationManager, TriggerManager, UpdaterService, SshConnectionManager, FileSystemProvider (local/SSH)
|
||||
- Key files: `DataCache.ts` (LRU cache), `FileWatcher.ts` (file monitoring), `ConfigManager.ts` (config persistence)
|
||||
|
||||
**src/main/services/parsing/**
|
||||
- Purpose: JSONL parsing and classification
|
||||
- Contains: SessionParser, MessageClassifier, ClaudeMdReader, GitIdentityResolver
|
||||
- Key files: `SessionParser.ts` (JSONL parsing), `MessageClassifier.ts` (message categorization)
|
||||
|
||||
**src/main/services/error/**
|
||||
- Purpose: Error detection and notification triggers
|
||||
- Contains: ErrorDetector, ErrorMessageBuilder, ErrorTriggerChecker, ErrorTriggerTester, TriggerMatcher
|
||||
- Key files: `ErrorDetector.ts` (token-based detection), `TriggerMatcher.ts` (pattern matching)
|
||||
|
||||
**src/main/ipc/**
|
||||
- Purpose: IPC request handlers organized by domain
|
||||
- Contains: projects, sessions, search, subagents, validation, utility, notifications, config, ssh, updater handlers
|
||||
- Key files: `handlers.ts` (registration), `sessions.ts` (session operations), `config.ts` (configuration)
|
||||
|
||||
**src/preload/**
|
||||
- Purpose: Secure IPC bridge between main and renderer
|
||||
- Contains: ElectronAPI implementation, IPC channel constants
|
||||
- Key files: `index.ts` (contextBridge API), `constants/ipcChannels.ts` (channel names)
|
||||
|
||||
**src/renderer/components/chat/**
|
||||
- Purpose: Session message display and visualization
|
||||
- Contains: Chat groups (User, AI, System), chat history, display items, viewers (markdown, code, diff), SessionContextPanel (visible context tracking)
|
||||
- Key files: `ChatHistory.tsx` (timeline container), `AIChatGroup.tsx` (AI responses), `ContextBadge.tsx` (per-turn context popover)
|
||||
|
||||
**src/renderer/components/chat/items/**
|
||||
- Purpose: Individual message/tool item renderers
|
||||
- Contains: BaseItem, LinkedToolItem, ExecutionTrace, SubagentItem, ThinkingItem, TextItem, SlashItem, TeammateMessageItem, MetricsPill
|
||||
- Key files: `LinkedToolItem.tsx` (tool call+result), `SubagentItem.tsx` (subagent display)
|
||||
|
||||
**src/renderer/components/chat/SessionContextPanel/**
|
||||
- Purpose: Visible context tracking panel UI
|
||||
- Contains: Main panel component, section wrappers, per-injection item renderers, directory tree, formatting utils
|
||||
- Key files: `index.tsx` (panel component), `components/` (section wrappers), `items/` (injection renderers)
|
||||
|
||||
**src/renderer/store/**
|
||||
- Purpose: Zustand state management
|
||||
- Contains: Store creation, 14 domain slices, store utilities
|
||||
- Key files: `index.ts` (store composition, IPC listeners), `slices/sessionDetailSlice.ts` (session data), `slices/tabSlice.ts` (tab management)
|
||||
|
||||
**src/renderer/store/slices/**
|
||||
- Purpose: Domain-specific state slices
|
||||
- Contains: projectSlice, repositorySlice, sessionSlice, sessionDetailSlice, subagentSlice, conversationSlice, tabSlice, tabUISlice, paneSlice, uiSlice, notificationSlice, configSlice, connectionSlice, updateSlice
|
||||
- Key files: `sessionDetailSlice.ts` (session data), `tabUISlice.ts` (per-tab UI state)
|
||||
|
||||
**src/shared/**
|
||||
- Purpose: Cross-process types and pure utilities
|
||||
- Contains: Type definitions (api, notifications, visualization), utilities (tokenFormatting, modelParser, logger), constants (cache, window, colors)
|
||||
- Key files: `types/index.ts` (type re-exports), `utils/tokenFormatting.ts` (token utilities)
|
||||
|
||||
**test/**
|
||||
- Purpose: Vitest unit tests
|
||||
- Contains: Tests organized by process (main, renderer, shared), mirrors src/ structure
|
||||
- Key files: `main/services/analysis/ChunkBuilder.test.ts`, `renderer/store/sessionSlice.test.ts`
|
||||
|
||||
## Key File Locations
|
||||
|
||||
**Entry Points:**
|
||||
- `src/main/index.ts`: Main process entry (381 lines) - app lifecycle, service initialization, window creation
|
||||
- `src/renderer/main.tsx`: Renderer entry (12 lines) - React rendering
|
||||
- `src/preload/index.ts`: Preload bridge (369 lines) - ElectronAPI implementation
|
||||
- `src/renderer/App.tsx`: React root - theme, IPC listeners, layout
|
||||
|
||||
**Configuration:**
|
||||
- `package.json`: Dependencies, build scripts
|
||||
- `tsconfig.json`: Base TypeScript config
|
||||
- `tsconfig.node.json`: Main process TypeScript config
|
||||
- `vite.config.ts`: Vite build configuration
|
||||
- `vitest.config.ts`: Vitest test configuration
|
||||
- `src/renderer/index.css`: Global styles, CSS custom properties
|
||||
|
||||
**Core Logic:**
|
||||
- `src/main/services/analysis/ChunkBuilder.ts`: Chunk building orchestration
|
||||
- `src/main/services/parsing/SessionParser.ts`: JSONL parsing
|
||||
- `src/main/services/discovery/SubagentResolver.ts`: Subagent linking
|
||||
- `src/main/services/infrastructure/DataCache.ts`: LRU cache
|
||||
- `src/main/services/infrastructure/FileWatcher.ts`: File monitoring
|
||||
|
||||
**Testing:**
|
||||
- `test/main/services/analysis/ChunkBuilder.test.ts`: Chunk building tests
|
||||
- `test/main/services/parsing/SessionParser.test.ts`: JSONL parsing tests
|
||||
- `test/renderer/store/sessionSlice.test.ts`: Session state tests
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**Files:**
|
||||
- Services/Components: PascalCase - `ChunkBuilder.ts`, `SessionParser.ts`, `AIChatGroup.tsx`
|
||||
- Utilities: camelCase - `pathDecoder.ts`, `jsonl.ts`, `formatters.ts`
|
||||
- Types: camelCase - `messages.ts`, `data.ts`, `api.ts`
|
||||
- Barrel exports: `index.ts`
|
||||
|
||||
**Directories:**
|
||||
- All lowercase with domain names - `services/`, `analysis/`, `components/`
|
||||
- Feature-based organization - `components/chat/`, `components/settings/`
|
||||
|
||||
**Constants:**
|
||||
- UPPER_SNAKE_CASE - `PARALLEL_WINDOW_MS`, `MAX_CACHE_SESSIONS`, `SESSION_REFRESH_DEBOUNCE_MS`
|
||||
|
||||
**Functions:**
|
||||
- Type guards: `isXxx` - `isUserChunk()`, `isParsedRealUserMessage()`, `isAIChunk()`
|
||||
- Builders: `buildXxx` - `buildChunks()`, `buildSubagentDetail()`, `buildDisplayItems()`
|
||||
- Getters: `getXxx` - `getSessionPath()`, `getProjects()`, `getTaskCalls()`
|
||||
- Creators: `createXxx` - `createLogger()`, `createConfigSlice()`, `createWindow()`
|
||||
|
||||
**Type Guards:**
|
||||
- Message: `isParsedRealUserMessage()`, `isParsedInternalUserMessage()`, `isAssistantMessage()`
|
||||
- Chunk: `isUserChunk()`, `isAIChunk()`, `isSystemChunk()`, `isCompactChunk()`
|
||||
- Context: `isClaudeMdInjection()`, `isMentionedFileInjection()`, `isToolOutputInjection()`, `isThinkingTextInjection()`, `isTeamCoordinationInjection()`, `isUserMessageInjection()`
|
||||
|
||||
## Where to Add New Code
|
||||
|
||||
**New Service (Main Process):**
|
||||
- Implementation: `src/main/services/{domain}/{ServiceName}.ts`
|
||||
- Export: Add to `src/main/services/{domain}/index.ts` barrel
|
||||
- Usage: Import from `@main/services` or `@main/services/{domain}`
|
||||
|
||||
**New IPC Handler:**
|
||||
- Implementation: Add to existing `src/main/ipc/{domain}.ts` or create new domain file
|
||||
- Channel constant: Add to `src/preload/constants/ipcChannels.ts`
|
||||
- Registration: Add to `src/main/ipc/handlers.ts` if new domain
|
||||
- API exposure: Add method to ElectronAPI in `src/preload/index.ts`
|
||||
- Type: Update `@shared/types/api.ts` if needed
|
||||
|
||||
**New React Component:**
|
||||
- Primary code: `src/renderer/components/{feature}/{ComponentName}.tsx`
|
||||
- Feature categories: chat, common, dashboard, layout, notifications, search, settings, sidebar
|
||||
- Tests: `test/renderer/components/{feature}/{ComponentName}.test.tsx`
|
||||
|
||||
**New Zustand Slice:**
|
||||
- Implementation: `src/renderer/store/slices/{domain}Slice.ts`
|
||||
- Export: Add factory function `create{Domain}Slice`
|
||||
- Integration: Add to store composition in `src/renderer/store/index.ts`
|
||||
- Type: Update `AppState` in `src/renderer/store/types.ts`
|
||||
|
||||
**New Hook:**
|
||||
- Implementation: `src/renderer/hooks/use{Name}.ts`
|
||||
- Tests: `test/renderer/hooks/use{Name}.test.ts`
|
||||
- Import: Direct import from file (no barrel exports for hooks)
|
||||
|
||||
**Utilities:**
|
||||
- Main process: `src/main/utils/{utilityName}.ts`
|
||||
- Renderer: `src/renderer/utils/{utilityName}.ts`
|
||||
- Shared (pure): `src/shared/utils/{utilityName}.ts`
|
||||
|
||||
**Types:**
|
||||
- Main process only: `src/main/types/{name}.ts`
|
||||
- Renderer only: `src/renderer/types/{name}.ts`
|
||||
- Shared across processes: `src/shared/types/{name}.ts`
|
||||
|
||||
## Special Directories
|
||||
|
||||
**node_modules/**
|
||||
- Purpose: Package dependencies
|
||||
- Generated: Yes (via `pnpm install`)
|
||||
- Committed: No
|
||||
|
||||
**dist/ and dist-electron/**
|
||||
- Purpose: Build output (production bundles)
|
||||
- Generated: Yes (via `pnpm build`)
|
||||
- Committed: No
|
||||
|
||||
**.planning/codebase/**
|
||||
- Purpose: GSD codebase analysis documents
|
||||
- Generated: Yes (via `/gsd:map-codebase`)
|
||||
- Committed: Yes (planning metadata)
|
||||
|
||||
**.claude/plans/**
|
||||
- Purpose: Feature implementation plans
|
||||
- Generated: Yes (via `/gsd:plan-phase`)
|
||||
- Committed: Yes (project planning)
|
||||
|
||||
**test/mocks/**
|
||||
- Purpose: Test fixtures and mock data
|
||||
- Generated: No (manually created)
|
||||
- Committed: Yes (test infrastructure)
|
||||
|
||||
**resources/**
|
||||
- Purpose: Application icons and assets
|
||||
- Generated: No (manually created)
|
||||
- Committed: Yes (app resources)
|
||||
|
||||
**src/renderer/components/chat/SessionContextPanel/**
|
||||
- Purpose: Visible context tracking panel UI (6 context categories)
|
||||
- Generated: No
|
||||
- Committed: Yes
|
||||
- Special: Deep component hierarchy with section wrappers and item renderers
|
||||
|
||||
**src/main/services/**
|
||||
- Purpose: Domain-organized business logic
|
||||
- Generated: No
|
||||
- Committed: Yes
|
||||
- Special: Each domain has barrel export (`index.ts`)
|
||||
|
||||
---
|
||||
|
||||
*Structure analysis: 2026-02-12*
|
||||
506
.planning/codebase/TESTING.md
Normal file
506
.planning/codebase/TESTING.md
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
# Testing Patterns
|
||||
|
||||
**Analysis Date:** 2026-02-12
|
||||
|
||||
## Test Framework
|
||||
|
||||
**Runner:**
|
||||
- Vitest 3.1.4
|
||||
- Config: `/Users/bskim/claude-devtools/vitest.config.ts`
|
||||
|
||||
**Environment:**
|
||||
- `happy-dom` 17.4.6 (DOM simulation)
|
||||
- Globals enabled (`describe`, `it`, `expect` available without imports)
|
||||
|
||||
**Assertion Library:**
|
||||
- Vitest built-in assertions (Chai-compatible API)
|
||||
|
||||
**Run Commands:**
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # Coverage report
|
||||
pnpm test:coverage:critical # Critical path coverage only
|
||||
pnpm test:chunks # Chunk building tests
|
||||
pnpm test:semantic # Semantic step extraction
|
||||
pnpm test:noise # Noise filtering tests
|
||||
pnpm test:task-filtering # Task tool filtering
|
||||
```
|
||||
|
||||
## Test File Organization
|
||||
|
||||
**Location:**
|
||||
Co-located in separate `test/` directory, mirroring source structure.
|
||||
|
||||
**Naming:**
|
||||
`*.test.ts` - matches source file name exactly.
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
test/
|
||||
├── main/
|
||||
│ ├── ipc/ # IPC handler tests
|
||||
│ │ ├── configValidation.test.ts
|
||||
│ │ └── guards.test.ts
|
||||
│ ├── services/ # Service layer tests
|
||||
│ │ ├── analysis/ # ChunkBuilder, etc.
|
||||
│ │ ├── discovery/ # ProjectPathResolver, SessionSearcher
|
||||
│ │ ├── infrastructure/ # FileWatcher
|
||||
│ │ └── parsing/ # MessageClassifier, SessionParser
|
||||
│ └── utils/ # Utility function tests
|
||||
│ ├── jsonl.test.ts
|
||||
│ ├── pathDecoder.test.ts
|
||||
│ ├── pathValidation.test.ts
|
||||
│ ├── regexValidation.test.ts
|
||||
│ └── tokenizer.test.ts
|
||||
├── renderer/
|
||||
│ ├── hooks/ # Hook tests
|
||||
│ │ ├── navigationUtils.test.ts
|
||||
│ │ ├── useAutoScrollBottom.test.ts
|
||||
│ │ ├── useSearchContextNavigation.test.ts
|
||||
│ │ └── useVisibleAIGroup.test.ts
|
||||
│ ├── store/ # Zustand store slice tests
|
||||
│ │ ├── notificationSlice.test.ts
|
||||
│ │ ├── paneSlice.test.ts
|
||||
│ │ ├── pathResolution.test.ts
|
||||
│ │ ├── sessionSlice.test.ts
|
||||
│ │ ├── tabSlice.test.ts
|
||||
│ │ └── tabUISlice.test.ts
|
||||
│ └── utils/ # Renderer utilities
|
||||
│ ├── claudeMdTracker.test.ts
|
||||
│ ├── dateGrouping.test.ts
|
||||
│ ├── formatters.test.ts
|
||||
│ └── pathUtils.test.ts
|
||||
├── shared/
|
||||
│ └── utils/ # Shared utilities
|
||||
│ ├── markdownSearchRendererAlignment.test.ts
|
||||
│ ├── markdownTextSearch.test.ts
|
||||
│ ├── modelParser.test.ts
|
||||
│ └── tokenFormatting.test.ts
|
||||
├── mocks/ # Test fixtures and mocks
|
||||
│ └── electronAPI.ts # Mock window.electronAPI
|
||||
└── setup.ts # Global test setup
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
**Suite Organization:**
|
||||
```typescript
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { ChunkBuilder } from '../../../../src/main/services/analysis/ChunkBuilder';
|
||||
import { isAIChunk, isUserChunk } from '../../../../src/main/types';
|
||||
|
||||
describe('ChunkBuilder', () => {
|
||||
const builder = new ChunkBuilder();
|
||||
|
||||
describe('buildChunks', () => {
|
||||
it('should return empty array for empty input', () => {
|
||||
const chunks = builder.buildChunks([]);
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out sidechain messages', () => {
|
||||
const messages = [
|
||||
createMessage({ type: 'user', isSidechain: false }),
|
||||
createMessage({ type: 'assistant', isSidechain: true }),
|
||||
];
|
||||
|
||||
const chunks = builder.buildChunks(messages);
|
||||
expect(chunks).toHaveLength(1);
|
||||
expect(isUserChunk(chunks[0])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserChunk creation', () => {
|
||||
// Nested describe for logical grouping
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Patterns:**
|
||||
- Top-level `describe` per class/module
|
||||
- Nested `describe` per method/function
|
||||
- Descriptive `it` statements ("should do X when Y")
|
||||
- Arrange-Act-Assert pattern
|
||||
|
||||
## Fixtures and Factories
|
||||
|
||||
**Test Data:**
|
||||
Helper functions to create test objects:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Creates a minimal ParsedMessage for testing.
|
||||
*/
|
||||
function createMessage(overrides: Partial<ParsedMessage>): ParsedMessage {
|
||||
return {
|
||||
uuid: `msg-${Math.random().toString(36).slice(2, 11)}`,
|
||||
parentUuid: null,
|
||||
type: 'user',
|
||||
timestamp: new Date(),
|
||||
content: '',
|
||||
isSidechain: false,
|
||||
isMeta: false,
|
||||
toolCalls: [],
|
||||
toolResults: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal Process (subagent) for testing.
|
||||
*/
|
||||
function createSubagent(overrides: Partial<Process>): Process {
|
||||
return {
|
||||
id: `agent-${Math.random().toString(36).slice(2, 11)}`,
|
||||
filePath: '/path/to/agent.jsonl',
|
||||
parentTaskId: 'task-1',
|
||||
description: 'Test subagent',
|
||||
startTime: new Date(),
|
||||
endTime: new Date(),
|
||||
durationMs: 1000,
|
||||
isOngoing: false,
|
||||
messages: [],
|
||||
metrics: {
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
cacheReadTokens: 0,
|
||||
cacheCreationTokens: 0,
|
||||
totalTokens: 150,
|
||||
messageCount: 2,
|
||||
durationMs: 1000,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Location:**
|
||||
Defined in test files (not centralized) for visibility and simplicity.
|
||||
|
||||
## Mocking
|
||||
|
||||
**Framework:** Vitest built-in mocking (`vi.fn()`, `vi.mock()`, `vi.spyOn()`)
|
||||
|
||||
**ElectronAPI Mock Pattern:**
|
||||
```typescript
|
||||
// test/mocks/electronAPI.ts
|
||||
export interface MockElectronAPI {
|
||||
getProjects: ReturnType<typeof vi.fn>;
|
||||
getSessions: ReturnType<typeof vi.fn>;
|
||||
getSessionsPaginated: ReturnType<typeof vi.fn>;
|
||||
// ... all IPC methods
|
||||
}
|
||||
|
||||
export function installMockElectronAPI(): MockElectronAPI {
|
||||
const mock: MockElectronAPI = {
|
||||
getProjects: vi.fn(),
|
||||
getSessions: vi.fn(),
|
||||
// ...
|
||||
};
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
electronAPI: mock,
|
||||
});
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
// Usage in tests:
|
||||
import { installMockElectronAPI, type MockElectronAPI } from '../../mocks/electronAPI';
|
||||
|
||||
describe('sessionSlice', () => {
|
||||
let mockAPI: MockElectronAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAPI = installMockElectronAPI();
|
||||
});
|
||||
|
||||
it('should fetch sessions', async () => {
|
||||
mockAPI.getSessions.mockResolvedValue([/* data */]);
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Console Mocking:**
|
||||
Automatic via `test/setup.ts` - all tests fail if unexpected console.error/warn occurs:
|
||||
```typescript
|
||||
// test/setup.ts
|
||||
beforeEach(() => {
|
||||
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const unexpectedErrors = errorSpy.mock.calls.map(formatConsoleCall);
|
||||
const unexpectedWarnings = warnSpy.mock.calls.map(formatConsoleCall);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
|
||||
expect(unexpectedErrors, `Unexpected console.error calls:\n${unexpectedErrors.join('\n')}`).toEqual([]);
|
||||
expect(unexpectedWarnings, `Unexpected console.warn calls:\n${unexpectedWarnings.join('\n')}`).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
**What to Mock:**
|
||||
- `window.electronAPI` - Always mock in renderer tests
|
||||
- File system operations - Mock when testing logic, not I/O
|
||||
- External dependencies - Mock when testing integration points
|
||||
|
||||
**What NOT to Mock:**
|
||||
- Internal utilities (test them directly)
|
||||
- Type guards (pure functions)
|
||||
- Formatters and transformers (integration is valuable)
|
||||
|
||||
## Coverage
|
||||
|
||||
**Requirements:** No enforced minimum (quality over coverage)
|
||||
|
||||
**Provider:** v8 (native V8 coverage)
|
||||
|
||||
**Reporters:**
|
||||
- `text` - Terminal output
|
||||
- `json` - Machine-readable
|
||||
- `html` - Interactive browser report
|
||||
|
||||
**View Coverage:**
|
||||
```bash
|
||||
pnpm test:coverage # Full coverage
|
||||
pnpm test:coverage:critical # Critical paths only
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- `src/**/*.ts`
|
||||
- `src/**/*.tsx`
|
||||
|
||||
**Excludes:**
|
||||
- `src/**/*.d.ts` (type definitions)
|
||||
- `src/main/index.ts` (entry point)
|
||||
- `src/preload/index.ts` (entry point)
|
||||
|
||||
**Critical Path Config:**
|
||||
Separate config at `/Users/bskim/claude-devtools/vitest.critical.config.ts` focuses on:
|
||||
- Chunk building (`ChunkBuilder.test.ts`)
|
||||
- Message classification (`MessageClassifier.test.ts`)
|
||||
- Session parsing (`SessionParser.test.ts`)
|
||||
|
||||
## Test Types
|
||||
|
||||
**Unit Tests:**
|
||||
Test individual functions, classes, and utilities in isolation.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
describe('pathDecoder', () => {
|
||||
describe('encodePath', () => {
|
||||
it('should encode absolute POSIX paths', () => {
|
||||
expect(encodePath('/Users/username/project')).toBe('-Users-username-project');
|
||||
});
|
||||
|
||||
it('should encode Windows paths', () => {
|
||||
expect(encodePath('C:\\Users\\username\\project')).toBe('-C:-Users-username-project');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Integration Tests:**
|
||||
Test interactions between modules (e.g., ChunkBuilder + MessageClassifier).
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
it('should link subagents to AIChunks', () => {
|
||||
const messages = [
|
||||
createMessage({ type: 'assistant', toolCalls: [{ isTask: true, id: 'task-1' }] }),
|
||||
];
|
||||
const subagents = [
|
||||
createSubagent({ parentTaskId: 'task-1' }),
|
||||
];
|
||||
|
||||
const chunks = builder.buildChunks(messages, subagents);
|
||||
const aiChunk = chunks.find(isAIChunk);
|
||||
|
||||
expect(aiChunk?.subagents).toHaveLength(1);
|
||||
});
|
||||
```
|
||||
|
||||
**Store Tests:**
|
||||
Test Zustand slice behavior (state updates, async actions).
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
describe('sessionSlice', () => {
|
||||
it('should update sessions on fetch', async () => {
|
||||
mockAPI.getSessions.mockResolvedValue([
|
||||
{ id: 'session-1', createdAt: '2024-01-15T10:00:00Z' },
|
||||
]);
|
||||
|
||||
await store.getState().fetchSessions('project-1');
|
||||
|
||||
expect(store.getState().sessions).toHaveLength(1);
|
||||
expect(store.getState().sessionsLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**E2E Tests:**
|
||||
Not currently implemented.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Async Testing:**
|
||||
```typescript
|
||||
it('should handle async operations', async () => {
|
||||
mockAPI.getSessions.mockResolvedValue([/* data */]);
|
||||
|
||||
await store.getState().fetchSessions('project-1');
|
||||
|
||||
expect(store.getState().sessions).toHaveLength(1);
|
||||
});
|
||||
```
|
||||
|
||||
**Error Testing:**
|
||||
```typescript
|
||||
it('should handle fetch error', async () => {
|
||||
mockAPI.getSessions.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await store.getState().fetchSessions('project-1');
|
||||
|
||||
expect(store.getState().sessionsError).toBe('Network error');
|
||||
expect(store.getState().sessionsLoading).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
**Timing/Debounce Testing:**
|
||||
```typescript
|
||||
it('should debounce rapid calls', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const debounced = debounce(callback, 100);
|
||||
|
||||
debounced();
|
||||
debounced();
|
||||
debounced();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
```
|
||||
|
||||
**State Transitions:**
|
||||
```typescript
|
||||
it('should set loading state during fetch', async () => {
|
||||
mockAPI.getSessions.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve([]), 100))
|
||||
);
|
||||
|
||||
const fetchPromise = store.getState().fetchSessions('project-1');
|
||||
expect(store.getState().sessionsLoading).toBe(true);
|
||||
|
||||
await fetchPromise;
|
||||
expect(store.getState().sessionsLoading).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
## Test-Specific ESLint Relaxations
|
||||
|
||||
Tests use relaxed TypeScript rules (from `eslint.config.js`):
|
||||
|
||||
```typescript
|
||||
// Relaxed for tests
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
```
|
||||
|
||||
**Rationale:** Test code prioritizes readability and flexibility over type safety.
|
||||
|
||||
## Setup and Teardown
|
||||
|
||||
**Global Setup:**
|
||||
`/Users/bskim/claude-devtools/test/setup.ts` runs before each test file:
|
||||
- Mocks `process.env.HOME`
|
||||
- Installs console spies (fail on unexpected errors/warnings)
|
||||
|
||||
**Per-Suite Setup:**
|
||||
```typescript
|
||||
describe('MyService', () => {
|
||||
let mockAPI: MockElectronAPI;
|
||||
let store: TestStore;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAPI = installMockElectronAPI();
|
||||
store = createTestStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Path Aliases in Tests
|
||||
|
||||
Vitest config mirrors TypeScript paths:
|
||||
```typescript
|
||||
resolve: {
|
||||
alias: {
|
||||
'@shared': resolve(__dirname, 'src/shared'),
|
||||
'@main': resolve(__dirname, 'src/main'),
|
||||
'@renderer': resolve(__dirname, 'src/renderer'),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use same aliases as source code:
|
||||
```typescript
|
||||
import { ChunkBuilder } from '@main/services';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { formatTokens } from '@shared/utils/tokenFormatting';
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Test Independence:**
|
||||
Each test should run independently - no shared state between tests.
|
||||
|
||||
**Descriptive Names:**
|
||||
```typescript
|
||||
// Good
|
||||
it('should return empty array when no messages provided', () => {});
|
||||
|
||||
// Bad
|
||||
it('returns []', () => {});
|
||||
```
|
||||
|
||||
**Single Assertion Focus:**
|
||||
Prefer multiple small tests over one large test with many assertions.
|
||||
|
||||
**Factory Functions:**
|
||||
Use factory functions for test data creation - more maintainable than inline objects.
|
||||
|
||||
**Mock Minimally:**
|
||||
Only mock external boundaries (IPC, file system) - test real logic.
|
||||
|
||||
**Console Discipline:**
|
||||
If a test legitimately logs errors, it will fail. Either:
|
||||
1. Fix the code to not log errors
|
||||
2. Mock the specific logger call
|
||||
3. Adjust test expectations
|
||||
|
||||
---
|
||||
|
||||
*Testing analysis: 2026-02-12*
|
||||
12
.planning/config.json
Normal file
12
.planning/config.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"mode": "yolo",
|
||||
"depth": "quick",
|
||||
"parallelization": true,
|
||||
"commit_docs": false,
|
||||
"model_profile": "balanced",
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": false,
|
||||
"verifier": false
|
||||
}
|
||||
}
|
||||
329
.planning/phases/01-provider-plumbing/01-01-PLAN.md
Normal file
329
.planning/phases/01-provider-plumbing/01-01-PLAN.md
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
---
|
||||
phase: 01-provider-plumbing
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/main/services/discovery/ProjectScanner.ts
|
||||
- src/main/services/parsing/SessionParser.ts
|
||||
- src/main/services/discovery/SubagentResolver.ts
|
||||
- src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
- src/main/services/analysis/ChunkBuilder.ts
|
||||
- test/main/services/parsing/SessionParser.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "SessionParser.parseSessionFile() passes FileSystemProvider to parseJsonlFile()"
|
||||
- "SubagentResolver.parseSubagentFile() passes FileSystemProvider to parseJsonlFile()"
|
||||
- "SubagentDetailBuilder uses FileSystemProvider.exists() instead of fs.access()"
|
||||
- "SubagentDetailBuilder constructs paths using ProjectScanner.getSubagentsPath() instead of os.homedir()"
|
||||
- "No service in the parsing stack imports fs/promises directly for session data reads"
|
||||
artifacts:
|
||||
- path: "src/main/services/discovery/ProjectScanner.ts"
|
||||
provides: "getFileSystemProvider() getter"
|
||||
contains: "getFileSystemProvider"
|
||||
- path: "src/main/services/parsing/SessionParser.ts"
|
||||
provides: "Provider-aware session parsing"
|
||||
contains: "getFileSystemProvider"
|
||||
- path: "src/main/services/discovery/SubagentResolver.ts"
|
||||
provides: "Provider-aware subagent resolution"
|
||||
contains: "getFileSystemProvider"
|
||||
- path: "src/main/services/analysis/SubagentDetailBuilder.ts"
|
||||
provides: "Provider-aware subagent detail building"
|
||||
contains: "fsProvider"
|
||||
key_links:
|
||||
- from: "src/main/services/parsing/SessionParser.ts"
|
||||
to: "src/main/utils/jsonl.ts"
|
||||
via: "parseJsonlFile(filePath, provider)"
|
||||
pattern: "parseJsonlFile\\(.*,.*getFileSystemProvider"
|
||||
- from: "src/main/services/discovery/SubagentResolver.ts"
|
||||
to: "src/main/utils/jsonl.ts"
|
||||
via: "parseJsonlFile(filePath, provider)"
|
||||
pattern: "parseJsonlFile\\(.*,.*getFileSystemProvider"
|
||||
- from: "src/main/services/analysis/SubagentDetailBuilder.ts"
|
||||
to: "src/main/services/infrastructure/FileSystemProvider.ts"
|
||||
via: "fsProvider.exists() replaces fs.access()"
|
||||
pattern: "fsProvider\\.exists"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Thread FileSystemProvider through the entire session parsing stack so SSH sessions load correctly.
|
||||
|
||||
Purpose: Currently, SessionParser, SubagentResolver, and SubagentDetailBuilder all call parseJsonlFile() without passing a FileSystemProvider, causing silent fallback to LocalFileSystemProvider. In SSH mode, this means sessions show "No conversation history" because the local filesystem has no matching files. SubagentDetailBuilder additionally hardcodes os.homedir() for path construction. This plan fixes all three services to use the provider from ProjectScanner.
|
||||
|
||||
Output: All three services use FileSystemProvider consistently. SSH sessions display full conversation history and subagent drill-down works over SFTP.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/bskim/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/bskim/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-provider-plumbing/01-RESEARCH.md
|
||||
|
||||
@src/main/services/infrastructure/FileSystemProvider.ts
|
||||
@src/main/services/discovery/ProjectScanner.ts
|
||||
@src/main/services/parsing/SessionParser.ts
|
||||
@src/main/services/discovery/SubagentResolver.ts
|
||||
@src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
@src/main/services/analysis/ChunkBuilder.ts
|
||||
@src/main/utils/jsonl.ts
|
||||
@src/main/ipc/subagents.ts
|
||||
@src/main/index.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add provider getter to ProjectScanner and thread through SessionParser + SubagentResolver</name>
|
||||
<files>
|
||||
src/main/services/discovery/ProjectScanner.ts
|
||||
src/main/services/parsing/SessionParser.ts
|
||||
src/main/services/discovery/SubagentResolver.ts
|
||||
test/main/services/parsing/SessionParser.test.ts
|
||||
</files>
|
||||
<action>
|
||||
**ProjectScanner (1 change):**
|
||||
Add a public getter method `getFileSystemProvider()` that returns `this.fsProvider`. Place it in the "Utility Methods" section near `getProjectsDir()` and `getTodosDir()`:
|
||||
```typescript
|
||||
getFileSystemProvider(): FileSystemProvider {
|
||||
return this.fsProvider;
|
||||
}
|
||||
```
|
||||
This requires adding `FileSystemProvider` to the type imports (it's currently only imported as a type for the constructor parameter — verify the import is accessible for the return type annotation).
|
||||
|
||||
**SessionParser (2 changes):**
|
||||
1. In `parseSessionFile()` (line 77), change:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath);
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
||||
```
|
||||
|
||||
2. In `parseSubagentFile()` (line 342), change:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath);
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
||||
```
|
||||
|
||||
No constructor changes needed — SessionParser already receives ProjectScanner in its constructor.
|
||||
|
||||
**SubagentResolver (1 change):**
|
||||
In the private `parseSubagentFile()` method (line 88), change:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath);
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
||||
```
|
||||
|
||||
No constructor changes needed — SubagentResolver already receives ProjectScanner in its constructor.
|
||||
|
||||
**Test updates (SessionParser.test.ts):**
|
||||
The existing `mockProjectScanner` object (lines 24-33) must include the new `getFileSystemProvider` method. Add:
|
||||
```typescript
|
||||
getFileSystemProvider: vi.fn().mockReturnValue(new LocalFileSystemProvider()),
|
||||
```
|
||||
Import `LocalFileSystemProvider` from `@main/services/infrastructure/LocalFileSystemProvider` (or use a minimal mock object with type 'local' if import resolution is an issue in tests — check the existing test's module resolution).
|
||||
</action>
|
||||
<verify>
|
||||
Run `pnpm typecheck` — no type errors in the modified files.
|
||||
Run `pnpm test test/main/services/parsing/SessionParser.test.ts` — all existing tests pass.
|
||||
Grep for bare `parseJsonlFile(filePath)` calls (without second argument) in SessionParser.ts and SubagentResolver.ts — should find zero matches:
|
||||
```bash
|
||||
grep -n 'parseJsonlFile(filePath)' src/main/services/parsing/SessionParser.ts src/main/services/discovery/SubagentResolver.ts
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
SessionParser.parseSessionFile() and parseSubagentFile() both pass FileSystemProvider to parseJsonlFile(). SubagentResolver.parseSubagentFile() passes FileSystemProvider to parseJsonlFile(). ProjectScanner exposes getFileSystemProvider() getter. All existing SessionParser tests pass with updated mock.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Fix SubagentDetailBuilder to use FileSystemProvider instead of hardcoded fs/os imports</name>
|
||||
<files>
|
||||
src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
src/main/services/analysis/ChunkBuilder.ts
|
||||
</files>
|
||||
<action>
|
||||
**SubagentDetailBuilder (major refactor of buildSubagentDetail function):**
|
||||
|
||||
The current function (lines 39-135) has three problems:
|
||||
1. Imports `fs/promises` dynamically (line 48) — bypasses provider abstraction
|
||||
2. Uses `os.homedir()` to construct paths (line 53) — always resolves to local home directory
|
||||
3. Uses `fs.access()` for existence check (line 58) — bypasses provider abstraction
|
||||
|
||||
Refactor the function signature to accept `fsProvider` and `projectsDir` parameters:
|
||||
```typescript
|
||||
export async function buildSubagentDetail(
|
||||
projectId: string,
|
||||
_sessionId: string,
|
||||
subagentId: string,
|
||||
sessionParser: SessionParser,
|
||||
subagentResolver: SubagentResolver,
|
||||
buildChunksFn: (messages: ParsedMessage[], subagents: Process[]) => EnhancedChunk[],
|
||||
fsProvider: FileSystemProvider,
|
||||
projectsDir: string
|
||||
): Promise<SubagentDetail | null>
|
||||
```
|
||||
|
||||
Replace the function body's path construction and existence check (lines 47-62):
|
||||
- Remove the dynamic `fs`, `path`, `os` imports at lines 48-50.
|
||||
- Add a static `import * as path from 'path'` at the top of the file (alongside existing imports).
|
||||
- Add `import type { FileSystemProvider } from '../infrastructure/FileSystemProvider'` at the top.
|
||||
- Replace path construction:
|
||||
```typescript
|
||||
// OLD (lines 53-54):
|
||||
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
||||
const subagentPath = path.join(claudeDir, projectId, 'subagents', `agent-${subagentId}.jsonl`);
|
||||
|
||||
// NEW:
|
||||
const subagentPath = path.join(projectsDir, projectId, 'subagents', `agent-${subagentId}.jsonl`);
|
||||
```
|
||||
- Replace existence check:
|
||||
```typescript
|
||||
// OLD (lines 57-62):
|
||||
try {
|
||||
await fs.access(subagentPath);
|
||||
} catch {
|
||||
logger.warn(`Subagent file not found: ${subagentPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// NEW:
|
||||
if (!(await fsProvider.exists(subagentPath))) {
|
||||
logger.warn(`Subagent file not found: ${subagentPath}`);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
The rest of the function body (lines 64-134) can remain unchanged — it delegates to `sessionParser.parseSessionFile()` and `subagentResolver.resolveSubagents()` which now use the provider from Task 1.
|
||||
|
||||
**ChunkBuilder (update the delegation call):**
|
||||
|
||||
In `ChunkBuilder.buildSubagentDetail()` (lines 426-442), update the call to pass the new parameters. The ChunkBuilder needs access to `fsProvider` and `projectsDir`. Two options — use the simpler one: pass them as parameters from the IPC layer.
|
||||
|
||||
Update `ChunkBuilder.buildSubagentDetail()` signature and implementation:
|
||||
```typescript
|
||||
async buildSubagentDetail(
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
subagentId: string,
|
||||
sessionParser: SessionParser,
|
||||
subagentResolver: SubagentResolver,
|
||||
fsProvider: FileSystemProvider,
|
||||
projectsDir: string
|
||||
): Promise<SubagentDetail | null> {
|
||||
return buildSubagentDetailFn(
|
||||
projectId,
|
||||
sessionId,
|
||||
subagentId,
|
||||
sessionParser,
|
||||
subagentResolver,
|
||||
(messages, subagents) => this.buildChunks(messages, subagents),
|
||||
fsProvider,
|
||||
projectsDir
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Add `import type { FileSystemProvider } from '../infrastructure/FileSystemProvider'` to ChunkBuilder.ts imports if not already present.
|
||||
|
||||
**IPC subagents.ts (update the call site):**
|
||||
|
||||
In `handleGetSubagentDetail()` (line 101), the call to `chunkBuilder.buildSubagentDetail()` needs `fsProvider` and `projectsDir`. The subagent handler module has access to `sessionParser` which has `projectScanner`. However, the handler doesn't have direct access to projectScanner.
|
||||
|
||||
Add `projectScanner` to the subagent handler's service dependencies:
|
||||
1. Add `let projectScanner: ProjectScanner;` to the module-level service variables
|
||||
2. Update `initializeSubagentHandlers` to accept and store `projectScanner`
|
||||
3. In the handler, get provider and projectsDir from projectScanner:
|
||||
```typescript
|
||||
const fsProvider = projectScanner.getFileSystemProvider();
|
||||
const projectsDir = projectScanner.getProjectsDir();
|
||||
```
|
||||
4. Pass them to the call:
|
||||
```typescript
|
||||
const builtDetail = await chunkBuilder.buildSubagentDetail(
|
||||
safeProjectId,
|
||||
safeSessionId,
|
||||
safeSubagentId,
|
||||
sessionParser,
|
||||
subagentResolver,
|
||||
fsProvider,
|
||||
projectsDir
|
||||
);
|
||||
```
|
||||
|
||||
**IPC handlers.ts (update initialization calls):**
|
||||
|
||||
Update `initializeSubagentHandlers` calls in both `initializeIpcHandlers` and `reinitializeServiceHandlers` to pass `scanner`:
|
||||
```typescript
|
||||
initializeSubagentHandlers(builder, cache, parser, resolver, scanner);
|
||||
```
|
||||
|
||||
**src/main/index.ts** — no changes needed since it calls `initializeIpcHandlers` and `reinitializeServiceHandlers` which will propagate the change.
|
||||
</action>
|
||||
<verify>
|
||||
Run `pnpm typecheck` — no type errors across the entire project.
|
||||
Run `pnpm test` — all tests pass (especially ChunkBuilder tests).
|
||||
Grep for `fs/promises` or `os.homedir` in SubagentDetailBuilder.ts — should find zero matches:
|
||||
```bash
|
||||
grep -n "fs/promises\|os\.homedir" src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
```
|
||||
Grep for bare `parseJsonlFile(filePath)` (without second arg) across all services — should find zero matches:
|
||||
```bash
|
||||
grep -rn 'parseJsonlFile(filePath)$' src/main/services/
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
SubagentDetailBuilder uses fsProvider.exists() instead of fs.access(), constructs paths using projectsDir parameter instead of os.homedir(), and no longer imports fs/promises or os. ChunkBuilder passes fsProvider and projectsDir through to SubagentDetailBuilder. IPC subagent handler obtains provider from ProjectScanner and passes it through the call chain. Full session parsing and subagent drill-down chain uses FileSystemProvider consistently.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After both tasks are complete, verify the full provider chain:
|
||||
|
||||
1. **Type safety:** `pnpm typecheck` passes with zero errors
|
||||
2. **Test suite:** `pnpm test` passes — all existing tests remain green
|
||||
3. **No local filesystem leaks in services:**
|
||||
```bash
|
||||
# Should find ZERO matches in these three files:
|
||||
grep -n "fs/promises\|fs\.access\|os\.homedir\|parseJsonlFile(filePath)" \
|
||||
src/main/services/parsing/SessionParser.ts \
|
||||
src/main/services/discovery/SubagentResolver.ts \
|
||||
src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
```
|
||||
4. **Provider flows through entire chain:** Trace the call path:
|
||||
- `index.ts` creates `ProjectScanner(projectsDir, undefined, provider)` in SSH mode
|
||||
- `SessionParser.parseSessionFile()` calls `parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider())`
|
||||
- `SubagentResolver.parseSubagentFile()` calls `parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider())`
|
||||
- `SubagentDetailBuilder.buildSubagentDetail()` receives `fsProvider` and `projectsDir`, uses `fsProvider.exists()` for file check
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All parseJsonlFile() calls in SessionParser and SubagentResolver pass the FileSystemProvider from ProjectScanner
|
||||
- SubagentDetailBuilder does not import fs/promises, os, or use os.homedir()
|
||||
- SubagentDetailBuilder uses fsProvider.exists() for file existence checks
|
||||
- SubagentDetailBuilder uses projectsDir parameter for path construction (not hardcoded ~/.claude/projects)
|
||||
- pnpm typecheck passes
|
||||
- pnpm test passes
|
||||
- The provider chain is complete: SSH provider set in index.ts flows all the way to parseJsonlFile() in every code path
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-provider-plumbing/01-01-SUMMARY.md`
|
||||
</output>
|
||||
153
.planning/phases/01-provider-plumbing/01-01-SUMMARY.md
Normal file
153
.planning/phases/01-provider-plumbing/01-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
---
|
||||
phase: 01-provider-plumbing
|
||||
plan: 01
|
||||
subsystem: session-parsing
|
||||
tags: [ssh-support, provider-threading, refactoring]
|
||||
dependency-graph:
|
||||
requires: [FileSystemProvider, ProjectScanner]
|
||||
provides: [provider-aware-parsing, provider-aware-subagent-detail]
|
||||
affects: [SessionParser, SubagentResolver, SubagentDetailBuilder, ChunkBuilder, IPC-handlers]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [provider-injection, dependency-threading]
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- src/main/services/discovery/ProjectScanner.ts
|
||||
- src/main/services/parsing/SessionParser.ts
|
||||
- src/main/services/discovery/SubagentResolver.ts
|
||||
- src/main/services/analysis/SubagentDetailBuilder.ts
|
||||
- src/main/services/analysis/ChunkBuilder.ts
|
||||
- src/main/ipc/subagents.ts
|
||||
- src/main/ipc/handlers.ts
|
||||
- test/main/services/parsing/SessionParser.test.ts
|
||||
decisions:
|
||||
- "Added getFileSystemProvider() getter to ProjectScanner for consistent provider access across services"
|
||||
- "Threaded provider through all three parseJsonlFile() call sites instead of relying on optional parameter fallback"
|
||||
- "Refactored SubagentDetailBuilder to accept fsProvider and projectsDir as explicit parameters instead of using dynamic imports"
|
||||
- "Propagated ProjectScanner to IPC subagent handler to obtain provider and projectsDir at runtime"
|
||||
metrics:
|
||||
duration: 4
|
||||
completed: 2026-02-12T00:15:16Z
|
||||
---
|
||||
|
||||
# Phase 01 Plan 01: Thread FileSystemProvider Through Session Parsing Stack Summary
|
||||
|
||||
**FileSystemProvider now flows consistently from ProjectScanner through SessionParser, SubagentResolver, and SubagentDetailBuilder, enabling SSH sessions to load conversation history over SFTP.**
|
||||
|
||||
## What Was Built
|
||||
|
||||
Threaded the `FileSystemProvider` abstraction through the entire session parsing stack, eliminating silent fallbacks to `LocalFileSystemProvider` that caused SSH sessions to show "No conversation history."
|
||||
|
||||
### Task 1: Provider Threading Through SessionParser and SubagentResolver
|
||||
|
||||
Added `getFileSystemProvider()` getter to `ProjectScanner` and updated three `parseJsonlFile()` call sites to pass the provider:
|
||||
|
||||
1. **ProjectScanner.getFileSystemProvider()** - New getter exposes the provider instance
|
||||
2. **SessionParser.parseSessionFile()** - Now passes `this.projectScanner.getFileSystemProvider()` to `parseJsonlFile()`
|
||||
3. **SessionParser.parseSubagentFile()** - Now passes `this.projectScanner.getFileSystemProvider()` to `parseJsonlFile()`
|
||||
4. **SubagentResolver.parseSubagentFile()** - Now passes `this.projectScanner.getFileSystemProvider()` to `parseJsonlFile()`
|
||||
|
||||
Updated `SessionParser.test.ts` mock to include `getFileSystemProvider` method returning `LocalFileSystemProvider` instance.
|
||||
|
||||
**Commit:** `a3f5daf` - feat(01-01): thread FileSystemProvider through SessionParser and SubagentResolver
|
||||
|
||||
### Task 2: Refactor SubagentDetailBuilder to Use Provider
|
||||
|
||||
Removed hardcoded `fs/promises`, `os.homedir()`, and `fs.access()` calls from `SubagentDetailBuilder`, replacing them with provider-based operations:
|
||||
|
||||
1. **SubagentDetailBuilder.buildSubagentDetail()** - Added `fsProvider` and `projectsDir` parameters, removed dynamic imports of `fs/promises`, `os`, replaced `os.homedir()` with `projectsDir`, replaced `fs.access()` with `fsProvider.exists()`
|
||||
2. **ChunkBuilder.buildSubagentDetail()** - Added `fsProvider` and `projectsDir` parameters, passed through to `buildSubagentDetailFn()`
|
||||
3. **IPC subagents handler** - Updated `initializeSubagentHandlers()` to accept `ProjectScanner`, obtains `fsProvider` and `projectsDir` from scanner in `handleGetSubagentDetail()`
|
||||
4. **IPC handlers.ts** - Updated both `initializeIpcHandlers()` and `reinitializeServiceHandlers()` to pass `scanner` to `initializeSubagentHandlers()`
|
||||
|
||||
**Commit:** `c12b329` - feat(01-01): refactor SubagentDetailBuilder to use FileSystemProvider
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Provider Flow Architecture
|
||||
|
||||
```
|
||||
index.ts (creates ProjectScanner with SSH provider)
|
||||
└─> ProjectScanner.fsProvider (stored privately)
|
||||
└─> ProjectScanner.getFileSystemProvider() (public getter)
|
||||
├─> SessionParser.parseSessionFile() → parseJsonlFile(path, provider)
|
||||
├─> SessionParser.parseSubagentFile() → parseJsonlFile(path, provider)
|
||||
├─> SubagentResolver.parseSubagentFile() → parseJsonlFile(path, provider)
|
||||
└─> IPC subagents handler → ChunkBuilder → SubagentDetailBuilder
|
||||
├─> fsProvider.exists() (file check)
|
||||
└─> projectsDir (path construction)
|
||||
```
|
||||
|
||||
### Key Pattern Changes
|
||||
|
||||
**Before (Task 1):**
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath);
|
||||
// Silently falls back to LocalFileSystemProvider
|
||||
```
|
||||
|
||||
**After (Task 1):**
|
||||
```typescript
|
||||
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
||||
// Explicitly uses the provider from ProjectScanner
|
||||
```
|
||||
|
||||
**Before (Task 2):**
|
||||
```typescript
|
||||
const fs = await import('fs/promises');
|
||||
const os = await import('os');
|
||||
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
||||
await fs.access(subagentPath);
|
||||
// Always uses local filesystem, always assumes ~/.claude/projects
|
||||
```
|
||||
|
||||
**After (Task 2):**
|
||||
```typescript
|
||||
const subagentPath = path.join(projectsDir, projectId, 'subagents', `agent-${subagentId}.jsonl`);
|
||||
if (!(await fsProvider.exists(subagentPath))) { /* ... */ }
|
||||
// Uses injected provider and projectsDir, works for both local and SSH
|
||||
```
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
- **Type safety:** `pnpm typecheck` passes with zero errors
|
||||
- **Test suite:** All 494 tests pass (31 test files)
|
||||
- **No local filesystem leaks:** Verified zero matches for `fs/promises`, `os.homedir`, and bare `parseJsonlFile(filePath)` calls in SessionParser, SubagentResolver, and SubagentDetailBuilder
|
||||
- **Provider flow confirmed:** All three services now receive FileSystemProvider from ProjectScanner's getter
|
||||
|
||||
## Impact
|
||||
|
||||
### Immediate
|
||||
- SSH sessions will now load conversation history correctly (once SSH provider is fully wired in index.ts)
|
||||
- Subagent drill-down will work over SFTP
|
||||
- No more silent fallbacks to local filesystem in parsing stack
|
||||
|
||||
### Downstream
|
||||
- Phase 1 Plans 02-04 can now implement provider-based operations for todos, watchers, and search
|
||||
- All file operations in the parsing/analysis layer now use the provider abstraction consistently
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
### Created Files
|
||||
No new files created (all modifications to existing files).
|
||||
|
||||
### Modified Files Verified
|
||||
- [x] src/main/services/discovery/ProjectScanner.ts - Contains `getFileSystemProvider()` getter
|
||||
- [x] src/main/services/parsing/SessionParser.ts - Both `parseJsonlFile()` calls pass provider
|
||||
- [x] src/main/services/discovery/SubagentResolver.ts - `parseJsonlFile()` call passes provider
|
||||
- [x] src/main/services/analysis/SubagentDetailBuilder.ts - No `fs/promises` or `os.homedir`, uses `fsProvider.exists()`
|
||||
- [x] src/main/services/analysis/ChunkBuilder.ts - Passes `fsProvider` and `projectsDir` parameters
|
||||
- [x] src/main/ipc/subagents.ts - Obtains provider from `ProjectScanner`
|
||||
- [x] src/main/ipc/handlers.ts - Passes `scanner` to `initializeSubagentHandlers()`
|
||||
- [x] test/main/services/parsing/SessionParser.test.ts - Mock includes `getFileSystemProvider()`
|
||||
|
||||
### Commits Verified
|
||||
- [x] a3f5daf - Task 1 commit exists
|
||||
- [x] c12b329 - Task 2 commit exists
|
||||
|
||||
All files exist, all commits present. Self-check passed.
|
||||
350
.planning/phases/01-provider-plumbing/01-RESEARCH.md
Normal file
350
.planning/phases/01-provider-plumbing/01-RESEARCH.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# Phase 1: Provider Plumbing - Research
|
||||
|
||||
**Researched:** 2026-02-12
|
||||
**Domain:** Electron main process service architecture, filesystem abstraction patterns
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 1 requires threading `FileSystemProvider` through three critical parsing services (`SessionParser`, `SubagentResolver`, `SubagentDetailBuilder`) to enable SSH session loading. The current architecture already has the provider abstraction and SSH implementation in place, but the parsing stack hardcodes calls to `parseJsonlFile()` which defaults to local filesystem access. The fix is straightforward: pass `FileSystemProvider` through service constructors and method signatures to reach `parseJsonlFile()`, which already accepts an optional provider parameter.
|
||||
|
||||
The existing mode-switching architecture in `src/main/index.ts` (lines 96-136) provides the blueprint: when switching local↔SSH, services are recreated with the correct provider. However, Phase 1 only needs to ensure the provider threads through correctly; Phase 2 will handle multi-context infrastructure.
|
||||
|
||||
**Primary recommendation:** Add `FileSystemProvider` parameters to service constructors and method signatures following the existing `ProjectScanner` pattern, ensuring all `parseJsonlFile()` calls receive the correct provider instance.
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| ssh2 | (current) | SFTP implementation | Industry standard Node.js SSH/SFTP client, already used in `SshFileSystemProvider` |
|
||||
| Node.js stream | Built-in | Streaming JSONL parsing | Native API, optimal for large file processing |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| readline | Built-in | Line-by-line JSONL processing | Already used in `parseJsonlFile()` for streaming |
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| Constructor injection | Service locator pattern | Constructor injection is explicit and testable; avoid service locator for this scope |
|
||||
| Optional parameters | Required parameters | Keep optional for backward compatibility during refactor, but document the default |
|
||||
|
||||
**Installation:**
|
||||
No new dependencies required. Existing code has all necessary abstractions.
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
Current structure is correct:
|
||||
```
|
||||
src/main/
|
||||
├── services/
|
||||
│ ├── infrastructure/ # FileSystemProvider, LocalFileSystemProvider, SshFileSystemProvider
|
||||
│ ├── parsing/ # SessionParser (NEEDS provider threading)
|
||||
│ ├── discovery/ # SubagentResolver (NEEDS provider threading)
|
||||
│ └── analysis/ # SubagentDetailBuilder (NEEDS provider threading)
|
||||
├── utils/
|
||||
│ └── jsonl.ts # parseJsonlFile() already accepts provider parameter
|
||||
└── index.ts # Service instantiation, mode-switching handler
|
||||
```
|
||||
|
||||
### Pattern 1: Provider Injection via Constructor
|
||||
**What:** Pass `FileSystemProvider` to service constructors, store as instance field
|
||||
**When to use:** For services that make multiple filesystem calls (SessionParser, SubagentResolver)
|
||||
**Example:**
|
||||
```typescript
|
||||
// Source: Existing pattern in ProjectScanner (lines 78-89)
|
||||
export class SessionParser {
|
||||
private projectScanner: ProjectScanner;
|
||||
private fsProvider: FileSystemProvider; // ADD THIS
|
||||
|
||||
constructor(projectScanner: ProjectScanner, fsProvider?: FileSystemProvider) {
|
||||
this.projectScanner = projectScanner;
|
||||
// Default to local provider if not specified
|
||||
this.fsProvider = fsProvider ?? new LocalFileSystemProvider();
|
||||
}
|
||||
|
||||
async parseSessionFile(filePath: string): Promise<ParsedSession> {
|
||||
const messages = await parseJsonlFile(filePath, this.fsProvider); // PASS THROUGH
|
||||
return this.processMessages(messages);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Provider Access via Delegation
|
||||
**What:** Services that already have a `ProjectScanner` instance can get provider from it
|
||||
**When to use:** When refactoring would be minimal and avoids parameter bloat
|
||||
**Example:**
|
||||
```typescript
|
||||
// ProjectScanner already stores fsProvider (line 71)
|
||||
export class SessionParser {
|
||||
private projectScanner: ProjectScanner;
|
||||
|
||||
// Access provider through projectScanner
|
||||
async parseSessionFile(filePath: string): Promise<ParsedSession> {
|
||||
const provider = this.projectScanner.getFileSystemProvider();
|
||||
const messages = await parseJsonlFile(filePath, provider);
|
||||
return this.processMessages(messages);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Pattern 2 requires adding a `getFileSystemProvider()` getter to `ProjectScanner`, but reduces constructor parameter changes and keeps provider access centralized.
|
||||
|
||||
### Pattern 3: SubagentDetailBuilder Function Signature
|
||||
**What:** `buildSubagentDetail()` is a standalone function, not a class. Add provider parameter.
|
||||
**When to use:** For functional builders called directly by IPC handlers
|
||||
**Example:**
|
||||
```typescript
|
||||
// Current signature (line 39-46)
|
||||
export async function buildSubagentDetail(
|
||||
projectId: string,
|
||||
_sessionId: string,
|
||||
subagentId: string,
|
||||
sessionParser: SessionParser,
|
||||
subagentResolver: SubagentResolver,
|
||||
buildChunksFn: (messages: ParsedMessage[], subagents: Process[]) => EnhancedChunk[]
|
||||
): Promise<SubagentDetail | null>
|
||||
|
||||
// After Phase 1: add fsProvider parameter
|
||||
export async function buildSubagentDetail(
|
||||
projectId: string,
|
||||
_sessionId: string,
|
||||
subagentId: string,
|
||||
sessionParser: SessionParser,
|
||||
subagentResolver: SubagentResolver,
|
||||
buildChunksFn: (messages: ParsedMessage[], subagents: Process[]) => EnhancedChunk[],
|
||||
fsProvider: FileSystemProvider // ADD THIS
|
||||
): Promise<SubagentDetail | null>
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Importing `fs/promises` directly in services:** SubagentDetailBuilder (lines 48-62) bypasses provider abstraction by using direct `fs.access()`. Replace with `fsProvider.exists()`.
|
||||
- **Global default provider:** `jsonl.ts` line 36 creates `defaultProvider = new LocalFileSystemProvider()`. This is correct as a fallback, but services must explicitly pass their provider to avoid silent local fallback in SSH mode.
|
||||
- **Provider singleton:** Do not create a global provider instance. Each service context needs its own provider instance (critical for Phase 2).
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| SFTP read streams | Custom buffer management, chunk assembly | ssh2's `createReadStream()` piped to PassThrough | ssh2 handles SSH protocol details, reconnection, buffering; PassThrough ensures Node.js stream compatibility (see `SshFileSystemProvider.ts` lines 98-112) |
|
||||
| Filesystem abstraction testing | Mock entire `fs` module | Inject `FileSystemProvider` interface | Interface injection enables clean testing without rewiring Node.js internals |
|
||||
| Service lifecycle during mode switch | Manual cleanup tracking | Follow existing `handleModeSwitch()` pattern | Already proven pattern in `index.ts` lines 96-136: stop watchers, clear cache, recreate services |
|
||||
|
||||
**Key insight:** The provider abstraction already exists and works correctly (`FileSystemProvider`, `LocalFileSystemProvider`, `SshFileSystemProvider`). The problem is not the abstraction itself, but that some services bypass it. Don't rebuild the abstraction; just use it consistently.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Silent Fallback to Local Provider
|
||||
**What goes wrong:** `parseJsonlFile()` defaults to local provider when no provider is passed, causing SSH sessions to silently read local files (or fail with "file not found").
|
||||
**Why it happens:** Optional parameter with default value in `jsonl.ts` line 52: `fsProvider: FileSystemProvider = defaultProvider`
|
||||
**How to avoid:**
|
||||
- Always explicitly pass provider to `parseJsonlFile()` in all service methods
|
||||
- Add logging in development mode when default provider is used (helps catch missed call sites)
|
||||
**Warning signs:**
|
||||
- SSH mode shows "No conversation history" for sessions that definitely have messages
|
||||
- Console logs from `jsonl.ts` show ENOENT errors for paths that should exist remotely
|
||||
|
||||
### Pitfall 2: Hardcoded Path Construction
|
||||
**What goes wrong:** `SubagentDetailBuilder` constructs paths using `os.homedir()` + hardcoded `.claude/projects/` (lines 53-54), which always resolves to local filesystem even in SSH mode.
|
||||
**Why it happens:** Function was written before SSH support, assumes single filesystem
|
||||
**How to avoid:**
|
||||
- Use `ProjectScanner` path utilities (`buildSessionPath`, `buildSubagentsPath`) which already handle encoding
|
||||
- OR: Accept absolute file paths from callers who know the correct base directory
|
||||
**Warning signs:**
|
||||
- Subagent drill-down works locally but fails with "file not found" in SSH mode
|
||||
- Path logging shows local home directory when SSH mode is active
|
||||
|
||||
### Pitfall 3: Forgetting Nested Subagent Resolution
|
||||
**What goes wrong:** `SubagentDetailBuilder` calls `subagentResolver.resolveSubagents()` (line 68) which internally calls `parseJsonlFile()` multiple times. If `SubagentResolver.parseSubagentFile()` doesn't have the provider, nested subagents fail to load.
|
||||
**Why it happens:** Chain of calls: `buildSubagentDetail()` → `SubagentResolver.resolveSubagents()` → `SubagentResolver.parseSubagentFile()` → `parseJsonlFile()`. Provider must thread through entire chain.
|
||||
**How to avoid:**
|
||||
- Test subagent drill-down specifically in SSH mode (not just session loading)
|
||||
- Ensure `SubagentResolver` stores provider as instance field and uses it in `parseSubagentFile()`
|
||||
**Warning signs:**
|
||||
- Main session messages load correctly but subagent cards show "Failed to load" or empty state
|
||||
- Subagent drill-down modal opens but shows no content
|
||||
|
||||
### Pitfall 4: Test Mocking Inconsistency
|
||||
**What goes wrong:** Tests mock `ProjectScanner` but don't provide a `getFileSystemProvider()` method, causing tests to fail after refactoring.
|
||||
**Why it happens:** Test mocks in `SessionParser.test.ts` (lines 24-33) are partial mocks missing new methods
|
||||
**How to avoid:**
|
||||
- Update test mocks to include `getFileSystemProvider()` returning a `LocalFileSystemProvider` instance
|
||||
- Consider creating a `MockFileSystemProvider` test helper for consistent mocking
|
||||
**Warning signs:**
|
||||
- Tests pass before refactor, fail with "getFileSystemProvider is not a function" after
|
||||
- Tests fail even though manual testing works
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from existing codebase:
|
||||
|
||||
### Example 1: Service Initialization with Provider (from main/index.ts)
|
||||
```typescript
|
||||
// Source: src/main/index.ts lines 111-113
|
||||
const provider = sshConnectionManager.getProvider();
|
||||
const projectsDir = mode === 'ssh'
|
||||
? (sshConnectionManager.getRemoteProjectsPath() ?? undefined)
|
||||
: undefined;
|
||||
|
||||
projectScanner = new ProjectScanner(projectsDir, undefined, provider);
|
||||
sessionParser = new SessionParser(projectScanner);
|
||||
subagentResolver = new SubagentResolver(projectScanner);
|
||||
```
|
||||
|
||||
### Example 2: Existing Provider Pattern in ProjectScanner
|
||||
```typescript
|
||||
// Source: src/main/services/discovery/ProjectScanner.ts lines 78-89
|
||||
constructor(projectsDir?: string, todosDir?: string, fsProvider?: FileSystemProvider) {
|
||||
this.projectsDir = projectsDir ?? getProjectsBasePath();
|
||||
this.todosDir = todosDir ?? getTodosBasePath();
|
||||
this.fsProvider = fsProvider ?? new LocalFileSystemProvider();
|
||||
|
||||
// Initialize delegated services WITH PROVIDER
|
||||
this.sessionContentFilter = SessionContentFilter;
|
||||
this.worktreeGrouper = new WorktreeGrouper(this.projectsDir, this.fsProvider);
|
||||
this.subagentLocator = new SubagentLocator(this.projectsDir, this.fsProvider);
|
||||
this.sessionSearcher = new SessionSearcher(this.projectsDir, this.fsProvider);
|
||||
this.projectPathResolver = new ProjectPathResolver(this.projectsDir, this.fsProvider);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: parseJsonlFile with Provider Parameter
|
||||
```typescript
|
||||
// Source: src/main/utils/jsonl.ts lines 50-64
|
||||
export async function parseJsonlFile(
|
||||
filePath: string,
|
||||
fsProvider: FileSystemProvider = defaultProvider // OPTIONAL with default
|
||||
): Promise<ParsedMessage[]> {
|
||||
const messages: ParsedMessage[] = [];
|
||||
|
||||
if (!(await fsProvider.exists(filePath))) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' });
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const parsed = parseJsonlLine(line);
|
||||
if (parsed) {
|
||||
messages.push(parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error parsing line in ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Mode Switch Handler (Service Recreation Pattern)
|
||||
```typescript
|
||||
// Source: src/main/index.ts lines 96-136
|
||||
const handleModeSwitch = async (mode: 'local' | 'ssh'): Promise<void> => {
|
||||
logger.info(`Switching to ${mode} mode`);
|
||||
|
||||
// 1. Stop watchers
|
||||
fileWatcher.stop();
|
||||
|
||||
// 2. Clear cache
|
||||
dataCache.clear();
|
||||
|
||||
// 3. Get provider from connection manager
|
||||
const provider = sshConnectionManager.getProvider();
|
||||
const projectsDir = mode === 'ssh'
|
||||
? (sshConnectionManager.getRemoteProjectsPath() ?? undefined)
|
||||
: undefined;
|
||||
|
||||
// 4. Recreate services with new provider
|
||||
projectScanner = new ProjectScanner(projectsDir, undefined, provider);
|
||||
sessionParser = new SessionParser(projectScanner);
|
||||
subagentResolver = new SubagentResolver(projectScanner);
|
||||
|
||||
// 5. Re-initialize IPC handlers with new instances
|
||||
reinitializeServiceHandlers(
|
||||
projectScanner,
|
||||
sessionParser,
|
||||
subagentResolver,
|
||||
chunkBuilder,
|
||||
dataCache
|
||||
);
|
||||
|
||||
// 6. Update file watcher provider
|
||||
fileWatcher.setFileSystemProvider(provider);
|
||||
|
||||
// 7. Restart watcher
|
||||
fileWatcher.start();
|
||||
|
||||
// 8. Notify renderer
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus());
|
||||
}
|
||||
|
||||
logger.info(`Mode switch to ${mode} complete`);
|
||||
};
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Direct `fs` imports throughout services | `FileSystemProvider` abstraction | Added with SSH support (recent) | Enables remote file access, but not all services use it yet |
|
||||
| Single global service instances | Mode-switching service recreation | Added with SSH support (recent) | Enables local↔SSH switching, but destroys local state (Phase 2 will fix) |
|
||||
| Synchronous file reads | Streaming with readline | Original architecture | Efficient for large JSONL files, correct pattern |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- Direct `fs/promises` imports in services: `SubagentDetailBuilder` still uses this (lines 48-50). Should be replaced with provider calls.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should ProjectScanner expose `getFileSystemProvider()` or should services store their own reference?**
|
||||
- What we know: ProjectScanner already stores `fsProvider` (line 71). SessionParser and SubagentResolver both receive ProjectScanner in constructor.
|
||||
- What's unclear: Whether delegating through ProjectScanner is cleaner than duplicating the reference in each service.
|
||||
- Recommendation: Add `getFileSystemProvider()` getter to ProjectScanner. Reduces constructor changes, centralizes provider access, matches existing delegation pattern (line 84 uses SessionContentFilter, lines 85-88 pass provider to delegated services).
|
||||
|
||||
2. **Should SubagentDetailBuilder become a class or stay a function?**
|
||||
- What we know: It's currently a standalone function (line 39). IPC handler calls it directly. It needs access to `fsProvider`, `projectsDir`, and multiple service instances.
|
||||
- What's unclear: Whether the complexity warrants a class or if adding parameters is sufficient.
|
||||
- Recommendation: Keep as function for Phase 1, add `fsProvider` parameter. Phase 2 (ServiceContextRegistry) may benefit from a builder class, but don't pre-optimize.
|
||||
|
||||
3. **Do tests need a dedicated MockFileSystemProvider or just mock existing providers?**
|
||||
- What we know: Tests currently mock ProjectScanner (test/main/services/parsing/SessionParser.test.ts lines 24-33). LocalFileSystemProvider is a real implementation.
|
||||
- What's unclear: Whether tests should use real LocalFileSystemProvider with temp files, or mock it.
|
||||
- Recommendation: Use real LocalFileSystemProvider for integration-style tests (current pattern). Mock only for unit tests that don't need real file I/O. Create `MockFileSystemProvider` helper if tests start duplicating mock setup.
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Existing codebase files:
|
||||
- `src/main/services/infrastructure/FileSystemProvider.ts` - Interface definition
|
||||
- `src/main/services/infrastructure/SshFileSystemProvider.ts` - SSH implementation
|
||||
- `src/main/services/discovery/ProjectScanner.ts` - Provider injection pattern
|
||||
- `src/main/utils/jsonl.ts` - parseJsonlFile provider parameter
|
||||
- `src/main/index.ts` - Mode-switching handler
|
||||
- `.planning/ROADMAP.md` - Phase 1 requirements and success criteria
|
||||
- `.planning/REQUIREMENTS.md` - PROV-01, PROV-02 detailed requirements
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- None required; all information sourced from codebase inspection
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- None
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH - No external libraries needed, existing abstractions are complete
|
||||
- Architecture: HIGH - Patterns already proven in ProjectScanner and delegated services
|
||||
- Pitfalls: HIGH - All identified from actual code inspection and known failure modes
|
||||
|
||||
**Research date:** 2026-02-12
|
||||
**Valid until:** 30 days (stable architecture, no fast-moving dependencies)
|
||||
580
.planning/research/ARCHITECTURE.md
Normal file
580
.planning/research/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
# Architecture Research: Multi-Context Workspace System
|
||||
|
||||
**Domain:** Electron application with multi-context workspace switching
|
||||
**Researched:** 2026-02-12
|
||||
**Confidence:** MEDIUM
|
||||
|
||||
## Standard Architecture
|
||||
|
||||
### System Overview
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ Main Process (Node.js) │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ServiceContextRegistry (NEW) │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ LocalContext │ │ SshContext │ │ SshContext │ │ │
|
||||
│ │ │ (always) │ │ (Host A) │ │ (Host B) │ │ │
|
||||
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ ┌────▼──────────────────▼──────────────────▼───────┐ │ │
|
||||
│ │ │ Services (per context) │ │ │
|
||||
│ │ │ - ProjectScanner │ │ │
|
||||
│ │ │ - SessionParser │ │ │
|
||||
│ │ │ - SubagentResolver │ │ │
|
||||
│ │ │ - ChunkBuilder (shared) │ │ │
|
||||
│ │ │ - DataCache (per context) │ │ │
|
||||
│ │ │ - FileWatcher (per context) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────▼───────────────────────────────┐ │
|
||||
│ │ IPC Bridge (via preload) │ │
|
||||
│ │ - getCurrentContext() │ │
|
||||
│ │ - switchContext(contextId) │ │
|
||||
│ │ - getContextSnapshot(contextId) │ │
|
||||
│ │ - listContexts() │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────┬────────────────────────────────────┘
|
||||
│ IPC
|
||||
┌──────────────────────────────────▼────────────────────────────────────┐
|
||||
│ Renderer Process (Chromium) │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ContextSwitcher (NEW) │ │
|
||||
│ │ - Manages active context ID │ │
|
||||
│ │ - Coordinates switch flow │ │
|
||||
│ │ - Updates connection slice │ │
|
||||
│ └───────────────────────────────┬────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────▼────────────────────────────────┐ │
|
||||
│ │ Zustand Store (with snapshots) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Current State (active context) │ │ │
|
||||
│ │ │ - projects, sessions, selectedProjectId, etc. │ │ │
|
||||
│ │ └────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ contextSnapshots: Map<contextId, StateSnapshot> │ │ │
|
||||
│ │ │ - Stores full state per context for instant restore │ │ │
|
||||
│ │ └────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────▼────────────────────────────────┐ │
|
||||
│ │ React Components │ │
|
||||
│ │ - ContextSwitcher UI (dropdown/sidebar) │ │
|
||||
│ │ - Dashboard (context-aware) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
| Component | Responsibility | Typical Implementation |
|
||||
|-----------|----------------|------------------------|
|
||||
| **ServiceContextRegistry** | Manages multiple service contexts (local + N SSH), lifecycle, and active context switching | Map of contextId → ServiceContext objects, provides getActive(), switch(), register() |
|
||||
| **ServiceContext** | Encapsulates service instances and FileSystemProvider for one context | Holds ProjectScanner, SessionParser, SubagentResolver, DataCache, FileWatcher, fsProvider |
|
||||
| **ContextSwitcher** (renderer) | Orchestrates context switches from renderer side, manages UI state | Calls IPC to switch, captures/restores snapshots, updates Zustand |
|
||||
| **StateSnapshot** | Frozen copy of renderer state for a context | Full or partial state (projects, sessions, selections, UI state) |
|
||||
| **IPC Context Handlers** | Exposes context management to renderer | getCurrentContext, switchContext, getContextSnapshot, listContexts |
|
||||
|
||||
## Recommended Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── services/
|
||||
│ │ ├── infrastructure/
|
||||
│ │ │ ├── ServiceContext.ts # NEW: Encapsulates services for one context
|
||||
│ │ │ ├── ServiceContextRegistry.ts # NEW: Manages all contexts
|
||||
│ │ │ └── ContextLifecycleManager.ts # NEW: Start/stop context services
|
||||
│ │ └── ... (existing services)
|
||||
│ ├── ipc/
|
||||
│ │ └── context.ts # NEW: Context switching IPC handlers
|
||||
│ └── index.ts # Modified: Initialize registry
|
||||
├── renderer/
|
||||
│ ├── store/
|
||||
│ │ ├── slices/
|
||||
│ │ │ ├── contextSlice.ts # NEW: Context management state
|
||||
│ │ │ └── connectionSlice.ts # Modified: Works with contextSlice
|
||||
│ │ └── utils/
|
||||
│ │ └── stateSnapshot.ts # NEW: Snapshot capture/restore
|
||||
│ ├── components/
|
||||
│ │ └── common/
|
||||
│ │ └── ContextSwitcher.tsx # NEW: Context switcher UI
|
||||
│ └── hooks/
|
||||
│ └── useContextSwitch.ts # NEW: Hook for switching contexts
|
||||
└── shared/
|
||||
└── types/
|
||||
└── context.ts # NEW: Context-related types
|
||||
```
|
||||
|
||||
### Structure Rationale
|
||||
|
||||
- **ServiceContextRegistry** in infrastructure: Central registry pattern, manages context lifecycle
|
||||
- **ServiceContext** wraps all service instances: Clean isolation boundary, easy to create/destroy
|
||||
- **ContextSlice** separate from connectionSlice: Context is broader than SSH (could add Docker, WSL, etc. later)
|
||||
- **State snapshots** in store utils: Serialize/deserialize state for instant restore
|
||||
- **IPC context handlers** in dedicated file: Clear separation of concerns from existing SSH handlers
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Service Context Registry
|
||||
|
||||
**What:** Central registry that manages multiple isolated service contexts, each with its own FileSystemProvider and service instances.
|
||||
|
||||
**When to use:** When you need to support multiple data sources (local, SSH hosts, containers) without tearing down/recreating all services on every switch.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pros:**
|
||||
- Local context stays alive (critical for notifications, config)
|
||||
- Instant switching between known contexts
|
||||
- Clear isolation boundaries
|
||||
- Easy to add new context types (Docker, WSL, etc.)
|
||||
- **Cons:**
|
||||
- Memory overhead (multiple service sets in memory)
|
||||
- Complexity of managing context lifecycle
|
||||
- Need to handle cross-context data requests carefully
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// ServiceContext.ts
|
||||
export interface ServiceContext {
|
||||
id: string;
|
||||
type: 'local' | 'ssh';
|
||||
label: string; // "Local" or "user@hostname"
|
||||
|
||||
// Service instances
|
||||
projectScanner: ProjectScanner;
|
||||
sessionParser: SessionParser;
|
||||
subagentResolver: SubagentResolver;
|
||||
dataCache: DataCache;
|
||||
fileWatcher: FileWatcher;
|
||||
|
||||
// Provider
|
||||
fsProvider: FileSystemProvider;
|
||||
|
||||
// Lifecycle
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
lastAccessedAt: Date;
|
||||
}
|
||||
|
||||
// ServiceContextRegistry.ts
|
||||
export class ServiceContextRegistry {
|
||||
private contexts = new Map<string, ServiceContext>();
|
||||
private activeContextId: string = 'local';
|
||||
|
||||
constructor() {
|
||||
// Always initialize local context
|
||||
this.registerLocalContext();
|
||||
}
|
||||
|
||||
register(context: ServiceContext): void {
|
||||
this.contexts.set(context.id, context);
|
||||
}
|
||||
|
||||
getActive(): ServiceContext {
|
||||
return this.contexts.get(this.activeContextId)!;
|
||||
}
|
||||
|
||||
async switch(contextId: string): Promise<ServiceContext> {
|
||||
const context = this.contexts.get(contextId);
|
||||
if (!context) throw new Error(`Context ${contextId} not found`);
|
||||
|
||||
// Pause current context's watchers
|
||||
const current = this.getActive();
|
||||
current.fileWatcher.stop();
|
||||
current.isActive = false;
|
||||
|
||||
// Activate new context
|
||||
context.isActive = true;
|
||||
context.lastAccessedAt = new Date();
|
||||
context.fileWatcher.start();
|
||||
this.activeContextId = contextId;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
list(): ServiceContext[] {
|
||||
return Array.from(this.contexts.values());
|
||||
}
|
||||
|
||||
async createSshContext(
|
||||
host: string,
|
||||
sshManager: SshConnectionManager
|
||||
): Promise<ServiceContext> {
|
||||
// Create services with SSH provider
|
||||
const provider = sshManager.getProvider();
|
||||
const projectsDir = sshManager.getRemoteProjectsPath()!;
|
||||
|
||||
const context: ServiceContext = {
|
||||
id: `ssh:${host}`,
|
||||
type: 'ssh',
|
||||
label: host,
|
||||
projectScanner: new ProjectScanner(projectsDir, undefined, provider),
|
||||
sessionParser: new SessionParser(/* ... */),
|
||||
subagentResolver: new SubagentResolver(/* ... */),
|
||||
dataCache: new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES),
|
||||
fileWatcher: new FileWatcher(/* ... */),
|
||||
fsProvider: provider,
|
||||
isActive: false,
|
||||
createdAt: new Date(),
|
||||
lastAccessedAt: new Date(),
|
||||
};
|
||||
|
||||
this.register(context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: State Snapshot with Instant Restore
|
||||
|
||||
**What:** Capture full renderer state for each context and restore it instantly on switch, avoiding re-fetching from main process.
|
||||
|
||||
**When to use:** When you need instant (<50ms) context switching and want to preserve user's navigation/selection state per context.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pros:**
|
||||
- Instant perceived switching (no loading states)
|
||||
- Preserves user's place in each context (selected project, open tabs, scroll position)
|
||||
- Reduces IPC round-trips
|
||||
- **Cons:**
|
||||
- Memory overhead in renderer (full state × N contexts)
|
||||
- Snapshot can become stale (need expiration/refresh strategy)
|
||||
- Need to handle snapshot compatibility across app versions
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// stateSnapshot.ts
|
||||
export interface StateSnapshot {
|
||||
contextId: string;
|
||||
capturedAt: Date;
|
||||
expiresAt: Date; // Auto-refresh if older than 5 minutes
|
||||
|
||||
// Core data
|
||||
projects: Project[];
|
||||
sessions: Session[];
|
||||
repositoryGroups: RepositoryGroup[];
|
||||
|
||||
// Selections
|
||||
selectedProjectId: string | null;
|
||||
selectedSessionId: string | null;
|
||||
|
||||
// UI state
|
||||
tabs: Tab[];
|
||||
activeTabId: string | null;
|
||||
paneLayout: PaneLayout;
|
||||
|
||||
// Metadata
|
||||
version: string; // App version for compatibility check
|
||||
}
|
||||
|
||||
export function captureSnapshot(state: AppState, contextId: string): StateSnapshot {
|
||||
return {
|
||||
contextId,
|
||||
capturedAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 min
|
||||
projects: state.projects,
|
||||
sessions: state.sessions,
|
||||
repositoryGroups: state.repositoryGroups,
|
||||
selectedProjectId: state.selectedProjectId,
|
||||
selectedSessionId: state.selectedSessionId,
|
||||
tabs: state.tabs,
|
||||
activeTabId: state.activeTabId,
|
||||
paneLayout: state.paneLayout,
|
||||
version: state.appVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreSnapshot(snapshot: StateSnapshot): Partial<AppState> {
|
||||
// Check if snapshot is stale
|
||||
if (new Date() > snapshot.expiresAt) {
|
||||
// Return only UI state, let data re-fetch
|
||||
return {
|
||||
tabs: snapshot.tabs,
|
||||
activeTabId: snapshot.activeTabId,
|
||||
paneLayout: snapshot.paneLayout,
|
||||
};
|
||||
}
|
||||
|
||||
// Restore full state
|
||||
return {
|
||||
projects: snapshot.projects,
|
||||
sessions: snapshot.sessions,
|
||||
repositoryGroups: snapshot.repositoryGroups,
|
||||
selectedProjectId: snapshot.selectedProjectId,
|
||||
selectedSessionId: snapshot.selectedSessionId,
|
||||
tabs: snapshot.tabs,
|
||||
activeTabId: snapshot.activeTabId,
|
||||
paneLayout: snapshot.paneLayout,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: IPC Handler Re-Routing
|
||||
|
||||
**What:** IPC handlers always query the active context from registry instead of using module-level service variables.
|
||||
|
||||
**When to use:** When you need IPC handlers to automatically target the active context without manual re-initialization on every switch.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pros:**
|
||||
- No need for `reinitializeServiceHandlers()` on every switch
|
||||
- Handlers automatically use correct context
|
||||
- Less code to maintain
|
||||
- **Cons:**
|
||||
- Need to pass registry to all handler initializers
|
||||
- Small performance cost (registry lookup on every IPC call)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// ipc/projects.ts (modified)
|
||||
let registry: ServiceContextRegistry;
|
||||
|
||||
export function initializeProjectHandlers(reg: ServiceContextRegistry): void {
|
||||
registry = reg;
|
||||
}
|
||||
|
||||
export function registerProjectHandlers(ipcMain: IpcMain): void {
|
||||
ipcMain.handle(GET_PROJECTS, async () => {
|
||||
try {
|
||||
// Always use active context
|
||||
const context = registry.getActive();
|
||||
const projects = await context.projectScanner.scan();
|
||||
return projects;
|
||||
} catch (err) {
|
||||
logger.error('Failed to get projects:', err);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Context Switch Flow
|
||||
|
||||
```
|
||||
User clicks context in UI
|
||||
↓
|
||||
[Renderer: ContextSwitcher]
|
||||
↓ (1) Capture current state
|
||||
[Renderer: captureSnapshot(currentContextId)]
|
||||
↓ (2) Store in contextSnapshots map
|
||||
[Renderer: contextSnapshots.set(currentContextId, snapshot)]
|
||||
↓ (3) Call IPC to switch context
|
||||
[IPC: switchContext(newContextId)]
|
||||
↓ (4) Switch active context in registry
|
||||
[Main: ServiceContextRegistry.switch(newContextId)]
|
||||
│
|
||||
├── Stop current context's FileWatcher
|
||||
├── Mark current context inactive
|
||||
├── Activate new context
|
||||
└── Start new context's FileWatcher
|
||||
↓ (5) Return new context metadata
|
||||
[IPC Response: { contextId, type, label }]
|
||||
↓ (6) Check for existing snapshot
|
||||
[Renderer: contextSnapshots.get(newContextId)]
|
||||
│
|
||||
├─── Snapshot exists? ──────────┐
|
||||
│ │
|
||||
│ (instant restore)
|
||||
│ ↓
|
||||
│ [restoreSnapshot(snapshot)]
|
||||
│ ↓
|
||||
│ [UI updates immediately]
|
||||
│
|
||||
└─── No snapshot? ──────────────┐
|
||||
│
|
||||
(fetch fresh)
|
||||
↓
|
||||
[fetchProjects(), fetchRepositoryGroups()]
|
||||
↓
|
||||
[Show loading states]
|
||||
↓
|
||||
[UI updates when data arrives]
|
||||
```
|
||||
|
||||
### Key Data Flows
|
||||
|
||||
1. **Context registration (SSH):** User connects → SshConnectionManager.connect() → ServiceContextRegistry.createSshContext() → Context registered
|
||||
2. **Active context query:** IPC handler → registry.getActive() → ServiceContext → service.method()
|
||||
3. **Context list update:** Registry change → main sends IPC event → renderer updates context list UI
|
||||
4. **Snapshot refresh:** Context switch + stale snapshot → partial restore → background re-fetch → update snapshot
|
||||
|
||||
## Scaling Considerations
|
||||
|
||||
| Scale | Architecture Adjustments |
|
||||
|-------|--------------------------|
|
||||
| 1-3 contexts | Simple Map-based registry, full state snapshots, no eviction |
|
||||
| 4-10 contexts | Add LRU eviction (keep 3 most recent contexts), lazy service initialization |
|
||||
| 10+ contexts | Move to connection pool pattern, on-demand context creation, aggressive cache eviction |
|
||||
|
||||
### Scaling Priorities
|
||||
|
||||
1. **First bottleneck:** Memory usage from multiple DataCache instances. **Fix:** Share ChunkBuilder, only keep DataCache per-context for active requests.
|
||||
2. **Second bottleneck:** FileWatcher overhead. **Fix:** Only watch active context + local (for notifications). Pause watchers on inactive contexts.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Anti-Pattern 1: Destroying Local Context on SSH Connect
|
||||
|
||||
**What people do:** Call `disconnect()` on local services when connecting to SSH, assuming exclusive mode.
|
||||
|
||||
**Why it's wrong:** Notifications, config updates, and local file watching should continue running even when viewing remote data. User may want to quickly check local sessions without full reconnect.
|
||||
|
||||
**Do this instead:** Keep local context always alive. Add it to registry at startup with id="local". SSH contexts are additive, not replacements.
|
||||
|
||||
### Anti-Pattern 2: Re-Initializing All Services on Every Switch
|
||||
|
||||
**What people do:** Call `initializeServices()` and `reinitializeServiceHandlers()` on every context switch, recreating everything.
|
||||
|
||||
**Why it's wrong:** Expensive (2-3 second delay), destroys caches, resets watchers, loses in-flight operations. Causes UI flicker and poor UX.
|
||||
|
||||
**Do this instead:** Use ServiceContextRegistry to maintain multiple contexts. Switch by updating `activeContextId` pointer. Services stay alive and warm.
|
||||
|
||||
### Anti-Pattern 3: Blocking UI on Context Switch
|
||||
|
||||
**What people do:** Show full-screen loading spinner, disable all controls, wait for all data to re-fetch before showing any UI.
|
||||
|
||||
**Why it's wrong:** Context switch feels slow (500ms+ perceived latency). User loses sense of continuity. Can't cancel or go back.
|
||||
|
||||
**Do this instead:** Use optimistic state snapshots. Restore snapshot immediately (<50ms), show UI instantly, refresh data in background. Show subtle loading indicators only for stale data.
|
||||
|
||||
### Anti-Pattern 4: Sharing DataCache Across Contexts
|
||||
|
||||
**What people do:** Use a single DataCache for all contexts, keyed by `${contextId}:${projectId}:${sessionId}`.
|
||||
|
||||
**Why it's wrong:** Cache keys collide if same project path exists on multiple hosts. Eviction strategy becomes complex. Memory usage unbounded.
|
||||
|
||||
**Do this instead:** Each ServiceContext has its own DataCache. When context becomes inactive, optionally clear its cache to free memory (LRU policy).
|
||||
|
||||
### Anti-Pattern 5: No Context Metadata in IPC Responses
|
||||
|
||||
**What people do:** IPC handlers return raw data (projects, sessions) without indicating which context it came from.
|
||||
|
||||
**Why it's wrong:** Renderer can't detect stale responses from previous context. If user switches quickly A→B→A, response from first A might arrive after B response, causing wrong data to display.
|
||||
|
||||
**Do this instead:** Every IPC response includes `contextId` field. Renderer checks if response matches current active context before applying to state. Discard stale responses.
|
||||
|
||||
## Integration Points
|
||||
|
||||
### External Services
|
||||
|
||||
| Service | Integration Pattern | Notes |
|
||||
|---------|---------------------|-------|
|
||||
| SshConnectionManager | Wrap in ServiceContext | Create ServiceContext after successful connect, register with registry |
|
||||
| FileWatcher | Per-context instance | Only active context's watcher runs. Start/stop on switch. |
|
||||
| NotificationManager | Singleton, local only | Always uses local FileSystemProvider. Notifications are local-only feature. |
|
||||
| ConfigManager | Singleton, local only | Settings stored locally. Applies across all contexts. |
|
||||
|
||||
### Internal Boundaries
|
||||
|
||||
| Boundary | Communication | Notes |
|
||||
|----------|---------------|-------|
|
||||
| Registry ↔ IPC Handlers | Direct method calls | Handlers call `registry.getActive().service.method()` |
|
||||
| Main ↔ Renderer | IPC events + responses | Main sends `context-list-updated` event when contexts change |
|
||||
| ServiceContext ↔ Services | Constructor injection | Pass fsProvider, projectsDir to service constructors |
|
||||
| ContextSwitcher ↔ Store | Zustand actions | Call `switchContext(id)` action, store handles snapshot logic |
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Main Process Architecture (Foundation)
|
||||
**Goal:** Establish ServiceContext and ServiceContextRegistry
|
||||
|
||||
**Components to build:**
|
||||
1. `ServiceContext.ts` - Interface and factory function
|
||||
2. `ServiceContextRegistry.ts` - Registry with register/switch/getActive
|
||||
3. Modify `index.ts` - Initialize registry instead of individual services
|
||||
4. Update `ipc/handlers.ts` - Pass registry to all handlers
|
||||
|
||||
**Why first:** Main process architecture must be stable before renderer changes. This phase has no user-facing changes.
|
||||
|
||||
**Build order:** ServiceContext → ServiceContextRegistry → Modify index.ts → Update IPC handlers
|
||||
|
||||
### Phase 2: IPC Context API (Bridge)
|
||||
**Goal:** Expose context operations to renderer
|
||||
|
||||
**Components to build:**
|
||||
1. `ipc/context.ts` - Context IPC handlers (getCurrentContext, switchContext, listContexts)
|
||||
2. `preload/index.ts` - Expose context API methods
|
||||
3. Add IPC channel constants in `preload/constants/ipcChannels.ts`
|
||||
|
||||
**Why second:** Bridge must exist before renderer can consume it. Testable from Node.js before building UI.
|
||||
|
||||
**Build order:** IPC handlers → Preload API → Test with node REPL
|
||||
|
||||
### Phase 3: Renderer State Management (State)
|
||||
**Goal:** Add context slice and snapshot system
|
||||
|
||||
**Components to build:**
|
||||
1. `store/slices/contextSlice.ts` - Context state, actions, snapshot storage
|
||||
2. `store/utils/stateSnapshot.ts` - Snapshot capture/restore functions
|
||||
3. Modify `store/slices/connectionSlice.ts` - Delegate to contextSlice for SSH mode
|
||||
|
||||
**Why third:** State layer must exist before UI components can trigger switches.
|
||||
|
||||
**Build order:** stateSnapshot utils → contextSlice → Update connectionSlice → Test actions in console
|
||||
|
||||
### Phase 4: UI Integration (User-Facing)
|
||||
**Goal:** Add context switcher UI
|
||||
|
||||
**Components to build:**
|
||||
1. `components/common/ContextSwitcher.tsx` - Dropdown or sidebar UI
|
||||
2. `hooks/useContextSwitch.ts` - Hook for switching with loading states
|
||||
3. Update `Dashboard.tsx` - Show current context, integrate switcher
|
||||
|
||||
**Why last:** UI is the final layer. Depends on all previous phases.
|
||||
|
||||
**Build order:** useContextSwitch hook → ContextSwitcher UI → Integrate in Dashboard
|
||||
|
||||
## Build Dependencies
|
||||
|
||||
```
|
||||
Phase 1 (Main)
|
||||
↓ (ServiceContextRegistry needed by IPC)
|
||||
Phase 2 (IPC Bridge)
|
||||
↓ (IPC API needed by store)
|
||||
Phase 3 (Renderer State)
|
||||
↓ (Store actions needed by UI)
|
||||
Phase 4 (UI)
|
||||
```
|
||||
|
||||
**Critical path:** ServiceContext → ServiceContextRegistry → IPC context handlers → contextSlice → ContextSwitcher UI
|
||||
|
||||
**Parallelizable:**
|
||||
- Phase 1 and Phase 3 (main vs renderer) can be worked on by different developers
|
||||
- stateSnapshot utils can be built before contextSlice is finalized
|
||||
- UI components can be mocked with fake data while state layer is being built
|
||||
|
||||
## Sources
|
||||
|
||||
**Architecture Patterns:**
|
||||
- [Electron Process Model](https://www.electronjs.org/docs/latest/tutorial/process-model) - Official Electron multi-process architecture
|
||||
- [Electron Inter-Process Communication](https://www.electronjs.org/docs/latest/tutorial/ipc) - IPC patterns for context coordination
|
||||
- [Advanced Electron.js architecture - LogRocket Blog](https://blog.logrocket.com/advanced-electron-js-architecture/) - Advanced patterns for Electron apps
|
||||
- [Building Multi-Screen Electron Applications - CorticalFlow](https://corticalflow.com/en/blog/building-multi-screen-electron-apps) - Cognitive workflow optimization with multi-context apps
|
||||
|
||||
**State Management:**
|
||||
- [Syncing State between Electron Contexts - Bruno Scheufler](https://brunoscheufler.com/blog/2023-10-29-syncing-state-between-electron-contexts) - State synchronization patterns
|
||||
- [Zutron - GitHub](https://github.com/goosewobbler/zutron) - Zustand for Electron, main-renderer sync
|
||||
- [Creating a synchronized store between main and renderer - BigBinary](https://www.bigbinary.com/blog/sync-store-main-renderer-electron) - Store sync techniques
|
||||
|
||||
**Service Registry & DI:**
|
||||
- [tsyringe - GitHub](https://github.com/microsoft/tsyringe) - Microsoft's TypeScript DI container
|
||||
- [node-dependency-injection - npm](https://www.npmjs.com/package/node-dependency-injection) - DI for Node.js
|
||||
- [Dependency Injection in NodeJS TypeScript - Lodely](https://www.lodely.com/blog/dependency-injection-in-nodejs-typescript) - DI patterns for Node.js/TypeScript
|
||||
- [Top 5 TypeScript dependency injection containers - LogRocket](https://blog.logrocket.com/top-five-typescript-dependency-injection-containers/) - Comparison of DI libraries
|
||||
|
||||
**Context Patterns:**
|
||||
- [ServiceTalk Asynchronous Context](https://apple.github.io/servicetalk/servicetalk-concurrent-api/SNAPSHOT/async-context.html) - Context isolation in async systems
|
||||
- [Provider Pattern with React Context API - Flexiple](https://flexiple.com/react/provider-pattern-with-react-context-api) - Provider patterns for context management
|
||||
|
||||
---
|
||||
*Architecture research for: Multi-context workspace switching in claude-devtools*
|
||||
*Researched: 2026-02-12*
|
||||
512
.planning/research/FEATURES.md
Normal file
512
.planning/research/FEATURES.md
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
# Feature Research: Multi-Context Workspace Switching
|
||||
|
||||
**Domain:** Desktop application with multi-context/workspace switching
|
||||
**Researched:** 2026-02-12
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Multi-context desktop applications require three fundamental pillars: **instant switching**, **comprehensive state preservation**, and **clear status communication**. Research across VS Code Remote, JetBrains Gateway, Slack, Notion, Discord, and Figma reveals that users expect workspace switching to feel instantaneous with zero cognitive load, complete context preservation across switches, and continuous awareness of connection status. The line between table stakes and differentiators is clear: users will leave apps with clunky switching UX but deeply value innovations that reduce mental overhead during context transitions.
|
||||
|
||||
## Feature Landscape
|
||||
|
||||
### Table Stakes (Users Expect These)
|
||||
|
||||
Features users assume exist. Missing these = product feels incomplete.
|
||||
|
||||
| Feature | Why Expected | Complexity | Dependencies | Notes |
|
||||
|---------|--------------|------------|--------------|-------|
|
||||
| **Visual workspace list** | Every multi-context app shows available workspaces | LOW | None | Dropdown, sidebar, or command palette |
|
||||
| **Keyboard shortcuts for switching** | Power users demand keyboard-driven workflows | LOW | Visual workspace list | Ctrl/Cmd+Number or dedicated switcher shortcut |
|
||||
| **Current workspace indicator** | Users need to know "where am I?" at all glance | LOW | None | Status bar, title bar, or persistent sidebar element |
|
||||
| **Connection status indicators** | Network-dependent contexts require real-time status | MEDIUM | None | Online, connecting, offline, error states with distinct visual treatment |
|
||||
| **Saved connection profiles** | Users refuse to re-enter connection details repeatedly | MEDIUM | Profile storage system | Name, host, port, credentials (secure), last connected |
|
||||
| **Recent connections list** | Users return to recent contexts 80%+ of the time | LOW | Profile system | Default to showing 5-10 most recent |
|
||||
| **Per-workspace state preservation** | Context loss on switch = immediate user frustration | HIGH | State management system | Window size, scroll position, open files, UI state |
|
||||
| **Auto-reconnect on network restore** | Brief network blips shouldn't require manual reconnection | MEDIUM | Connection health monitoring | Exponential backoff, max 6-10 retries |
|
||||
| **Error state communication** | Silent failures destroy user trust | LOW | Connection monitoring | Clear error messages with actionable guidance |
|
||||
| **Loading indicators during switch** | Switching delays without feedback feel like freezes | LOW | None | Skeleton states, progress indicators, or spinners |
|
||||
|
||||
### Differentiators (Competitive Advantage)
|
||||
|
||||
Features that set the product apart. Not required, but valued.
|
||||
|
||||
| Feature | Value Proposition | Complexity | Dependencies | Notes |
|
||||
|---------|-------------------|------------|--------------|-------|
|
||||
| **Instant context preview on hover** | Reduces cognitive load by showing context before switching | MEDIUM | State caching | Slack's tab preview pattern - preview without full switch |
|
||||
| **Workspace-specific color coding** | Visual distinction reduces mental overhead | LOW | Visual theming system | Discord/Slack pattern - unique color per workspace |
|
||||
| **Parallel workspace windows** | Advanced users want simultaneous multi-context view | HIGH | Window management, resource isolation | Slack's separate windows feature |
|
||||
| **Smart workspace ordering** | Auto-prioritize by usage frequency | LOW | Usage analytics | VS Code's MRU (most recently used) ordering |
|
||||
| **Workspace search/filter** | Critical when managing 10+ workspaces | LOW | Workspace metadata | Command palette pattern from VS Code |
|
||||
| **One-click duplicate workspace** | Speeds up creating similar configurations | MEDIUM | Profile cloning system | Common in database tools |
|
||||
| **Workspace activity notifications** | Stay informed without context switching | MEDIUM | Event system | Slack's unread indicators per workspace |
|
||||
| **Offline-first with sync queue** | Continue working during network issues | HIGH | Local storage, sync engine | WhatsApp pattern - queue and sync later |
|
||||
| **Workspace templates** | Accelerate common setup patterns | MEDIUM | Template storage system | JetBrains pattern for remote dev environments |
|
||||
| **Connection health metrics** | Proactive awareness prevents surprises | MEDIUM | Network monitoring | Latency, bandwidth, stability indicators |
|
||||
| **Workspace-specific keyboard shortcuts** | Power users customize per context | HIGH | Shortcut management system | VS Code's workspace-level settings pattern |
|
||||
| **Quick switcher with fuzzy search** | Fastest method for 20+ workspaces | LOW | Search algorithm | Cmd+K pattern from modern apps |
|
||||
| **Workspace groups/folders** | Organize 50+ workspaces hierarchically | MEDIUM | Grouping data model | Remote Desktop Manager pattern |
|
||||
| **Context-aware AI assistance** | Auto-suggest next workspace based on patterns | HIGH | ML/pattern recognition | 2026 trend - VS Code January 2026 release |
|
||||
|
||||
### Anti-Features (Commonly Requested, Often Problematic)
|
||||
|
||||
Features that seem good but create problems.
|
||||
|
||||
| Feature | Why Requested | Why Problematic | Alternative |
|
||||
|---------|---------------|-----------------|-------------|
|
||||
| **Automatic workspace switching** | Users think it saves time | Destroys mental model, causes confusion about current context | Suggest switching with one-click confirmation |
|
||||
| **Unlimited parallel workspaces** | Power users want "all contexts open" | Resource exhaustion, performance degradation | Limit to 3-5 windows with clear capacity indicator |
|
||||
| **Real-time sync of all workspace state** | Seems like cloud-native best practice | Network overhead, conflict resolution complexity | Sync critical state only (profiles, favorites); defer non-critical |
|
||||
| **Complex workspace hierarchies** | Enterprise users request deep nesting | Cognitive overhead, navigation confusion | Flat list with tags/labels for filtering |
|
||||
| **Automatic reconnection without confirmation** | Reduces user friction | Security risk for sensitive connections; unexpected costs | Auto-reconnect with user-configurable policy per profile |
|
||||
| **Workspace merging** | Requested for "combining contexts" | State collision, unclear semantics | Support opening multiple windows instead |
|
||||
| **Background workspace updates** | Keep all contexts "warm" | Resource drain, battery impact | Update on switch or on-demand refresh only |
|
||||
| **Infinite workspace history** | "Never lose a workspace" | Storage bloat, performance degradation | Keep 50-100 recent, archive older |
|
||||
| **Cross-workspace clipboard sync** | Seems convenient | Security issue, unexpected data leakage between contexts | Explicit "copy to..." action with user confirmation |
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
Core Infrastructure:
|
||||
Profile Storage System
|
||||
└──requires──> Secure Credential Storage
|
||||
└──enables──> Recent Connections List
|
||||
└──enables──> Favorite/Pinned Workspaces
|
||||
└──enables──> Workspace Templates
|
||||
|
||||
Connection Manager
|
||||
└──requires──> Profile System
|
||||
└──enables──> Connection Status Indicators
|
||||
└──enables──> Auto-reconnect
|
||||
└──enables──> Connection Health Metrics
|
||||
|
||||
State Preservation System
|
||||
└──requires──> Per-workspace state isolation
|
||||
└──enables──> Instant context switching
|
||||
└──enables──> Context preview on hover
|
||||
└──enables──> Parallel workspace windows
|
||||
|
||||
Visual Layer:
|
||||
Workspace List UI
|
||||
└──requires──> Profile System
|
||||
└──enhances──with──> Quick Switcher
|
||||
└──enhances──with──> Fuzzy Search
|
||||
└──enhances──with──> Workspace Groups
|
||||
|
||||
Status Indicators
|
||||
└──requires──> Connection Manager
|
||||
└──enhances──with──> Connection Health Metrics
|
||||
└──enhances──with──> Activity Notifications
|
||||
|
||||
Advanced Features:
|
||||
Parallel Windows
|
||||
└──requires──> State Preservation
|
||||
└──requires──> Resource Isolation
|
||||
└──conflicts──with──> Unlimited Parallel Workspaces (anti-pattern)
|
||||
|
||||
Offline-First
|
||||
└──requires──> Local State Storage
|
||||
└──requires──> Sync Queue
|
||||
└──requires──> Conflict Resolution
|
||||
└──enables──> Continue work during outages
|
||||
|
||||
Context-Aware AI
|
||||
└──requires──> Usage Analytics
|
||||
└──requires──> Pattern Recognition
|
||||
└──enhances──> Quick Switcher
|
||||
```
|
||||
|
||||
### Dependency Notes
|
||||
|
||||
- **Profile System is foundational**: Nearly all features depend on having profile storage and management
|
||||
- **State Preservation enables advanced features**: Context preview, parallel windows, and offline mode all require robust state management
|
||||
- **Connection Manager is critical path**: Status indicators, health metrics, and auto-reconnect all depend on centralized connection management
|
||||
- **Quick Switcher amplifies other features**: When combined with fuzzy search, workspace groups, and AI suggestions, it becomes the primary navigation method
|
||||
- **Parallel windows conflict with unlimited contexts**: Must enforce limits to prevent resource exhaustion
|
||||
|
||||
## MVP Definition
|
||||
|
||||
### Launch With (v1) - Core Context Switching
|
||||
|
||||
**Goal**: Users can switch between local and SSH workspaces without friction
|
||||
|
||||
**Essential features** (ranked by priority):
|
||||
1. **Visual workspace list with status** - Users need to see available workspaces and connection status at a glance
|
||||
- Why: Table stakes. Users can't switch if they can't see options
|
||||
- Implementation: Sidebar with workspace cards showing name, type (local/SSH), and status dot
|
||||
2. **Saved connection profiles** - Store SSH host, port, user for quick access
|
||||
- Why: Re-entering connection details is unacceptable UX
|
||||
- Implementation: Profile manager with CRUD operations, secure credential storage
|
||||
3. **Recent connections (5-10)** - Most users return to same 3-5 workspaces
|
||||
- Why: 80% of switches go to recent contexts
|
||||
- Implementation: Auto-populate list from connection history
|
||||
4. **Current workspace indicator** - Status bar showing active workspace
|
||||
- Why: Prevents "where am I?" confusion
|
||||
- Implementation: Persistent badge in title bar or status bar
|
||||
5. **Per-workspace state preservation** - Restore open files, scroll position, window size
|
||||
- Why: Context loss = user frustration and productivity loss
|
||||
- Implementation: State snapshot per workspace, restore on switch
|
||||
6. **Connection status indicators** - Online, connecting, offline, error states
|
||||
- Why: Network-dependent contexts need real-time status
|
||||
- Implementation: Color-coded dots with tooltip details
|
||||
7. **Loading states during switch** - Visual feedback for 0.5s+ operations
|
||||
- Why: Delays without feedback feel like hangs
|
||||
- Implementation: Skeleton UI or progress bar
|
||||
8. **Basic error handling** - Clear messages for connection failures
|
||||
- Why: Silent failures destroy trust
|
||||
- Implementation: Error modal with actionable message and retry button
|
||||
9. **Keyboard shortcut for switcher** - Cmd/Ctrl+K to open workspace list
|
||||
- Why: Power users demand keyboard access
|
||||
- Implementation: Keyboard shortcut opening modal or command palette
|
||||
|
||||
**Explicitly NOT in v1**:
|
||||
- Command palette/fuzzy search (v1.x priority)
|
||||
- Auto-reconnect (v1.x priority)
|
||||
- Parallel windows (v2+ complexity)
|
||||
- Workspace groups (only needed at 20+ workspaces)
|
||||
- AI suggestions (v2+ nice-to-have)
|
||||
|
||||
### Add After Validation (v1.x) - Enhanced Switching
|
||||
|
||||
**Triggers for adding**:
|
||||
- User feedback: "I have 10+ workspaces and can't find them"
|
||||
- Analytics: Users repeatedly reconnecting after brief network blips
|
||||
- Support tickets: Confusion about workspace state after switch
|
||||
|
||||
**Features to add**:
|
||||
1. **Quick switcher with fuzzy search** - Cmd+K to instantly filter workspaces
|
||||
- When: Users managing 10+ workspaces
|
||||
- Why: Linear list becomes overwhelming; search is faster than scrolling
|
||||
2. **Auto-reconnect with exponential backoff** - Automatically retry failed connections
|
||||
- When: Users report frustration with manual reconnection
|
||||
- Why: Brief network blips are common; auto-recovery improves UX
|
||||
3. **Workspace templates** - Save connection profile as reusable template
|
||||
- When: Users creating multiple similar configurations
|
||||
- Why: Accelerates common setup patterns
|
||||
4. **Connection health metrics** - Latency, stability indicators in workspace list
|
||||
- When: Users working across high-latency connections
|
||||
- Why: Proactive awareness prevents surprises
|
||||
5. **Workspace-specific color coding** - Visual distinction per workspace
|
||||
- When: Users managing 5+ workspaces and switching frequently
|
||||
- Why: Reduces cognitive load through visual patterns
|
||||
6. **Activity notifications** - Unread indicators for background workspaces
|
||||
- When: Users need to monitor multiple contexts simultaneously
|
||||
- Why: Stay informed without switching
|
||||
|
||||
### Future Consideration (v2+) - Advanced Capabilities
|
||||
|
||||
Features to defer until product-market fit is established.
|
||||
|
||||
1. **Parallel workspace windows** - Open 2-3 workspaces simultaneously
|
||||
- Why defer: High complexity, resource management challenges
|
||||
- Prerequisites: Usage data showing demand for multi-window workflows
|
||||
2. **Offline-first with sync queue** - Queue operations during network outages
|
||||
- Why defer: Complex conflict resolution, requires robust sync engine
|
||||
- Prerequisites: Users reporting productivity loss during outages
|
||||
3. **Context-aware AI workspace suggestions** - Predict next workspace based on patterns
|
||||
- Why defer: Requires ML infrastructure, significant training data
|
||||
- Prerequisites: Strong v1 adoption, usage analytics baseline
|
||||
4. **Workspace groups/folders** - Hierarchical organization for 50+ workspaces
|
||||
- Why defer: Only needed at significant scale
|
||||
- Prerequisites: Users managing 20+ workspaces
|
||||
5. **Workspace-specific keyboard shortcuts** - Customize shortcuts per context
|
||||
- Why defer: Implementation complexity, potential user confusion
|
||||
- Prerequisites: Power user requests for per-workspace customization
|
||||
6. **Cross-workspace search** - Search files/content across all workspaces
|
||||
- Why defer: Performance challenges, requires indexing infrastructure
|
||||
- Prerequisites: Users managing large numbers of workspaces
|
||||
|
||||
## Feature Prioritization Matrix
|
||||
|
||||
| Feature | User Value | Implementation Cost | Priority | Phase |
|
||||
|---------|------------|---------------------|----------|-------|
|
||||
| Visual workspace list | HIGH | LOW | P1 | v1 |
|
||||
| Saved connection profiles | HIGH | MEDIUM | P1 | v1 |
|
||||
| Recent connections | HIGH | LOW | P1 | v1 |
|
||||
| Current workspace indicator | HIGH | LOW | P1 | v1 |
|
||||
| Per-workspace state preservation | HIGH | HIGH | P1 | v1 |
|
||||
| Connection status indicators | HIGH | MEDIUM | P1 | v1 |
|
||||
| Loading states | HIGH | LOW | P1 | v1 |
|
||||
| Basic error handling | HIGH | LOW | P1 | v1 |
|
||||
| Keyboard shortcut for switcher | HIGH | LOW | P1 | v1 |
|
||||
| Quick switcher with fuzzy search | HIGH | LOW | P2 | v1.x |
|
||||
| Auto-reconnect | HIGH | MEDIUM | P2 | v1.x |
|
||||
| Workspace templates | MEDIUM | MEDIUM | P2 | v1.x |
|
||||
| Connection health metrics | MEDIUM | MEDIUM | P2 | v1.x |
|
||||
| Workspace color coding | MEDIUM | LOW | P2 | v1.x |
|
||||
| Activity notifications | MEDIUM | MEDIUM | P2 | v1.x |
|
||||
| Context preview on hover | MEDIUM | MEDIUM | P2 | v1.x |
|
||||
| Parallel workspace windows | HIGH | HIGH | P3 | v2+ |
|
||||
| Offline-first with sync queue | MEDIUM | HIGH | P3 | v2+ |
|
||||
| Context-aware AI suggestions | LOW | HIGH | P3 | v2+ |
|
||||
| Workspace groups/folders | MEDIUM | MEDIUM | P3 | v2+ |
|
||||
| Workspace-specific shortcuts | LOW | HIGH | P3 | v2+ |
|
||||
| Cross-workspace search | MEDIUM | HIGH | P3 | v2+ |
|
||||
|
||||
**Priority key**:
|
||||
- **P1 (Must have for launch)**: Core functionality, missing = broken product
|
||||
- **P2 (Should have, add when possible)**: Significantly improves UX, validates value proposition
|
||||
- **P3 (Nice to have, future consideration)**: Advanced capabilities for mature product
|
||||
|
||||
## UX Pattern Analysis by Application
|
||||
|
||||
### VS Code Remote (2026)
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- Remote Explorer with visual host list
|
||||
- SSH config file integration (saved profiles)
|
||||
- Status bar indicator showing local/remote context
|
||||
- Recent connections in Command Palette
|
||||
- Extension classification (UI vs Workspace) for state isolation
|
||||
|
||||
**Differentiators**:
|
||||
- Workspace indexing for faster code search (January 2026)
|
||||
- Agent integration with remote workspaces (AI-assisted development)
|
||||
- Extension recommendations per workspace type
|
||||
- Transparent remote filesystem access
|
||||
|
||||
**Observed Patterns**:
|
||||
- Command Palette as primary navigation (Ctrl+Shift+P)
|
||||
- Activity Bar for persistent context (sidebar with icons)
|
||||
- Status bar for ambient awareness (current context, connection status)
|
||||
|
||||
### JetBrains Gateway
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- Workspace list with start/stop controls
|
||||
- Connection status display (started, stopped, busy)
|
||||
- Recent workspaces list
|
||||
- IDE backend management (version control)
|
||||
|
||||
**Differentiators**:
|
||||
- Explicit workspace lifecycle control (start, stop, not just connect)
|
||||
- IDE backend version management from Gateway
|
||||
- Integration with cloud dev environments (Coder, Gitpod, Harness)
|
||||
|
||||
**Observed Patterns**:
|
||||
- Explicit state management (stopped vs running)
|
||||
- Green/red controls for visual clarity (start/stop icons)
|
||||
- Backend management separated from connection management
|
||||
|
||||
### Slack
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- Workspace switcher icon in title bar
|
||||
- Keyboard shortcuts (Ctrl+Shift+S for switcher, Ctrl+Number to jump)
|
||||
- Recent workspace list
|
||||
- Per-workspace notifications
|
||||
|
||||
**Differentiators**:
|
||||
- Separate windows for simultaneous multi-workspace view
|
||||
- Workspace filter to show/hide specific workspaces
|
||||
- Tab preview on hover (see content without switching)
|
||||
- Ctrl+Shift+[ and ] for sequential navigation
|
||||
- Enterprise Grid support (simplified multi-workspace in large orgs)
|
||||
|
||||
**Observed Patterns**:
|
||||
- Workspace switcher always visible (persistent awareness)
|
||||
- Multiple access methods (click, keyboard numbers, sequential nav)
|
||||
- Parallel windows for advanced users
|
||||
|
||||
### Notion
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- Workspace dropdown in top-left
|
||||
- Ctrl+Shift+Number for quick switching
|
||||
- Multi-account support (10+ accounts without hard limit)
|
||||
- Drag-to-reorder workspaces
|
||||
|
||||
**Differentiators**:
|
||||
- Account aggregation (multiple email accounts in one app)
|
||||
- Workspace prioritization via drag-and-drop
|
||||
- Smooth tab switching with preview on hover
|
||||
|
||||
**Observed Patterns**:
|
||||
- Top-left corner for workspace identity (consistent with OS patterns)
|
||||
- Visual priority ordering (users customize workspace sequence)
|
||||
- Performance degradation warning (10+ accounts)
|
||||
|
||||
### Discord
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- Server list in left sidebar (always visible)
|
||||
- Status indicators (online, idle, DND, streaming)
|
||||
- Platform-specific icons (desktop vs mobile)
|
||||
- Quick Switcher (Ctrl+K) with online status indicators (October 2025)
|
||||
|
||||
**Differentiators**:
|
||||
- Color-coded status (green=online, yellow=idle, red=DND, purple=streaming)
|
||||
- Platform awareness (desktop vs mobile indicators)
|
||||
- Status shown in Quick Switcher search results
|
||||
- Unread indicators per server
|
||||
|
||||
**Observed Patterns**:
|
||||
- Persistent sidebar for context awareness
|
||||
- Quick Switcher as power user feature
|
||||
- Visual status prioritized over text labels
|
||||
|
||||
### Figma (2026)
|
||||
|
||||
**Table Stakes Implemented**:
|
||||
- File list with recent files
|
||||
- Standardized page structures (Cover, Playground, Handoff, Archive)
|
||||
- Team file navigation
|
||||
|
||||
**Differentiators**:
|
||||
- Multi-file modular architecture (tokens, components, docs in separate files)
|
||||
- AI-powered search across team files
|
||||
- UI3 redesign maximizing canvas space while retaining navigation anchors
|
||||
- Zero cognitive load context switching via standardized structures
|
||||
|
||||
**Observed Patterns**:
|
||||
- Standardization reduces cognitive load (consistent structure = instant familiarity)
|
||||
- Modular file architecture for independent updates
|
||||
- AI assistance for cross-file navigation
|
||||
|
||||
## Common Patterns Across Applications
|
||||
|
||||
### Switcher UI Patterns
|
||||
1. **Dropdown** (Notion, VS Code partial): Click workspace name to show list
|
||||
2. **Sidebar** (Discord, Slack partial): Always-visible list for instant access
|
||||
3. **Command Palette** (VS Code, Discord, Figma): Keyboard-first fuzzy search
|
||||
4. **Hybrid** (Slack): All three methods available
|
||||
|
||||
**Recommendation**: Start with dropdown + keyboard shortcut for MVP; add command palette in v1.x when users manage 10+ workspaces.
|
||||
|
||||
### Status Indicator Patterns
|
||||
1. **Color-coded dots** (Discord, VS Code): Green=connected, yellow=connecting, red=error, gray=offline
|
||||
2. **Text labels** (JetBrains): "Started", "Stopped", "Busy"
|
||||
3. **Icon + text combo** (Slack): Icon with tooltip on hover
|
||||
|
||||
**Recommendation**: Color-coded dots with tooltip for details (minimal space, accessible, standard pattern).
|
||||
|
||||
### Connection Profile Patterns
|
||||
1. **Manual entry** (VS Code): Edit SSH config file
|
||||
2. **Form-based** (JetBrains, database tools): GUI for connection details
|
||||
3. **Implicit** (Notion, Slack): Profiles created automatically on first connection
|
||||
|
||||
**Recommendation**: Form-based GUI for SSH connections (lower friction); auto-save on successful connection as implicit profile.
|
||||
|
||||
### State Preservation Patterns
|
||||
1. **Full snapshot** (VS Code): Restore all open files, cursor positions, UI state
|
||||
2. **Partial snapshot** (Slack): Remember channel but not scroll position
|
||||
3. **Session storage** (Web apps): Local/session storage for UI state
|
||||
|
||||
**Recommendation**: Full snapshot for development tools (VS Code pattern); partial for lighter contexts if needed.
|
||||
|
||||
### Loading State Patterns
|
||||
1. **Skeleton screens** (Modern web apps): Show structure while loading
|
||||
2. **Progress bars** (JetBrains): Percentage-based for long operations
|
||||
3. **Spinners** (Generic): Simple animation for indeterminate duration
|
||||
|
||||
**Recommendation**: Skeleton screens for predictable loading (workspace list); spinners for connection attempts (unpredictable duration).
|
||||
|
||||
### Error Handling Patterns
|
||||
1. **Modal dialogs** (Traditional apps): Blocking error message with retry
|
||||
2. **Toast notifications** (Modern apps): Non-blocking error with auto-dismiss
|
||||
3. **Inline errors** (Forms): Error message in context of failure
|
||||
|
||||
**Recommendation**: Toast notifications for transient errors (network blip); modal for critical errors requiring user action (invalid credentials).
|
||||
|
||||
## Roadmap Implications
|
||||
|
||||
### Phase 1: Core Infrastructure (v1)
|
||||
**Focus**: Make workspace switching work reliably
|
||||
|
||||
**Critical features**:
|
||||
- Profile storage with secure credentials
|
||||
- Connection manager with status tracking
|
||||
- State preservation per workspace
|
||||
- Visual workspace list with status
|
||||
- Basic keyboard navigation
|
||||
|
||||
**Success criteria**: Users can switch between local and SSH workspaces without re-entering details, with state preserved across switches.
|
||||
|
||||
### Phase 2: Enhanced UX (v1.x)
|
||||
**Focus**: Make switching delightful
|
||||
|
||||
**Critical features**:
|
||||
- Quick switcher with fuzzy search
|
||||
- Auto-reconnect on network restore
|
||||
- Workspace color coding
|
||||
- Connection health metrics
|
||||
- Activity notifications
|
||||
|
||||
**Success criteria**: Users with 10+ workspaces can find and switch to any workspace in <2 seconds; network blips don't interrupt workflows.
|
||||
|
||||
### Phase 3: Advanced Capabilities (v2+)
|
||||
**Focus**: Support power users and scale
|
||||
|
||||
**Critical features**:
|
||||
- Parallel workspace windows
|
||||
- Offline-first with sync queue
|
||||
- Workspace groups for 50+ contexts
|
||||
- Context-aware AI suggestions
|
||||
- Cross-workspace search
|
||||
|
||||
**Success criteria**: Users managing 20+ workspaces across flaky networks maintain productivity; power users can monitor multiple contexts simultaneously.
|
||||
|
||||
## Sources
|
||||
|
||||
### VS Code Remote Development
|
||||
- [VS Code Remote Development Overview](https://code.visualstudio.com/docs/remote/remote-overview)
|
||||
- [Remote Development using SSH](https://code.visualstudio.com/docs/remote/ssh)
|
||||
- [January 2026 Release Notes (version 1.109)](https://code.visualstudio.com/updates/v1_109)
|
||||
- [Supporting Remote Development and GitHub Codespaces](https://code.visualstudio.com/api/advanced-topics/remote-extensions)
|
||||
- [Remote Development FAQ](https://code.visualstudio.com/docs/remote/faq)
|
||||
|
||||
### JetBrains Gateway
|
||||
- [JetBrains Gateway - Remote Development for JetBrains IDEs](https://www.jetbrains.com/remote-development/gateway/)
|
||||
- [Connect and work with JetBrains Gateway | IntelliJ IDEA Documentation](https://www.jetbrains.com/help/idea/remote-development-a.html)
|
||||
- [A Deep Dive Into JetBrains Gateway | The JetBrains Blog](https://blog.jetbrains.com/blog/2021/12/03/dive-into-jetbrains-gateway/)
|
||||
- [Remote development overview | IntelliJ IDEA Documentation](https://www.jetbrains.com/help/idea/remote-development-overview.html)
|
||||
|
||||
### Slack
|
||||
- [Switch between workspaces | Slack](https://slack.com/help/articles/1500002200741-Switch-between-workspaces)
|
||||
- [A consolidated set of tabs for Slack on desktop | Slack](https://slack.com/help/articles/16764236868755-An-overview-of-Slacks-new-design)
|
||||
- [A redesigned Slack, built for focus | Slack](https://slack.com/blog/productivity/a-redesigned-slack-built-for-focus)
|
||||
- [More Intuitive Multi-Workspace Slack Experience | Medium](https://medium.com/design-bootcamp/slack-home-reinventing-a-more-intuitive-experience-for-you-a-ux-case-study-da7c3e399cc6)
|
||||
|
||||
### Notion
|
||||
- [Create, join & leave workspaces – Notion Help Center](https://www.notion.com/help/create-delete-and-switch-workspaces)
|
||||
- [Notion for desktop – Notion Help Center](https://www.notion.com/help/notion-for-desktop)
|
||||
- [A Notion guide on switching between work and personal accounts](https://www.notion.com/help/guides/a-notion-guide-on-switching-between-work-and-personal-accounts)
|
||||
- [Intro to workspaces – Notion Help Center](https://www.notion.com/help/intro-to-workspaces)
|
||||
|
||||
### Discord
|
||||
- [Discord Patch Notes: October 7, 2025](https://discord.com/blog/discord-patch-notes-october-7-2025)
|
||||
- [User Status | Discord Wiki](https://discord.fandom.com/wiki/User_Status)
|
||||
- [Discord Status Icons: 2026 Guide](https://www.qqtube.com/blog/what-does-the-green-phone-icon-mean-on-discord)
|
||||
|
||||
### Figma
|
||||
- [The Complete Guide to Design Systems in Figma (2026 Edition) | Medium](https://medium.com/@EmiliaBiblioKit/the-world-of-design-systems-is-no-longer-just-about-components-and-libraries-its-about-5beecc0d21cb)
|
||||
- [Stop the Chaos: The Best Figma Plugins to Organize Design Files in 2026 | Medium](https://medium.com/design-bootcamp/stop-the-chaos-the-best-figma-plugins-to-organize-design-files-in-2026-ff9941d213a6)
|
||||
- [Figma Config 2025: What's new, what's next, and what you should be doing - LogRocket Blog](https://blog.logrocket.com/ux-design/figma-config-2025-whats-new-whats-next/)
|
||||
|
||||
### UX Patterns & Best Practices
|
||||
- [Linux Desktop: Do we need better Workspace Management?](https://linuxblog.io/linux-desktop-workspace-management/)
|
||||
- [Designing desktop apps for cross-platform UX | ToDesktop Blog](https://www.todesktop.com/blog/posts/designing-desktop-apps-cross-platform-ux)
|
||||
- [Command Palette | UX Patterns #1 | Medium](https://medium.com/design-bootcamp/command-palette-ux-patterns-1-d6b6e68f30c1)
|
||||
- [Command Palette UI Design: Best practices, Design variants & Examples | Mobbin](https://mobbin.com/glossary/command-palette)
|
||||
|
||||
### Connection Profile Management
|
||||
- [Remote Desktop Connection Manager - Microsoft Learn](https://learn.microsoft.com/en-us/sysinternals/downloads/rdcman)
|
||||
- [Favorite Connections - Compass - MongoDB Docs](https://docs.mongodb.com/compass/master/connect/favorite-connections/)
|
||||
- [Manage Connection Profiles](https://github.com/microsoft/vscode-mssql/wiki/manage-connection-profiles)
|
||||
- [Preferred connections: "remember" frequently used servers/databases with SSMSBoost](https://www.ssmsboost.com/Features/ssms-add-in-preferred-connections)
|
||||
|
||||
### Error Handling & Reconnection
|
||||
- [Finally! Improved Blazor Server reconnection UX](https://jonhilton.net/blazor-server-reconnects/)
|
||||
- [Connection Management | PubNub Docs](https://www.pubnub.com/docs/general/setup/connection-management)
|
||||
- [Handling Database Reconnection Issues in .NET with Polly - NashTech Blog](https://blog.nashtechglobal.com/handling-database-reconnection-issues-in-net-with-polly/)
|
||||
- [Offline Mobile App Design: Challenges, Strategies, Best Practices - LeanCode](https://leancode.co/blog/offline-mobile-app-design)
|
||||
|
||||
### Loading States & System Status
|
||||
- [UX Design Patterns for Loading - Pencil & Paper](https://www.pencilandpaper.io/articles/ux-pattern-analysis-loading-feedback)
|
||||
- [6 Loading State Patterns That Feel Premium | Medium](https://medium.com/uxdworld/6-loading-state-patterns-that-feel-premium-716aa0fe63e8)
|
||||
- [4 Ways To Communicate the Visibility of System Status in UI | UX Planet](https://uxplanet.org/4-ways-to-communicate-the-visibility-of-system-status-in-ui-14ff2351c8e8)
|
||||
- [FOSDEM 2026 - Designing for Local-First: UX Patterns for a Network-Optional World](https://fosdem.org/2026/schedule/event/JX7Y3D-ux-design-for-local-first/)
|
||||
|
||||
---
|
||||
*Feature research for: Multi-context workspace switching in desktop applications*
|
||||
*Researched: 2026-02-12*
|
||||
408
.planning/research/PITFALLS.md
Normal file
408
.planning/research/PITFALLS.md
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
# Pitfalls Research
|
||||
|
||||
**Domain:** Multi-context workspace switching for Electron + Zustand apps
|
||||
**Researched:** 2026-02-12
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
### Pitfall 1: Destructive Context Switching with Incomplete State Snapshots
|
||||
|
||||
**What goes wrong:**
|
||||
Switching from SSH to local (or vice versa) calls `getFullResetState()` which wipes ALL selections and loaded data, destroying the user's previous workspace state. When switching back, users must re-navigate to their project, re-open sessions, and restore their scroll position manually.
|
||||
|
||||
**Why it happens:**
|
||||
Current implementation treats context switch as a "hard reset" instead of maintaining separate state snapshots per context. The `connectionSlice` spreads `getFullResetState()` on connect/disconnect, clearing `selectedProjectId`, `selectedSessionId`, `conversation`, tabs, and all derived state.
|
||||
|
||||
**How to avoid:**
|
||||
- **Snapshot before switching**: Capture full AppState before connect/disconnect
|
||||
- **Scope snapshots by context**: Use `Map<'local' | SshHostKey, AppState>` to store separate state per context
|
||||
- **Partial restoration**: Only restore state compatible with the new context (project IDs may differ between local/SSH)
|
||||
- **Preserve UI preferences**: Tab layout, expanded groups, scroll positions are context-agnostic and should always restore
|
||||
|
||||
**Warning signs:**
|
||||
- User complaints about "losing their place" when switching contexts
|
||||
- Empty dashboard after SSH connect, despite having viewed sessions before
|
||||
- No tabs open after switching back to local mode
|
||||
- Context panel expansions reset on every switch
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — state snapshot/restore must be foundational before adding multi-context features.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: EventEmitter Listener Accumulation on Repeated Context Switches
|
||||
|
||||
**What goes wrong:**
|
||||
FileWatcher, SshConnectionManager, and NotificationManager are EventEmitters. Each context switch may call `setFileSystemProvider()` or re-initialize services without removing old listeners, causing:
|
||||
- Memory leaks (listeners hold references to previous provider instances)
|
||||
- Duplicate event emissions (one event triggers 2x, 3x handlers)
|
||||
- Stale event handlers executing against wrong context
|
||||
|
||||
**Why it happens:**
|
||||
Services use `.on()` to attach listeners but don't call `.removeAllListeners()` or track cleanup functions. The `initializeNotificationListeners()` function in store/index.ts returns a cleanup function, but there's no guarantee it's called before re-initialization.
|
||||
|
||||
**How to avoid:**
|
||||
- **Centralized cleanup**: Create `dispose()` methods for all EventEmitter services
|
||||
- **Lifecycle tracking**: Maintain `Map<string, () => void>` of cleanup functions keyed by service name
|
||||
- **Pre-switch cleanup**: Call all cleanup functions BEFORE provider swap
|
||||
- **Idempotent initialization**: Check `if (this.isInitialized) return` to prevent double-init
|
||||
- **Avoid `removeAllListeners()` on ipcRenderer**: Always specify channel name — blanket removal breaks Electron internals
|
||||
|
||||
**Warning signs:**
|
||||
- Process memory usage (RSS) grows 50-100MB per context switch
|
||||
- DevTools heap snapshot shows multiple FileWatcher/SshConnectionManager instances
|
||||
- Console logs show duplicate "file-change" events for same file
|
||||
- IPC handlers fire 2x for single user action
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — service lifecycle management is critical before multi-context.
|
||||
|
||||
**Sources:**
|
||||
- [Error: Removing all listeners from ipcRenderer will make Electron internals stop working](https://github.com/electron/electron/issues/10379)
|
||||
- [IPC in Electron - Ray](https://myray.app/blog/ipc-in-electron)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Stale Closures in Zustand Actions Capturing Old Provider References
|
||||
|
||||
**What goes wrong:**
|
||||
Zustand actions like `fetchProjects()` capture the FileSystemProvider in their closure at store creation time. After calling `sshConnectionManager.getProvider()` which returns a new SshFileSystemProvider, existing actions still reference the old LocalFileSystemProvider, causing operations to execute against the wrong context.
|
||||
|
||||
**Why it happens:**
|
||||
JavaScript closures capture variables at definition time. When a Zustand action calls `window.electronAPI.getProjects()`, the IPC handler calls `projectScanner.scan()`, which calls `this.fsProvider.readdir()`. If the scanner's provider reference wasn't updated, it uses the stale provider.
|
||||
|
||||
**How to avoid:**
|
||||
- **Late binding via getter**: Services should call `getProvider()` on every operation, not cache at construction
|
||||
- **Provider as parameter**: Pass provider explicitly to service methods instead of storing as instance variable
|
||||
- **Re-initialize services**: After provider swap, call `fileWatcher.setFileSystemProvider(newProvider)` on ALL services
|
||||
- **Service registry pattern**: Centralize provider injection so swap happens in one place
|
||||
- **Verify in tests**: Mock provider swap and assert service calls hit new provider
|
||||
|
||||
**Warning signs:**
|
||||
- SSH connect succeeds but dashboard shows local projects
|
||||
- File operations fail with "ENOENT" after provider swap
|
||||
- Console shows "LocalFileSystemProvider" operations when SSH is connected
|
||||
- User switches to SSH but sees local data until app restart
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — provider injection architecture must be correct from the start.
|
||||
|
||||
**Sources:**
|
||||
- [Be Aware of Stale Closures when Using React Hooks](https://dmitripavlutin.com/react-hooks-stale-closures/)
|
||||
- [How to Fix "Stale Closure" Issues in React Hooks](https://oneuptime.com/blog/post/2026-01-26-fix-stale-closure-issues-react-hooks/view)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: IPC Race Conditions During Context Switch with In-Flight Requests
|
||||
|
||||
**What goes wrong:**
|
||||
User clicks "Connect SSH" → IPC handler starts scanning remote projects → user quickly clicks "Disconnect" → scan completes and populates store with SSH data → store now shows SSH projects in local mode, creating data corruption.
|
||||
|
||||
**Why it happens:**
|
||||
Async IPC calls (getProjects, getSessions) don't track which context initiated them. By the time a response arrives, the context may have switched, but Zustand applies the stale response anyway.
|
||||
|
||||
**How to avoid:**
|
||||
- **Context ID stamping**: Include `contextId` (UUID per connection) in every IPC request/response
|
||||
- **Response validation**: Check `if (currentContextId !== response.contextId) return` before applying
|
||||
- **Request cancellation**: Track in-flight requests via AbortController and cancel on context switch
|
||||
- **State machine**: Use FSM with states: `idle → connecting → connected → disconnecting → idle`. Reject operations in wrong states.
|
||||
- **Sequential transitions**: Wait for disconnect cleanup to complete before starting connect
|
||||
|
||||
**Warning signs:**
|
||||
- Dashboard briefly flickers between local and SSH data during transitions
|
||||
- Error notifications appear for wrong context after switching
|
||||
- Search results from previous context appear after switch
|
||||
- Console shows "Cannot read property of null" after rapid connect/disconnect
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — race condition prevention is non-negotiable for reliability.
|
||||
|
||||
**Sources:**
|
||||
- [Syncing State between Electron Contexts](https://brunoscheufler.com/blog/2023-10-29-syncing-state-between-electron-contexts)
|
||||
- [Advanced Electron.js architecture - LogRocket Blog](https://blog.logrocket.com/advanced-electron-js-architecture/)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: FileWatcher Polling Timer Not Cleared on SSH Disconnect
|
||||
|
||||
**What goes wrong:**
|
||||
SSH mode uses `setInterval()` polling (5s) instead of fs.watch(). When user disconnects, `stop()` clears `pollingTimer`, but the cleanup happens in `finally` block — if an exception occurs, timer keeps running. After 10 switches, 10 polling timers consume CPU checking a disconnected SSH provider.
|
||||
|
||||
**Why it happens:**
|
||||
Interval timers require explicit `clearInterval()`, and error paths may skip cleanup. The `pollingTimer` property is set to null without actually clearing the interval, causing orphaned timers.
|
||||
|
||||
**How to avoid:**
|
||||
- **Cleanup in finally**: Always clear timers in `finally` block, not just normal path
|
||||
- **Defensive clearing**: Before creating new timer, clear existing: `if (this.timer) clearInterval(this.timer)`
|
||||
- **Timer registry**: Track all active timers in Set<NodeJS.Timeout> and clear all on dispose
|
||||
- **Polling refactor**: Consider using a single interval that checks `isRemote()` flag instead of separate modes
|
||||
- **Memory profiling**: Add setInterval/clearInterval tracking to detect orphaned timers
|
||||
|
||||
**Warning signs:**
|
||||
- CPU usage increases by 5-10% per context switch
|
||||
- Chrome DevTools → Performance shows multiple "pollForChanges" concurrent executions
|
||||
- Memory usage grows 20MB per switch (timer closures hold provider references)
|
||||
- App becomes sluggish after 5-10 SSH switches
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — timer lifecycle bugs cause performance degradation.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 6: SSH Connection Not Properly Disposed, Keeping Socket Open
|
||||
|
||||
**What goes wrong:**
|
||||
`SshConnectionManager.disconnect()` calls `client.end()`, but if the SFTP channel is still processing operations, the socket remains open (TCP FIN not sent). After 10 context switches, 10 SSH connections consume file descriptors and remote server resources.
|
||||
|
||||
**Why it happens:**
|
||||
ssh2 Client `.end()` is graceful (waits for channel closure), but `.destroy()` is immediate. If you don't wait for SFTP operations to complete before calling `.end()`, the connection may linger. The `dispose()` method swallows errors, hiding cleanup failures.
|
||||
|
||||
**How to avoid:**
|
||||
- **Force close on disconnect**: Use `client.destroy()` instead of `.end()` for immediate termination
|
||||
- **SFTP channel close**: Call `sftp.end()` explicitly before `client.end()`
|
||||
- **Connection timeout**: Set 5s timeout on disconnect — if client doesn't close, force destroy
|
||||
- **Socket tracking**: Expose `net.Socket` from Client and verify `socket.destroyed === true` post-cleanup
|
||||
- **Resource monitoring**: Log open file descriptors (lsof) to detect leaks
|
||||
|
||||
**Warning signs:**
|
||||
- `lsof -p <pid>` shows 10+ TCP connections to SSH host
|
||||
- SSH host reports "Max sessions exceeded" after many switches
|
||||
- `netstat` shows multiple ESTABLISHED connections to port 22
|
||||
- Disconnect takes 5+ seconds (waiting for timeout)
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — connection leaks cause operational issues at scale.
|
||||
|
||||
**Sources:**
|
||||
- [Diagnosing and Fixing Memory Leaks in Electron Applications](https://www.mindfulchase.com/explore/troubleshooting-tips/frameworks-and-libraries/diagnosing-and-fixing-memory-leaks-in-electron-applications.html)
|
||||
- [Viacheslav Eremin | Memory Leaks in Electron application](https://www.vb-net.com/AngularElectron/MemoryLeaks.htm)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 7: Tab State Restoration Without Context Validation
|
||||
|
||||
**What goes wrong:**
|
||||
Snapshot captures open tabs with `projectId: "abc-local-path"`. User switches to SSH where projects have different IDs. Restoration tries to open tab with local projectId against SSH provider → tab shows "Project not found" error or stale local data.
|
||||
|
||||
**Why it happens:**
|
||||
ProjectId encoding includes file path (`-Users-name-project`), so local and SSH projects for the same logical directory have different IDs. Tab restoration doesn't validate whether a project exists in the new context.
|
||||
|
||||
**How to avoid:**
|
||||
- **Path normalization**: Introduce `logicalProjectPath` (e.g., `/home/user/project`) separate from encoded ID
|
||||
- **Cross-context mapping**: Maintain `Map<logicalPath, {localId, sshId}>` to translate IDs during restore
|
||||
- **Graceful degradation**: If project doesn't exist in new context, close tab or show empty state instead of error
|
||||
- **Restore validation**: Before applying snapshot, call `window.electronAPI.getProjects()` and filter tabs to existing projects
|
||||
- **User notification**: Show toast "3 tabs closed - projects not available in SSH mode"
|
||||
|
||||
**Warning signs:**
|
||||
- User switches to SSH, tabs show "Failed to load session"
|
||||
- Console errors "Project ID abc-local not found" after context switch
|
||||
- Tab bar shows ghost tabs (visible but non-functional)
|
||||
- Click on restored tab triggers navigation to dashboard instead
|
||||
|
||||
**Phase to address:**
|
||||
Phase 2 (State Persistence) — after Phase 1 establishes snapshots, validation ensures safe restoration.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 8: Partial Snapshot Creates Inconsistent Derived State
|
||||
|
||||
**What goes wrong:**
|
||||
Snapshot captures `selectedProjectId`, `selectedSessionId`, but omits `sessionDetail`, `conversation`, `chunks`. Restoration sets selections without corresponding data → store state says "session XYZ selected" but detail is null → UI renders empty or crashes on null access.
|
||||
|
||||
**Why it happens:**
|
||||
Developers manually list fields to snapshot instead of capturing full AppState. Derived state (detail, conversation) is computed from selections, but if selections restore before data loads, the UI sees inconsistent state.
|
||||
|
||||
**How to avoid:**
|
||||
- **Snapshot everything**: Capture `AppState` wholesale, excluding only ephemeral UI flags (loading, error)
|
||||
- **Atomic restoration**: Use `setState(() => snapshot)` so all fields update simultaneously
|
||||
- **Lazy detail loading**: On restoration, if `selectedSessionId` exists but `sessionDetail` is null, trigger background fetch
|
||||
- **State versioning**: Include `snapshotVersion: 1` to handle schema changes (e.g., new fields added)
|
||||
- **Whitelist vs blacklist**: Easier to exclude ephemeral fields (`loading`, `error`) than enumerate 50+ persistent fields
|
||||
|
||||
**Warning signs:**
|
||||
- After switch, session viewer shows spinner indefinitely
|
||||
- Console error "Cannot read property 'chunks' of null"
|
||||
- Dashboard shows selected project but no sessions loaded
|
||||
- Tab labels show "undefined" or "[object Object]"
|
||||
|
||||
**Phase to address:**
|
||||
Phase 1 (Core Infrastructure) — snapshot completeness is foundational to state restoration.
|
||||
|
||||
**Sources:**
|
||||
- [Zustand - react state management made easy](https://graphqleditor.com/blog/zustand/)
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt Patterns
|
||||
|
||||
Shortcuts that seem reasonable but create long-term problems.
|
||||
|
||||
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|
||||
|----------|-------------------|----------------|-----------------|
|
||||
| **Storing provider as instance variable** | Simple, no getter needed | Stale closure bugs on provider swap | Never — always use getter |
|
||||
| **Not tracking cleanup functions** | Less boilerplate code | Memory leaks accumulate with usage | Never — cleanup is mandatory |
|
||||
| **Hard reset on context switch** | Easy to implement (3 lines) | User loses workspace state | Never — destroys UX |
|
||||
| **Using `removeAllListeners()`** | Clears everything at once | Breaks Electron IPC internals | Never — specify channel |
|
||||
| **Sync IPC for connect** | Blocks until complete | UI freezes 2-5s on slow networks | Never — SSH is async |
|
||||
| **Single global DataCache** | Shared cache across contexts | Cache pollution (local data in SSH mode) | Never — scope cache per context |
|
||||
| **Manual field-by-field snapshot** | Fine-grained control | Fragile, breaks when state schema evolves | Only for specific performance tuning |
|
||||
| **Not versioning snapshots** | Don't need migration code | Can't safely restore after app updates | Only in MVP — add versioning in Phase 2 |
|
||||
|
||||
---
|
||||
|
||||
## Integration Gotchas
|
||||
|
||||
Common mistakes when connecting to external services.
|
||||
|
||||
| Integration | Common Mistake | Correct Approach |
|
||||
|-------------|----------------|------------------|
|
||||
| **SSH2 Client** | Using `.end()` without waiting for SFTP close | Call `sftp.end()`, wait 100ms, then `client.destroy()` |
|
||||
| **SFTP operations** | Not catching ENOENT on remote path access | Wrap all `sftp.stat()` in try/catch, validate paths exist |
|
||||
| **IPC context bridge** | Passing large objects (10MB+ session JSON) | Stream via chunks or use temp file + file path |
|
||||
| **FileWatcher SSH polling** | Polling every 1s (too aggressive) | 5s minimum — SSH has latency |
|
||||
| **Zustand subscriptions** | Subscribing to full store on every render | Use selectors: `useStore((s) => s.field, shallow)` |
|
||||
| **React useEffect** | Empty deps array with state access | Include all state in deps or use functional updates |
|
||||
|
||||
---
|
||||
|
||||
## Performance Traps
|
||||
|
||||
Patterns that work at small scale but fail as usage grows.
|
||||
|
||||
| Trap | Symptoms | Prevention | When It Breaks |
|
||||
|------|----------|------------|----------------|
|
||||
| **Cache not scoped by context** | Search returns SSH results in local mode | Separate caches: `localCache`, `sshCache` | After first context switch |
|
||||
| **Snapshot entire 50MB state** | 500ms freeze during switch | Exclude non-serializable (functions, DOM refs) | 10+ open sessions |
|
||||
| **No debounce on provider swap events** | 10 re-renders per switch | Debounce 100ms, batch provider updates | Rapid switching (QA testing) |
|
||||
| **Loading all tabs eagerly** | UI freezes restoring 20 tabs | Virtual scrolling + lazy load tab content | 10+ open tabs |
|
||||
| **Not canceling in-flight IPC** | Stale responses overwrite new data | AbortController per IPC call | Slow network (SSH over VPN) |
|
||||
| **Synchronous validation during snapshot** | Blocks UI during switch | Validate asynchronously post-restoration | Large state (100+ projects) |
|
||||
|
||||
---
|
||||
|
||||
## Security Mistakes
|
||||
|
||||
Domain-specific security issues beyond general web security.
|
||||
|
||||
| Mistake | Risk | Prevention |
|
||||
|---------|------|------------|
|
||||
| **Storing SSH passwords in snapshot** | Plaintext credentials in memory dump | Never snapshot credentials — only host/port/user |
|
||||
| **Not validating SSH host keys** | MITM attacks | Use ssh2 `hostVerifier` callback, check known_hosts |
|
||||
| **Exposing SSH provider to renderer** | Renderer can execute arbitrary commands | Keep provider in main process only, expose via IPC |
|
||||
| **Logging full AppState snapshots** | Sensitive data in logs | Redact: `password`, `privateKey`, `sessionContent` |
|
||||
| **Not sanitizing project paths** | Path traversal attacks | Validate paths with `path.resolve()`, reject ".." |
|
||||
|
||||
---
|
||||
|
||||
## UX Pitfalls
|
||||
|
||||
Common user experience mistakes in this domain.
|
||||
|
||||
| Pitfall | User Impact | Better Approach |
|
||||
|---------|-------------|-----------------|
|
||||
| **No visual feedback during switch** | User doesn't know if click worked | Show connecting spinner + progress bar |
|
||||
| **Immediate switch without confirmation** | Accidental clicks lose workspace | "Switch will close X tabs. Continue?" dialog |
|
||||
| **No indication of current context** | User forgets if in local/SSH | Persistent badge in header: "Local" or "SSH: hostname" |
|
||||
| **Context switch closes all tabs** | User loses 10+ open sessions | Restore tabs if projects exist in new context |
|
||||
| **No way to view both contexts** | Can't compare local vs SSH data | Split view or dual panes (future enhancement) |
|
||||
| **Errors shown as raw exceptions** | "ENOTFOUND" means nothing to user | Friendly: "Cannot connect to SSH host. Check network." |
|
||||
|
||||
---
|
||||
|
||||
## "Looks Done But Isn't" Checklist
|
||||
|
||||
Things that appear complete but are missing critical pieces.
|
||||
|
||||
- [ ] **Context switch** — Looks like it works because SSH connects, but missing: cleanup old listeners, cancel in-flight IPC, validate restored tabs
|
||||
- [ ] **State snapshot** — Appears to save state, but missing: exclude ephemeral fields, version snapshots, validate restorable
|
||||
- [ ] **Provider swap** — FileWatcher updated, but missing: update ProjectScanner, SessionParser, ErrorDetector, NotificationManager
|
||||
- [ ] **Tab restoration** — Tabs re-open, but missing: validate project exists, load session detail, restore scroll position
|
||||
- [ ] **IPC request tracking** — Responses return, but missing: context ID validation, abort on disconnect, timeout handling
|
||||
- [ ] **Service dispose** — dispose() method exists, but missing: remove EventEmitter listeners, clear timers, close connections
|
||||
- [ ] **Cache invalidation** — Cache cleared on switch, but missing: scope cache per context, invalidate on reconnect
|
||||
- [ ] **Error handling** — Try/catch present, but missing: rollback state on failure, show user-friendly message, log to sentry
|
||||
|
||||
---
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
When pitfalls occur despite prevention, how to recover.
|
||||
|
||||
| Pitfall | Recovery Cost | Recovery Steps |
|
||||
|---------|---------------|----------------|
|
||||
| **Destructive switch lost state** | MEDIUM | Phase 2: Add persistent storage (localStorage) to survive restarts |
|
||||
| **Listener leaks** | LOW | Add dispose tracking, expose `debug.listenerCount()` IPC for diagnosis |
|
||||
| **Stale closure** | HIGH | Refactor service architecture to inject provider per call (breaking change) |
|
||||
| **IPC race condition** | MEDIUM | Add context ID retroactively, reject responses with mismatched ID |
|
||||
| **Timer leak** | LOW | DevTools audit: setInterval/clearInterval coverage, add cleanup tests |
|
||||
| **SSH connection leak** | LOW | Add connection pool with max limit, force destroy after 30s timeout |
|
||||
| **Tab validation missing** | MEDIUM | Add migration: filter invalid tabs on app startup |
|
||||
| **Partial snapshot** | HIGH | Ship hotfix: capture full state, deprecate manual field list |
|
||||
|
||||
---
|
||||
|
||||
## Pitfall-to-Phase Mapping
|
||||
|
||||
How roadmap phases should address these pitfalls.
|
||||
|
||||
| Pitfall | Prevention Phase | Verification |
|
||||
|---------|------------------|--------------|
|
||||
| Destructive context switching | Phase 1 | Unit test: switch → switch back → assert state restored |
|
||||
| EventEmitter listener accumulation | Phase 1 | Memory profiler: assert RSS stable after 10 switches |
|
||||
| Stale closures | Phase 1 | Integration test: mock provider swap, assert service calls new provider |
|
||||
| IPC race conditions | Phase 1 | E2E test: rapid connect/disconnect, assert no stale data |
|
||||
| FileWatcher polling timer | Phase 1 | Unit test: call stop(), assert pollingTimer cleared |
|
||||
| SSH connection disposal | Phase 1 | Integration test: assert socket.destroyed after disconnect |
|
||||
| Tab restoration validation | Phase 2 | E2E test: open tabs → switch SSH → assert only valid tabs restored |
|
||||
| Partial snapshot | Phase 1 | Integration test: snapshot → restore → assert detail populated |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Official Documentation
|
||||
- [Electron IPC Documentation](https://www.electronjs.org/docs/latest/tutorial/ipc)
|
||||
- [Electron ipcRenderer API](https://www.electronjs.org/docs/api/ipc-renderer)
|
||||
- [Node.js Events API](https://nodejs.org/api/events.html)
|
||||
|
||||
### Electron State Management
|
||||
- [Syncing State between Electron Contexts - Bruno Scheufler](https://brunoscheufler.com/blog/2023-10-29-syncing-state-between-electron-contexts)
|
||||
- [Advanced Electron.js architecture - LogRocket Blog](https://blog.logrocket.com/advanced-electron-js-architecture/)
|
||||
- [Zutron: Streamlined Electron State Management](https://github.com/goosewobbler/zutron)
|
||||
- [Notes on Electron Processes, Context Isolation, and IPC](https://abstractentropy.com/notes-on-electron/)
|
||||
|
||||
### Memory Leaks and Cleanup
|
||||
- [Diagnosing and Fixing Memory Leaks in Electron Applications](https://www.mindfulchase.com/explore/troubleshooting-tips/frameworks-and-libraries/diagnosing-and-fixing-memory-leaks-in-electron-applications.html)
|
||||
- [Viacheslav Eremin | Memory Leaks in Electron application](https://www.vb-net.com/AngularElectron/MemoryLeaks.htm)
|
||||
- [Error: Removing all listeners from ipcRenderer](https://github.com/electron/electron/issues/10379)
|
||||
- [IPC in Electron - Ray](https://myray.app/blog/ipc-in-electron)
|
||||
|
||||
### React Stale Closures
|
||||
- [Be Aware of Stale Closures when Using React Hooks](https://dmitripavlutin.com/react-hooks-stale-closures/)
|
||||
- [How to Fix "Stale Closure" Issues in React Hooks](https://oneuptime.com/blog/post/2026-01-24-fix-stale-closure-issues-react-hooks/view)
|
||||
- [React Stale Closure: Common Problems and Easy Solutions](https://www.dhiwise.com/post/react-stale-closure-common-problems-and-easy-solutions)
|
||||
- [Hooks, Dependencies and Stale Closures | TkDodo's blog](https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures)
|
||||
|
||||
### Zustand and State Management
|
||||
- [Zustand - react state management made easy](https://graphqleditor.com/blog/zustand/)
|
||||
- [Stores with React Context: Memory Leak Issue or Redux DevTools Bug?](https://github.com/pmndrs/zustand/discussions/2540)
|
||||
- [In an ESModule, will not cleaning up subscriber result in a memory leak?](https://github.com/pmndrs/zustand/discussions/2054)
|
||||
|
||||
### VS Code Multi-Root Workspaces (Reference Architecture)
|
||||
- [Adopting Multi Root Workspace APIs](https://github.com/microsoft/vscode/wiki/Adopting-Multi-Root-Workspace-APIs)
|
||||
- [Multi-Root Workspaces in VS Code](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces)
|
||||
- [Workspace Management: Multi-project VS Code Setup](https://www.mikul.me/blog/workspace-management-multi-project-vscode-setup)
|
||||
|
||||
### Dependency Injection and Service Lifecycle
|
||||
- [Dependency injection guidelines - .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/guidelines)
|
||||
- [Service lifetimes (dependency injection) - .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes)
|
||||
- [TSyringe: Lightweight dependency injection for TypeScript](https://github.com/microsoft/tsyringe)
|
||||
|
||||
---
|
||||
|
||||
*Pitfalls research for: Multi-context workspace switching in Electron + Zustand*
|
||||
*Researched: 2026-02-12*
|
||||
357
.planning/research/STACK.md
Normal file
357
.planning/research/STACK.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# Technology Stack: Multi-Context Workspace Management
|
||||
|
||||
**Domain:** Electron desktop app with SSH remote + local context switching
|
||||
**Researched:** 2026-02-12
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Recommended Stack
|
||||
|
||||
### Core State Management Pattern
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| **Zustand persist middleware** | 4.x | State snapshot/restoration | Built-in hydration control (`skipHydration`, `rehydrate`), selective persistence via `partialize`, storage backend abstraction. Already in use. |
|
||||
| **Context isolation per workspace** | Pattern | Independent workspace state | Each workspace (local + N SSH hosts) gets own Zustand store instance, snapshotted to storage, restored on context switch. |
|
||||
| **Broadcast Channel API** | Native | Multi-window state sync | Native browser API for same-origin window messaging. VS Code Remote and Akiflow use this. Zero dependencies, straightforward implementation. |
|
||||
| **Workspace registry in main process** | Pattern | Service lifecycle management | Main process owns workspace registry mapping `contextId → { store snapshot, service instances, connection state }`. IPC triggers context switches. |
|
||||
|
||||
### IPC Architecture
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| **ipcMain.handle / ipcRenderer.invoke** | Electron 28.x+ | Request/response IPC | Official Electron recommendation for async two-way IPC. Already in use. Type-safe with preload bridge. |
|
||||
| **Workspace-scoped IPC channels** | Pattern | Context-aware requests | Prefix channels with contextId: `workspace:${contextId}:getSessions`. Main process routes to correct service instance. |
|
||||
| **EventEmitter for status broadcasts** | Node.js native | Connection state events | Already used by SshConnectionManager. Extend pattern for workspace lifecycle events. |
|
||||
|
||||
### State Persistence
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| **Zustand persist → localStorage** | 4.x | Active workspace state | Fast synchronous access. Already configured. Each workspace gets namespaced key: `claude-devtools-workspace-${contextId}`. |
|
||||
| **Zustand persist → IndexedDB** | 4.x (via idb-keyval) | Inactive workspace snapshots | Async storage for multiple workspace snapshots without localStorage quota limits. Zustand persist middleware supports custom storage backends. |
|
||||
| **partialize for selective persistence** | Zustand built-in | Minimize storage overhead | Persist only domain data (projects, sessions, tabs), exclude transient UI state (loading flags, scroll positions). |
|
||||
|
||||
### Service Registry Pattern (Main Process)
|
||||
|
||||
| Component | Purpose | Implementation |
|
||||
|-----------|---------|----------------|
|
||||
| **WorkspaceRegistry** | Owns all workspace instances | `Map<contextId, WorkspaceContext>` where `WorkspaceContext = { provider: FileSystemProvider, services: ServiceInstances, lastAccessed: timestamp }` |
|
||||
| **ServiceInstances** | Per-workspace service lifecycle | Each workspace gets own instances of ProjectScanner, SessionParser, FileWatcher, etc. Services use workspace-specific FileSystemProvider. |
|
||||
| **Active workspace tracking** | Single active context at a time | `activeContextId: string`. IPC handlers route to `registry.get(activeContextId).services`. |
|
||||
| **Lazy initialization** | Services created on first use | Registry initializes workspace services on connect (SSH) or app launch (local). Disposes inactive workspaces after TTL (e.g., 30min). |
|
||||
|
||||
### Context Switching Flow
|
||||
|
||||
| Phase | Mechanism | Rationale |
|
||||
|-------|-----------|-----------|
|
||||
| **1. Save current state** | `zustand.getState()` → IndexedDB | Snapshot entire store before switch. Non-blocking async operation. |
|
||||
| **2. Broadcast workspace change** | `new BroadcastChannel('workspace-switch').postMessage({ contextId })` | Notify all renderer windows to update their UI for new context. |
|
||||
| **3. Switch main process context** | `WorkspaceRegistry.setActive(contextId)` | Atomic switch of active FileSystemProvider + service instances. |
|
||||
| **4. Restore target state** | IndexedDB → `zustand.setState()` | Hydrate store from snapshot. If no snapshot (new workspace), use empty initial state. |
|
||||
| **5. Re-fetch live data** | `fetchProjects()`, `fetchRepositoryGroups()` | Refresh data from new context's file system. Already implemented in connectionSlice. |
|
||||
|
||||
## What NOT to Use
|
||||
|
||||
| Avoid | Why | Use Instead |
|
||||
|-------|-----|-------------|
|
||||
| **Zutron library** | Designed for syncing single store across main+renderer. We need multiple independent workspace stores. Adds unnecessary abstraction. | Direct Zustand persist + IPC for workspace switching. |
|
||||
| **Redux/Redux Toolkit** | Overkill for context switching. Boilerplate-heavy. Zustand already in use and handles this elegantly with persist middleware. | Zustand with workspace-scoped stores. |
|
||||
| **Global store with workspace slice** | Single store with `workspaces: { [id]: WorkspaceState }` scales poorly with large states, makes persistence complex, increases re-render surface. | Separate Zustand store instance per workspace. |
|
||||
| **SharedWorker for multi-window** | More complex than BroadcastChannel, requires separate worker script, no tangible benefit for workspace switching use case. | BroadcastChannel API. |
|
||||
| **Electron IPC for multi-window sync** | Round-trip through main process for window-to-window messaging is slower and more complex than BroadcastChannel. | BroadcastChannel for renderer-to-renderer, IPC for renderer-to-main. |
|
||||
|
||||
## Stack Patterns by Variant
|
||||
|
||||
### Pattern 1: Workspace Store Initialization
|
||||
|
||||
**When:** User switches to a workspace (SSH connect or local mode)
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Renderer: Create workspace-scoped store
|
||||
const createWorkspaceStore = (contextId: string) => {
|
||||
return create<AppState>()(
|
||||
persist(
|
||||
(...args) => ({
|
||||
...createProjectSlice(...args),
|
||||
...createSessionSlice(...args),
|
||||
// ... other slices
|
||||
}),
|
||||
{
|
||||
name: `claude-devtools-workspace-${contextId}`,
|
||||
storage: createJSONStorage(() => indexedDBStorage), // For inactive
|
||||
partialize: (state) => ({
|
||||
// Only persist domain data, not UI state
|
||||
projects: state.projects,
|
||||
sessions: state.sessions,
|
||||
tabs: state.tabs,
|
||||
// Exclude: loading, error, selectedIds
|
||||
}),
|
||||
skipHydration: true, // Manual hydration control
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// After switch, manually rehydrate
|
||||
await store.persist.rehydrate();
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- `skipHydration: true` prevents auto-load on store creation, gives control over when to restore state
|
||||
- `partialize` minimizes storage size, excludes transient state that shouldn't survive context switch
|
||||
- IndexedDB storage for inactive workspaces avoids localStorage 5MB quota issues
|
||||
|
||||
### Pattern 2: Main Process Service Registry
|
||||
|
||||
**When:** Routing IPC calls to workspace-specific services
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Main process
|
||||
class WorkspaceRegistry {
|
||||
private workspaces = new Map<string, WorkspaceContext>();
|
||||
private activeContextId: string = 'local';
|
||||
|
||||
async initializeWorkspace(contextId: string, provider: FileSystemProvider) {
|
||||
const context: WorkspaceContext = {
|
||||
provider,
|
||||
services: {
|
||||
projectScanner: new ProjectScanner(provider),
|
||||
sessionParser: new SessionParser(provider),
|
||||
fileWatcher: new FileWatcher(provider),
|
||||
// ... other services
|
||||
},
|
||||
lastAccessed: Date.now(),
|
||||
};
|
||||
this.workspaces.set(contextId, context);
|
||||
}
|
||||
|
||||
getActiveServices(): ServiceInstances {
|
||||
const context = this.workspaces.get(this.activeContextId);
|
||||
if (!context) throw new Error('No active workspace');
|
||||
return context.services;
|
||||
}
|
||||
|
||||
setActive(contextId: string) {
|
||||
this.activeContextId = contextId;
|
||||
}
|
||||
}
|
||||
|
||||
// IPC handler uses active workspace
|
||||
ipcMain.handle('getProjects', async () => {
|
||||
const services = workspaceRegistry.getActiveServices();
|
||||
return services.projectScanner.scan();
|
||||
});
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Services use FileSystemProvider interface → same code works for local + SSH
|
||||
- Main process owns service lifecycle → renderer just calls IPC, doesn't manage connections
|
||||
- Active workspace pattern → single source of truth for which context is current
|
||||
|
||||
### Pattern 3: Context Switch with State Preservation
|
||||
|
||||
**When:** User clicks "Switch to SSH" or "Switch to Local"
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Renderer
|
||||
async function switchWorkspace(newContextId: string) {
|
||||
const currentStore = useStore.getState();
|
||||
|
||||
// 1. Save current workspace state to IndexedDB
|
||||
const currentSnapshot = currentStore;
|
||||
await saveWorkspaceSnapshot(currentContextId, currentSnapshot);
|
||||
|
||||
// 2. Broadcast switch event to other windows
|
||||
const channel = new BroadcastChannel('workspace-switch');
|
||||
channel.postMessage({ contextId: newContextId });
|
||||
|
||||
// 3. Tell main process to switch active context
|
||||
await window.electronAPI.workspace.setActive(newContextId);
|
||||
|
||||
// 4. Restore target workspace state from IndexedDB
|
||||
const targetSnapshot = await loadWorkspaceSnapshot(newContextId);
|
||||
if (targetSnapshot) {
|
||||
useStore.setState(targetSnapshot);
|
||||
} else {
|
||||
// New workspace: reset to initial state
|
||||
useStore.setState(getInitialState());
|
||||
}
|
||||
|
||||
// 5. Re-fetch live data from new context
|
||||
await currentStore.fetchProjects();
|
||||
await currentStore.fetchRepositoryGroups();
|
||||
}
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- State snapshot before switch → instant restoration when switching back
|
||||
- BroadcastChannel → multi-window apps stay in sync
|
||||
- Main process switch → IPC handlers route to correct services
|
||||
- Re-fetch after restore → fresh data from new filesystem
|
||||
|
||||
### Pattern 4: Multi-Window Sync with BroadcastChannel
|
||||
|
||||
**When:** User has multiple app windows open, switches workspace in one window
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Each renderer window listens
|
||||
const channel = new BroadcastChannel('workspace-switch');
|
||||
channel.onmessage = async (event) => {
|
||||
const { contextId } = event.data;
|
||||
|
||||
// Don't process if this window initiated the switch
|
||||
if (contextId === currentContextId) return;
|
||||
|
||||
// Update this window's state to match
|
||||
await switchWorkspace(contextId);
|
||||
};
|
||||
|
||||
// Send when switching
|
||||
channel.postMessage({ contextId: newContextId });
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Native API, no library needed
|
||||
- Same-origin by default → secure
|
||||
- Simpler than IPC round-trip for window-to-window messaging
|
||||
- VS Code Remote and Akiflow use this pattern
|
||||
|
||||
## Installation
|
||||
|
||||
### Core Dependencies (Already Installed)
|
||||
```bash
|
||||
# Already in package.json
|
||||
zustand@4.x
|
||||
electron@28.x
|
||||
ssh2@latest # For SSH connections
|
||||
```
|
||||
|
||||
### Additional Dependencies
|
||||
```bash
|
||||
# For IndexedDB storage backend
|
||||
pnpm install idb-keyval
|
||||
|
||||
# No other dependencies needed - BroadcastChannel is native
|
||||
```
|
||||
|
||||
## Architecture Best Practices
|
||||
|
||||
### Memory Management
|
||||
- **Dispose inactive workspaces**: After 30min of inactivity, dispose service instances and FileSystemProvider. Keep snapshot in IndexedDB for instant restoration.
|
||||
- **Limit snapshot count**: Keep max 5 workspace snapshots in IndexedDB. LRU eviction by `lastAccessed` timestamp.
|
||||
- **Clear transient state on switch**: Don't persist loading flags, error messages, scroll positions → they're meaningless in restored context.
|
||||
|
||||
### Error Handling
|
||||
- **Connection errors**: If SSH disconnects mid-session, auto-switch to local mode, preserve SSH workspace snapshot for reconnect.
|
||||
- **Storage quota**: IndexedDB quota errors → warn user, fall back to localStorage for active workspace only, disable snapshot restoration.
|
||||
- **Service initialization failures**: If workspace service init fails (e.g., remote path doesn't exist), show error banner but don't crash app. Allow manual retry.
|
||||
|
||||
### Performance Optimization
|
||||
- **Debounced persistence**: Don't save snapshot on every state change. Debounce by 2-5 seconds, trigger on workspace blur.
|
||||
- **Lazy service initialization**: Don't create all services on workspace init. Create ProjectScanner immediately, defer SessionParser until first session view.
|
||||
- **Partial state updates**: When re-fetching after context switch, use `refreshSessionsInPlace` pattern (already implemented) to avoid flickering.
|
||||
|
||||
## TypeScript Patterns
|
||||
|
||||
### Workspace Context Type
|
||||
```typescript
|
||||
type WorkspaceContextId = `local` | `ssh-${string}`;
|
||||
|
||||
interface WorkspaceContext {
|
||||
id: WorkspaceContextId;
|
||||
provider: FileSystemProvider;
|
||||
services: ServiceInstances;
|
||||
lastAccessed: number;
|
||||
connectionInfo?: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ServiceInstances {
|
||||
projectScanner: ProjectScanner;
|
||||
sessionParser: SessionParser;
|
||||
fileWatcher: FileWatcher;
|
||||
// ... other services
|
||||
}
|
||||
```
|
||||
|
||||
### Storage Backend Interface
|
||||
```typescript
|
||||
interface WorkspaceStorage {
|
||||
getItem(key: string): Promise<string | null>;
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
removeItem(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
// IndexedDB implementation
|
||||
const indexedDBStorage: WorkspaceStorage = {
|
||||
getItem: async (key) => {
|
||||
const value = await idbKeyval.get(key);
|
||||
return value ?? null;
|
||||
},
|
||||
setItem: async (key, value) => {
|
||||
await idbKeyval.set(key, value);
|
||||
},
|
||||
removeItem: async (key) => {
|
||||
await idbKeyval.del(key);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
| Package | Version | Compatible With | Notes |
|
||||
|---------|---------|-----------------|-------|
|
||||
| zustand | 4.x | persist middleware 4.x | Persist middleware ships with zustand 4.x, no separate install |
|
||||
| electron | 28.x+ | BroadcastChannel native | BroadcastChannel available in Chromium 54+ (Electron 1.x+) |
|
||||
| idb-keyval | 6.x | TypeScript 5.x | Simple IndexedDB wrapper, 600 bytes minified |
|
||||
| ssh2 | Latest | Node.js 18+ | Already in use for SSH connections |
|
||||
|
||||
## Confidence Levels
|
||||
|
||||
| Area | Confidence | Rationale |
|
||||
|------|------------|-----------|
|
||||
| **Zustand persist for state snapshotting** | HIGH | Official Zustand middleware, documented patterns for selective persistence and hydration control. Production-proven. |
|
||||
| **BroadcastChannel for multi-window** | HIGH | Native browser API, used by VS Code Remote, Akiflow, and other Electron apps. Well-documented, zero dependencies. |
|
||||
| **Service registry in main process** | HIGH | Standard Electron pattern for managing per-context resources. FileSystemProvider interface already supports this (SshFileSystemProvider vs LocalFileSystemProvider). |
|
||||
| **IndexedDB for inactive snapshots** | MEDIUM | Zustand persist supports custom storage, idb-keyval is battle-tested, but no direct examples of multi-workspace Electron apps using this exact pattern. Low risk. |
|
||||
| **Workspace TTL and eviction** | MEDIUM | Pattern is sound, but optimal TTL (30min) and max snapshots (5) are heuristics. May need tuning based on real-world usage. |
|
||||
|
||||
## Sources
|
||||
|
||||
### State Management
|
||||
- [React State Management in 2025: Zustand vs. Redux vs. Jotai vs. Context](https://www.meerako.com/blogs/react-state-management-zustand-vs-redux-vs-context-2025)
|
||||
- [React State Management 2025: Redux,Context, Recoil & Zustand](https://www.zignuts.com/blog/react-state-management-2025)
|
||||
- [Zustand persist middleware documentation](https://zustand.docs.pmnd.rs/middlewares/persist)
|
||||
- [Zustand persist - partialize option discussion](https://github.com/pmndrs/zustand/discussions/1273)
|
||||
|
||||
### Electron Multi-Window & IPC
|
||||
- [Multiple Windows in Electron apps (2025)](https://blog.bloomca.me/2025/07/21/multi-window-in-electron.html)
|
||||
- [Creating multi-window Electron apps using React portals](https://pietrasiak.com/creating-multi-window-electron-apps-using-react-portals)
|
||||
- [Advanced Electron.js architecture - LogRocket Blog](https://blog.logrocket.com/advanced-electron-js-architecture/)
|
||||
- [Electron IPC documentation](https://www.electronjs.org/docs/latest/tutorial/ipc)
|
||||
- [Electron – 3 Methods for Inter Process Communications (IPC)](https://www.intertech.com/electron-3-methods-for-inter-process-communications-ipc/)
|
||||
|
||||
### BroadcastChannel Pattern
|
||||
- [Multi-Window Messaging In Akiflow](https://akiflow.com/blog/multi-window-messaging-in-akiflow)
|
||||
- [Creating a synchronized store between main and renderer process in Electron](https://www.bigbinary.com/blog/sync-store-main-renderer-electron)
|
||||
- [BroadcastChannel API - 12 Days of Web](https://12daysofweb.dev/2024/broadcastchannel-api/)
|
||||
|
||||
### VS Code Architecture
|
||||
- [Supporting Remote Development and GitHub Codespaces | Visual Studio Code Extension API](https://code.visualstudio.com/api/advanced-topics/remote-extensions)
|
||||
- [VS Code 1.107 (November 2025 Update) Expands Multi-Agent Orchestration](https://visualstudiomagazine.com/articles/2025/12/12/vs-code-1-107-november-2025-update-expands-multi-agent-orchestration-model-management.aspx)
|
||||
- [Behind the feature: building multi-account | Figma Blog](https://www.figma.com/blog/behind-the-feature-building-multi-account/)
|
||||
|
||||
### Electron State Management Libraries
|
||||
- [Zutron: Streamlined Electron State Management](https://github.com/goosewobbler/zutron)
|
||||
- [Syncing State between Electron Contexts - Bruno Scheufler](https://brunoscheufler.com/blog/2023-10-29-syncing-state-between-electron-contexts)
|
||||
|
||||
---
|
||||
*Stack research for: Multi-context workspace management in Electron + Zustand*
|
||||
*Researched: 2026-02-12*
|
||||
320
.planning/research/SUMMARY.md
Normal file
320
.planning/research/SUMMARY.md
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
# Project Research Summary
|
||||
|
||||
**Project:** Multi-Context Workspace Management for claude-devtools
|
||||
**Domain:** Electron desktop application with SSH remote + local context switching
|
||||
**Researched:** 2026-02-12
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Multi-context workspace switching in Electron desktop applications requires a careful balance of instant switching, comprehensive state preservation, and robust resource lifecycle management. Research across VS Code Remote, JetBrains Gateway, Slack, and Notion reveals that users demand sub-second context switches with zero data loss and continuous connection awareness. The recommended approach centers on three architectural pillars: (1) a ServiceContextRegistry pattern to maintain separate service instances per context, avoiding expensive teardown/recreation; (2) Zustand state snapshots with IndexedDB persistence for instant restoration; (3) BroadcastChannel API for multi-window synchronization, eliminating the complexity of IPC-based window coordination.
|
||||
|
||||
The critical insight from research is that "destructive switching" (clearing all state on context change) is the primary UX killer. Users switching between local and SSH contexts expect their open sessions, selected projects, tab layout, and scroll positions to be preserved per context. VS Code Remote and Slack demonstrate that this is achievable through snapshot-based state management with context validation—when switching back to SSH, the app should restore exactly where the user left off, not force re-navigation. The technology stack already in place (Zustand 4.x, Electron 28.x, ssh2) fully supports this pattern through persist middleware with selective hydration control and workspace-scoped storage keys.
|
||||
|
||||
The most dangerous pitfall identified is the combination of EventEmitter listener accumulation and stale closures capturing old FileSystemProvider references. After 5-10 context switches, memory leaks from orphaned listeners (FileWatcher polling timers, SSH connections, IPC subscriptions) can consume 50-100MB per switch while services continue calling methods on the wrong provider. Prevention requires explicit lifecycle management: every service must implement a `dispose()` method that clears timers, removes listeners, and closes connections, coupled with late-binding provider injection (getter pattern instead of constructor caching) to avoid closure staleness. The roadmap must address these lifecycle issues in Phase 1 before any user-facing features, as retrofitting proper cleanup after discovering production memory leaks is expensive and risky.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
Research confirms that the existing stack (Zustand 4.x, Electron 28.x, ssh2) is well-suited for multi-context workspace management. The key addition is leveraging Zustand's persist middleware with IndexedDB storage (via idb-keyval) for inactive workspace snapshots, combined with BroadcastChannel API for multi-window synchronization. The ServiceContextRegistry pattern provides the architectural backbone—a main-process registry that owns service instances per context (local + N SSH hosts), eliminating expensive service recreation on every switch.
|
||||
|
||||
**Core technologies:**
|
||||
- **Zustand persist middleware**: State snapshot/restoration per workspace with selective hydration control (`skipHydration`, `rehydrate`, `partialize`)—already in use, no new dependencies
|
||||
- **ServiceContextRegistry pattern**: Map of contextId → ServiceContext (FileSystemProvider + service instances), provides `getActive()`, `switch()`, lifecycle management
|
||||
- **BroadcastChannel API**: Native browser API for same-origin window messaging, used by VS Code Remote and Akiflow for multi-window state sync—zero dependencies
|
||||
- **IndexedDB via idb-keyval**: Async storage for inactive workspace snapshots, avoids localStorage 5MB quota limits—only new dependency (600 bytes minified)
|
||||
- **IPC workspace-scoped channels**: Prefix channels with contextId (`workspace:${contextId}:getSessions`) for context-aware request routing through main process registry
|
||||
|
||||
**Critical version requirements:**
|
||||
- Zustand 4.x includes persist middleware (no separate install)
|
||||
- Electron 28.x+ includes BroadcastChannel (native in Chromium 54+)
|
||||
- idb-keyval 6.x compatible with TypeScript 5.x
|
||||
|
||||
**What NOT to use:**
|
||||
- Zutron library (designed for syncing single store across main+renderer, not multiple independent workspace stores)
|
||||
- Redux/Redux Toolkit (overkill for context switching, unnecessary boilerplate)
|
||||
- Global store with workspace slice pattern (scales poorly, makes persistence complex)
|
||||
|
||||
### Expected Features
|
||||
|
||||
Multi-context applications have a clear division between table stakes (users will leave if missing) and differentiators (competitive advantage but not required). Research across VS Code Remote, JetBrains Gateway, Slack, Notion, Discord, and Figma establishes the feature baseline.
|
||||
|
||||
**Must have (table stakes):**
|
||||
- **Visual workspace list with status** — users expect to see available workspaces at a glance (dropdown, sidebar, or command palette)
|
||||
- **Saved connection profiles** — users refuse to re-enter SSH details (host, port, username) repeatedly
|
||||
- **Recent connections list** — 80%+ of switches go to recent contexts (default 5-10 most recent)
|
||||
- **Current workspace indicator** — users need persistent "where am I?" awareness (status bar or title bar badge)
|
||||
- **Per-workspace state preservation** — context loss on switch = immediate frustration (open files, selections, scroll position, tabs, UI state)
|
||||
- **Connection status indicators** — real-time online/connecting/offline/error states with distinct visual treatment
|
||||
- **Loading indicators during switch** — delays without feedback feel like freezes (skeleton states or spinners)
|
||||
- **Basic error handling** — clear messages with actionable guidance, not raw exceptions
|
||||
- **Keyboard shortcut for switcher** — power users demand keyboard-driven workflows (Ctrl/Cmd+K pattern)
|
||||
|
||||
**Should have (competitive):**
|
||||
- **Quick switcher with fuzzy search** — fastest method for 10+ workspaces (Cmd+K command palette pattern)
|
||||
- **Auto-reconnect on network restore** — brief network blips shouldn't require manual reconnection (exponential backoff, max 6-10 retries)
|
||||
- **Workspace color coding** — visual distinction reduces cognitive load (Discord/Slack pattern)
|
||||
- **Connection health metrics** — latency, stability indicators provide proactive awareness
|
||||
- **Activity notifications** — unread indicators per workspace, stay informed without switching
|
||||
- **Context preview on hover** — reduce cognitive load by showing context before full switch (Slack's tab preview pattern)
|
||||
|
||||
**Defer (v2+):**
|
||||
- **Parallel workspace windows** — advanced users want simultaneous multi-context view (HIGH complexity, resource isolation challenges)
|
||||
- **Offline-first with sync queue** — continue working during network issues (HIGH complexity, requires conflict resolution)
|
||||
- **Workspace groups/folders** — hierarchical organization only needed at 50+ workspaces
|
||||
- **Context-aware AI suggestions** — predict next workspace based on patterns (HIGH complexity, requires ML infrastructure)
|
||||
- **Workspace-specific keyboard shortcuts** — per-workspace customization (HIGH complexity, potential confusion)
|
||||
|
||||
**Explicitly avoid (anti-features):**
|
||||
- Automatic workspace switching (destroys mental model)
|
||||
- Unlimited parallel workspaces (resource exhaustion)
|
||||
- Real-time sync of all state (network overhead, conflict complexity)
|
||||
- Complex workspace hierarchies (cognitive overhead)
|
||||
- Cross-workspace clipboard sync (security issue)
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
The recommended architecture uses a ServiceContextRegistry pattern in the main process to manage multiple isolated service contexts, each with its own FileSystemProvider and service instances. This avoids expensive teardown/recreation on every switch and maintains separate caches per context. The renderer uses workspace-scoped Zustand stores with state snapshots captured in IndexedDB for instant restoration. IPC handlers are re-routed through the registry's `getActive()` method rather than using module-level service variables, eliminating the need for `reinitializeServiceHandlers()` on every switch.
|
||||
|
||||
**Major components:**
|
||||
1. **ServiceContextRegistry (main)** — Central registry managing multiple ServiceContext instances (local + N SSH), provides `getActive()`, `switch()`, `register()`, handles lifecycle (start/stop watchers, dispose inactive contexts)
|
||||
2. **ServiceContext (main)** — Encapsulates service instances for one context: ProjectScanner, SessionParser, SubagentResolver, DataCache, FileWatcher, plus FileSystemProvider reference—isolated state per context
|
||||
3. **ContextSwitcher (renderer)** — Orchestrates context switches: captures current state snapshot, calls IPC to switch, restores target state from IndexedDB, triggers background data refresh
|
||||
4. **StateSnapshot (renderer)** — Frozen copy of AppState per context (projects, sessions, selections, tabs, pane layout) with expiration timestamp (5-minute TTL)—instant restore if fresh, partial restore + re-fetch if stale
|
||||
5. **IPC Context Handlers** — Expose `getCurrentContext()`, `switchContext(contextId)`, `listContexts()`, `getContextSnapshot(contextId)` via preload bridge
|
||||
|
||||
**Key architectural decisions:**
|
||||
- Local context always stays alive (never disposed) for notifications and config updates
|
||||
- Only active context's FileWatcher runs; inactive watchers are paused to conserve resources
|
||||
- Snapshots stored with `contextId` + `expiresAt` to handle stale data (older than 5 minutes triggers background refresh)
|
||||
- Each ServiceContext has its own DataCache to prevent cache pollution (local data appearing in SSH mode)
|
||||
- BroadcastChannel syncs context switches across multiple app windows without IPC round-trips
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
Research identified eight critical pitfalls that must be prevented from Phase 1. The most dangerous ones combine to create subtle bugs that manifest only after repeated context switches.
|
||||
|
||||
1. **Destructive context switching with incomplete state snapshots** — Current implementation calls `getFullResetState()` on SSH connect/disconnect, wiping all selections, open tabs, and loaded data. When switching back, users must re-navigate to their project and re-open sessions. **Fix:** Capture full AppState snapshot before switching, store in `Map<contextId, AppState>`, restore on switch. Phase 1 critical.
|
||||
|
||||
2. **EventEmitter listener accumulation on repeated switches** — FileWatcher, SshConnectionManager use `.on()` without cleanup, causing memory leaks (50-100MB per switch) and duplicate event emissions. **Fix:** Every service implements `dispose()` method with `removeAllListeners(channelName)` (never blank `removeAllListeners()` which breaks Electron IPC), track cleanup functions in registry. Phase 1 critical.
|
||||
|
||||
3. **Stale closures capturing old FileSystemProvider references** — Zustand actions and service methods capture provider at definition time. After SSH connect swaps provider, operations still hit old LocalFileSystemProvider. **Fix:** Use getter pattern (`getProvider()` on every operation) instead of constructor caching, or re-initialize services after provider swap. Phase 1 critical.
|
||||
|
||||
4. **IPC race conditions during context switch with in-flight requests** — User clicks Connect → IPC starts scanning remote projects → user clicks Disconnect → scan completes and populates store with SSH data in local mode. **Fix:** Include `contextId` UUID in every IPC request/response, validate `if (currentContextId !== response.contextId) return` before applying. Use AbortController to cancel in-flight requests on switch. Phase 1 critical.
|
||||
|
||||
5. **FileWatcher polling timer not cleared on SSH disconnect** — SSH mode uses `setInterval()` polling (5s). If error occurs in `stop()`, `clearInterval()` never runs, leaving orphaned timers. After 10 switches, 10 timers consume CPU. **Fix:** Always clear timers in `finally` block, defensive clearing before creating new timer. Phase 1 critical.
|
||||
|
||||
6. **SSH connection not properly disposed, keeping socket open** — `client.end()` is graceful but if SFTP channel is processing, socket stays open. After 10 switches, 10 SSH connections consume file descriptors. **Fix:** Call `sftp.end()` explicitly before `client.end()`, set 5s timeout and force `client.destroy()` if graceful close fails. Phase 1 critical.
|
||||
|
||||
7. **Tab state restoration without context validation** — Snapshot captures tabs with `projectId: "abc-local-path"`, user switches to SSH where projects have different IDs, restoration tries to open tab with local projectId → "Project not found" error. **Fix:** Validate restored tabs against `window.electronAPI.getProjects()`, close invalid tabs with user notification. Phase 2 priority.
|
||||
|
||||
8. **Partial snapshot creates inconsistent derived state** — Snapshot captures `selectedProjectId` but omits `sessionDetail`/`conversation`/`chunks`, restoration sets selection without data → UI renders empty or crashes on null access. **Fix:** Snapshot entire AppState except ephemeral flags (`loading`, `error`), restore atomically via `setState(() => snapshot)`. Phase 1 critical.
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Based on combined research, the roadmap must prioritize infrastructure over features. Six of eight critical pitfalls must be resolved in Phase 1 before any user-facing context switching is exposed—memory leaks, stale closures, and race conditions will cause production incidents if not addressed foundationally.
|
||||
|
||||
### Phase 1: Core Infrastructure (Foundation)
|
||||
**Rationale:** Main process architecture and lifecycle management must be bulletproof before renderer integration. Memory leaks, stale closures, and race conditions cannot be retrofitted after user-facing features ship.
|
||||
|
||||
**Delivers:**
|
||||
- ServiceContextRegistry in main process managing multiple ServiceContext instances
|
||||
- IPC context API (getCurrentContext, switchContext, listContexts)
|
||||
- Service lifecycle management: dispose() methods, EventEmitter cleanup, timer cleanup
|
||||
- StateSnapshot system with capture/restore in renderer
|
||||
- Context ID stamping for IPC request/response validation
|
||||
|
||||
**Addresses pitfalls:**
|
||||
- #1 Destructive switching (snapshot system)
|
||||
- #2 Listener accumulation (dispose methods)
|
||||
- #3 Stale closures (getActive() pattern)
|
||||
- #4 IPC race conditions (contextId stamping)
|
||||
- #5 Timer leaks (finally block cleanup)
|
||||
- #6 SSH connection leaks (explicit sftp.end() + timeout)
|
||||
- #8 Partial snapshots (full AppState capture)
|
||||
|
||||
**Key features from FEATURES.md:**
|
||||
- Per-workspace state preservation (table stakes)
|
||||
- Saved connection profiles (table stakes)
|
||||
|
||||
**Build order:**
|
||||
1. ServiceContext.ts + ServiceContextRegistry.ts (main)
|
||||
2. IPC context handlers + preload API
|
||||
3. stateSnapshot.ts + contextSlice.ts (renderer)
|
||||
4. Service dispose() methods + cleanup tracking
|
||||
|
||||
**Research flag:** Standard patterns—ServiceContextRegistry uses established DI container patterns, Zustand persist is well-documented. No additional research needed.
|
||||
|
||||
### Phase 2: Basic UI Integration (MVP)
|
||||
**Rationale:** With infrastructure stable, add minimal UI to expose context switching to users. Focus on table stakes features that users expect.
|
||||
|
||||
**Delivers:**
|
||||
- ContextSwitcher UI component (dropdown or sidebar)
|
||||
- Current workspace indicator in title bar/status bar
|
||||
- Connection status indicators (online/connecting/offline/error)
|
||||
- Loading states during switch
|
||||
- Basic error handling with user-friendly messages
|
||||
- Keyboard shortcut (Cmd/Ctrl+K) to open switcher
|
||||
|
||||
**Uses stack from STACK.md:**
|
||||
- Zustand persist with IndexedDB (idb-keyval)
|
||||
- BroadcastChannel for multi-window sync
|
||||
|
||||
**Implements architecture from ARCHITECTURE.md:**
|
||||
- ContextSwitcher component
|
||||
- useContextSwitch hook
|
||||
|
||||
**Addresses pitfall:**
|
||||
- #7 Tab validation (validate restored tabs, close invalid)
|
||||
|
||||
**Key features from FEATURES.md:**
|
||||
- Visual workspace list with status (table stakes)
|
||||
- Recent connections list (table stakes)
|
||||
- Current workspace indicator (table stakes)
|
||||
- Connection status indicators (table stakes)
|
||||
- Loading indicators (table stakes)
|
||||
- Basic error handling (table stakes)
|
||||
- Keyboard shortcut for switcher (table stakes)
|
||||
|
||||
**Research flag:** Standard patterns—workspace switcher UI follows VS Code/Slack patterns. Status indicators use established color-coding (green/yellow/red). No additional research needed.
|
||||
|
||||
### Phase 3: Enhanced UX (v1.x)
|
||||
**Rationale:** After validating core switching works, add features that significantly improve UX based on user feedback and analytics. Only add when usage data shows need (10+ workspaces, frequent network blips).
|
||||
|
||||
**Delivers:**
|
||||
- Quick switcher with fuzzy search (command palette pattern)
|
||||
- Auto-reconnect with exponential backoff
|
||||
- Workspace color coding for visual distinction
|
||||
- Connection health metrics (latency, stability)
|
||||
- Activity notifications (unread indicators)
|
||||
- Context preview on hover
|
||||
|
||||
**Key features from FEATURES.md:**
|
||||
- Quick switcher with fuzzy search (competitive)
|
||||
- Auto-reconnect (competitive)
|
||||
- Workspace color coding (competitive)
|
||||
- Connection health metrics (competitive)
|
||||
- Activity notifications (competitive)
|
||||
- Context preview on hover (competitive)
|
||||
|
||||
**Research flag:** **Needs phase research** for fuzzy search algorithm (Fuse.js vs native), auto-reconnect retry strategies (exponential backoff parameters), and connection health monitoring implementation (latency measurement techniques).
|
||||
|
||||
### Phase 4: Advanced Capabilities (v2+)
|
||||
**Rationale:** Defer high-complexity features until product-market fit is established and usage data justifies investment. Parallel windows and offline-first require significant architectural work and resource isolation.
|
||||
|
||||
**Delivers:**
|
||||
- Parallel workspace windows (2-3 simultaneous)
|
||||
- Offline-first with sync queue
|
||||
- Workspace groups/folders (50+ workspace scale)
|
||||
- Context-aware AI workspace suggestions
|
||||
- Workspace-specific keyboard shortcuts
|
||||
|
||||
**Key features from FEATURES.md:**
|
||||
- Parallel workspace windows (deferred)
|
||||
- Offline-first with sync queue (deferred)
|
||||
- Workspace groups (deferred)
|
||||
- AI suggestions (deferred)
|
||||
- Workspace-specific shortcuts (deferred)
|
||||
|
||||
**Research flag:** **Needs phase research** for parallel windows (resource isolation techniques, Chromium multi-process architecture), offline-first (conflict resolution strategies, operational transformation patterns), and AI suggestions (pattern recognition approaches, feature engineering for workspace switching).
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
**Why Phase 1 before UI:**
|
||||
- Memory leaks discovered in production are expensive to fix and damage user trust
|
||||
- Stale closures cause subtle bugs that only appear after repeated switches (hard to debug)
|
||||
- IPC race conditions lead to data corruption if not prevented architecturally
|
||||
- ServiceContextRegistry is foundational—all subsequent phases depend on it
|
||||
|
||||
**Why Phase 2 focused on table stakes:**
|
||||
- Users will abandon app if basic switching doesn't work smoothly
|
||||
- Visual workspace list, status indicators, and error handling are MVP requirements
|
||||
- Keyboard shortcuts are expected by power users (primary demographic)
|
||||
- State preservation makes or breaks the UX—users switching contexts expect zero data loss
|
||||
|
||||
**Why Phase 3 deferred until validation:**
|
||||
- Fuzzy search only matters at 10+ workspaces (analytics will show when threshold hits)
|
||||
- Auto-reconnect can be implemented after observing real-world network patterns
|
||||
- Color coding is nice-to-have, not essential for functionality
|
||||
- Connection health metrics require telemetry infrastructure
|
||||
|
||||
**Why Phase 4 is v2+:**
|
||||
- Parallel windows require Chromium multi-process expertise (HIGH complexity)
|
||||
- Offline-first needs conflict resolution (HIGH complexity, many edge cases)
|
||||
- Workspace groups only needed at significant scale (50+ workspaces)
|
||||
- AI suggestions require ML infrastructure and training data
|
||||
|
||||
**Dependency chain:**
|
||||
```
|
||||
ServiceContextRegistry → IPC Context API → StateSnapshot → ContextSwitcher UI
|
||||
↓
|
||||
Service disposal → Listener cleanup → Timer cleanup
|
||||
↓
|
||||
Provider injection pattern → Stale closure prevention
|
||||
↓
|
||||
Context ID stamping → Race condition prevention
|
||||
```
|
||||
|
||||
### Research Flags
|
||||
|
||||
**Phases needing deeper research during planning:**
|
||||
- **Phase 3** — Fuzzy search algorithm selection, auto-reconnect retry strategies, connection health monitoring
|
||||
- **Phase 4** — Parallel window resource isolation, offline-first conflict resolution, AI pattern recognition
|
||||
|
||||
**Phases with standard patterns (skip research-phase):**
|
||||
- **Phase 1** — ServiceContextRegistry uses established DI patterns, Zustand persist is documented, EventEmitter cleanup is standard Node.js
|
||||
- **Phase 2** — Workspace switcher UI follows VS Code/Slack patterns, status indicators use established conventions
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | Zustand persist middleware well-documented with official examples, BroadcastChannel used by VS Code Remote and Akiflow, ServiceContextRegistry pattern proven in Microsoft TSyringe and node-dependency-injection |
|
||||
| Features | HIGH | Feature baseline verified across 6 major applications (VS Code, JetBrains, Slack, Notion, Discord, Figma) with consistent patterns, table stakes vs differentiators clearly delineated |
|
||||
| Architecture | MEDIUM | ServiceContextRegistry pattern is sound and used in production systems, but no direct examples of multi-workspace Electron apps using this exact combination (Zustand + ServiceContext + BroadcastChannel). Low risk—components individually proven. |
|
||||
| Pitfalls | HIGH | All 8 pitfalls documented with real-world examples (Electron GitHub issues, production bug reports), prevention strategies verified through official docs and battle-tested patterns |
|
||||
|
||||
**Overall confidence:** HIGH
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
**Architecture validation:** No reference implementation combining Zustand persist + ServiceContextRegistry + BroadcastChannel for Electron multi-context switching. Each component individually proven, but integration is novel.
|
||||
- **Handle during planning:** Build small proof-of-concept in Phase 1 (10-file mini-app) to validate integration before full implementation
|
||||
- **Validation criteria:** Prove context switch completes in <100ms, no memory leaks after 50 switches, multi-window sync works
|
||||
|
||||
**IndexedDB snapshot expiration strategy:** Research provides 5-minute TTL heuristic, but optimal value depends on actual data refresh latency and user switching patterns.
|
||||
- **Handle during planning:** Start with 5-minute TTL, add telemetry in Phase 2 to track snapshot age at restoration, adjust based on p95 latency
|
||||
- **Validation criteria:** <5% of switches trigger full re-fetch (snapshot expired), user doesn't perceive staleness
|
||||
|
||||
**Parallel window resource limits:** Phase 4 defers parallel windows, but no research on optimal limit (2 windows? 5? 10?).
|
||||
- **Handle during execution:** When implementing Phase 4, research Chromium per-process memory overhead, test with 2/3/5/10 windows, establish limit based on 8GB RAM baseline
|
||||
- **Validation criteria:** Memory usage stays under 500MB per window, no UI jank with N windows open
|
||||
|
||||
**Auto-reconnect backoff parameters:** Research mentions exponential backoff and max 6-10 retries, but no specifics on initial delay or multiplier.
|
||||
- **Handle during Phase 3 planning:** Research PubNub connection management docs (cited in PITFALLS.md sources), test with 1s initial, 2x multiplier, 10s max to match SSH timeout patterns
|
||||
- **Validation criteria:** 90%+ of transient network blips recover within 30s, users rarely see manual reconnect prompt
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- [Electron Official Documentation](https://www.electronjs.org/docs/latest/) — IPC patterns, process model, security best practices
|
||||
- [Zustand Persist Middleware](https://zustand.docs.pmnd.rs/middlewares/persist) — Hydration control, partialize, storage backends
|
||||
- [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) — Context switching patterns, extension classification, workspace indexing
|
||||
- [Slack Workspace Switching](https://slack.com/help/articles/1500002200741-Switch-between-workspaces) — Multi-workspace UX patterns, keyboard shortcuts
|
||||
- [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/) — Workspace lifecycle, IDE backend management
|
||||
- [Akiflow Multi-Window Messaging](https://akiflow.com/blog/multi-window-messaging-in-akiflow) — BroadcastChannel implementation patterns
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [Advanced Electron.js Architecture - LogRocket](https://blog.logrocket.com/advanced-electron-js-architecture/) — Main process architecture, IPC best practices
|
||||
- [Syncing State Between Electron Contexts - Bruno Scheufler](https://brunoscheufler.com/blog/2023-10-29-syncing-state-between-electron-contexts) — State synchronization patterns
|
||||
- [React Stale Closures - Dmitri Pavlutin](https://dmitripavlutin.com/react-hooks-stale-closures/) — Stale closure prevention in React/Zustand
|
||||
- [Diagnosing Memory Leaks in Electron - Mindful Chase](https://www.mindfulchase.com/explore/troubleshooting-tips/frameworks-and-libraries/diagnosing-and-fixing-memory-leaks-in-electron-applications.html) — EventEmitter leak patterns
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- [TSyringe - Microsoft DI Container](https://github.com/microsoft/tsyringe) — Dependency injection patterns for ServiceContextRegistry (used as inspiration, not direct implementation)
|
||||
- [Connection Management - PubNub](https://www.pubnub.com/docs/general/setup/connection-management) — Auto-reconnect strategies (provides parameters for Phase 3)
|
||||
|
||||
---
|
||||
*Research completed: 2026-02-12*
|
||||
*Ready for roadmap: yes*
|
||||
|
|
@ -92,10 +92,10 @@ importers:
|
|||
version: 1.15.5
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.1
|
||||
version: 4.7.0(vite@5.4.21(@types/node@25.0.7))
|
||||
version: 4.7.0(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.1.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)(terser@5.46.0))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.17
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
|
|
@ -107,7 +107,7 @@ importers:
|
|||
version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
electron-vite:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(vite@5.4.21(@types/node@25.0.7))
|
||||
version: 2.3.0(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))
|
||||
eslint:
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
|
|
@ -179,10 +179,10 @@ importers:
|
|||
version: 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
vite:
|
||||
specifier: ^5.4.2
|
||||
version: 5.4.21(@types/node@25.0.7)
|
||||
version: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
vitest:
|
||||
specifier: ^3.1.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)(terser@5.46.0)
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -731,6 +731,9 @@ packages:
|
|||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
|
|
@ -1680,6 +1683,9 @@ packages:
|
|||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -3682,6 +3688,11 @@ packages:
|
|||
temp-file@3.4.0:
|
||||
resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==}
|
||||
|
||||
terser@5.46.0:
|
||||
resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4531,6 +4542,12 @@ snapshots:
|
|||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
|
|
@ -5017,7 +5034,7 @@ snapshots:
|
|||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||
optional: true
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.0.7))':
|
||||
'@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6)
|
||||
|
|
@ -5025,11 +5042,11 @@ snapshots:
|
|||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 5.4.21(@types/node@25.0.7)
|
||||
vite: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
|
|
@ -5044,7 +5061,7 @@ snapshots:
|
|||
std-env: 3.10.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -5056,13 +5073,13 @@ snapshots:
|
|||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.0.7))':
|
||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@25.0.7)
|
||||
vite: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
|
|
@ -5535,6 +5552,9 @@ snapshots:
|
|||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@2.20.3:
|
||||
optional: true
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commander@5.1.0: {}
|
||||
|
|
@ -5766,7 +5786,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@2.3.0(vite@5.4.21(@types/node@25.0.7)):
|
||||
electron-vite@2.3.0(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.6)
|
||||
|
|
@ -5774,7 +5794,7 @@ snapshots:
|
|||
esbuild: 0.21.5
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 5.4.21(@types/node@25.0.7)
|
||||
vite: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -8133,6 +8153,14 @@ snapshots:
|
|||
async-exit-hook: 2.0.1
|
||||
fs-extra: 10.1.0
|
||||
|
||||
terser@5.46.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
optional: true
|
||||
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
|
|
@ -8369,13 +8397,13 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-node@3.2.4(@types/node@25.0.7):
|
||||
vite-node@3.2.4(@types/node@25.0.7)(terser@5.46.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.21(@types/node@25.0.7)
|
||||
vite: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
|
|
@ -8387,7 +8415,7 @@ snapshots:
|
|||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.4.21(@types/node@25.0.7):
|
||||
vite@5.4.21(@types/node@25.0.7)(terser@5.46.0):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.6
|
||||
|
|
@ -8395,12 +8423,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/node': 25.0.7
|
||||
fsevents: 2.3.3
|
||||
terser: 5.46.0
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@17.6.3)(terser@5.46.0):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.7))
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.7)(terser@5.46.0))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
|
|
@ -8418,8 +8447,8 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.21(@types/node@25.0.7)
|
||||
vite-node: 3.2.4(@types/node@25.0.7)
|
||||
vite: 5.4.21(@types/node@25.0.7)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@25.0.7)(terser@5.46.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
|
|
|
|||
Loading…
Reference in a new issue