From 8a7aa6bf37bcea60f2c888b16224e34000eddd69 Mon Sep 17 00:00:00 2001 From: Sameeran Bandishti Date: Sun, 1 Feb 2026 15:35:51 +0530 Subject: [PATCH 01/15] Add INTERNAL_FEATURE_ANALYSIS.md from internal-v1-legacy --- INTERNAL_FEATURE_ANALYSIS.md | 819 +++++++++++++++++++++++++++++++++++ 1 file changed, 819 insertions(+) create mode 100644 INTERNAL_FEATURE_ANALYSIS.md diff --git a/INTERNAL_FEATURE_ANALYSIS.md b/INTERNAL_FEATURE_ANALYSIS.md new file mode 100644 index 000000000..72f692110 --- /dev/null +++ b/INTERNAL_FEATURE_ANALYSIS.md @@ -0,0 +1,819 @@ +# Internal Branch Feature Analysis & Reimplementation Plan + +**Date**: 2026-02-01 +**Branches**: `internal` vs `main` +**Strategy**: Selective feature reimplementation from main base + +--- + +## Executive Summary + +The `internal` branch contains **15 major feature categories** across 144 modified files (+31,790 / -2,614 lines). After thorough analysis, we've identified **10 features worth reimplementing** and **2 obsolete features to discard**. + +**Recommended Approach**: Create feature branch from `main` and reimplement in 5 phases over ~5 weeks. + +--- + +## Complete Feature Inventory + +### 🟢 High Priority Features (Must Have) + +#### 1. **MCP Integration System** +- **Files**: `mcp-auth.ts` (537 lines), `ii-mcp-servers.ts` (215 lines), `claude-config.ts` (204 lines) +- **What it does**: + - Manages 7 Python-based MCP servers on ports 11021-11027 + - Fetches tools from HTTP/SSE and stdio transports + - Auto-refreshes OAuth tokens before sessions + - Reads/writes `~/.claude.json` configuration + - Resolves worktree paths to original project paths +- **Key functionality**: + - `fetchMcpTools()` - HTTP/SSE transport + - `fetchMcpToolsStdio()` - stdio transport with proper environment + - `ensureMcpTokensFresh()` - Token refresh with 5-min buffer + - `startMcpOAuth()` - Initiate OAuth flows + - `getProjectMcpServers()` - Load servers from config + - Spawn Python servers with `uvx`, `uv run`, or system Python +- **Dependencies**: + - `@modelcontextprotocol/sdk` for client + - OAuth infrastructure + - Python environment (.venv or system) +- **Effort**: 🔴 Large (security-critical, multi-transport) + +#### 2. **OAuth & Credential Infrastructure** +- **Files**: `oauth.ts` (906 lines), `oauth-server.ts` (431 lines), `oauth-utils.ts` (111 lines), `credential-manager.ts` (821 lines) +- **What it does**: + - PKCE OAuth flows for Google, Slack, Microsoft + - Local OAuth callback server (ports 21300-21399) + - Unified credential storage with encryption + - Automatic token refresh + - Fallback handling (OAuth → bearer) +- **Key classes**: + - `CraftOAuth` - Main OAuth handler + - `SourceCredentialManager` - Credential CRUD +- **Security features**: + - PKCE code challenge generation (S256) + - Electron safeStorage for encryption + - 5-minute expiry buffer + - State parameter validation +- **Effort**: 🔴 Large (security-critical) + +#### 3. **System Prompt Extensions** +- **Files**: `system-prompt-extensions.ts` (117 lines) +- **What it does**: + - Injects workspace context into system prompts + - Teaches Claude about workspace file operations + - Enforces full path usage in file mentions + - Provides examples and best practices +- **Critical requirement**: Always use `.ii/workspaces/{chatId}/filename.md` format +- **Effort**: 🟢 Small (single file) + +--- + +### 🟡 Medium Priority Features (Should Have) + +#### 4. **Agent Management System** +- **Files**: `agents.ts` router (25 lines), agent DB schema, `agent-utils.ts` (152 lines) +- **What it does**: + - CRUD for agent markdown files + - Parse agent.md with gray-matter (YAML frontmatter) + - Sync filesystem ↔ database + - Track creation/modification via chat IDs + - Support user-level (`~/.claude/agents/`) and project-level (`.claude/agents/`) +- **Database schema**: +```typescript +{ + id, name, description, prompt, + tools: JSON, // Allowed tools + disallowedTools: JSON, // Disallowed tools + model, // "sonnet" | "opus" | "haiku" | "inherit" + source, // "user" | "project" + projectId, filePath, + createdViaChat: boolean, + creationChatIds: JSON, + modificationChatIds: JSON +} +``` +- **Key changes from internal**: + - **Auto-registration**: `buildAllAgentsOption()` loads ALL agents for auto-invocation + - Agents invoked based on descriptions, not just @mentions + - Sync to DB happens in background (fire-and-forget) +- **Effort**: 🟡 Medium (backend + file parsing) + +#### 5. **Agent Builder UI** +- **Files**: `agent-builder-modal.tsx` (38 lines wrapper), `chat-pane.tsx` (162 lines), `document-pane.tsx` (153 lines), `response-parser.ts` (176 lines) +- **What it does**: + - Dual-pane modal: chat (left) + live preview (right) + - Conversational agent creation + - Phase-based workflow: discovery → formalize → research → generate + - Response parser extracts structured data from assistant responses +- **Phases**: + 1. **Discovery**: Ask user about agent purpose + 2. **Formalization**: Extract requirements list + 3. **Research**: Gather context about codebase + 4. **Generation**: Generate agent.md file +- **Integration**: Reuses chat streaming infrastructure with special `agent-builder` mode +- **Effort**: 🟡 Medium (UI + parser logic) + +#### 6. **Model Profiles** +- **Files**: `model-profiles.ts` router (235 lines), DB schema +- **What it does**: + - Custom model configs for OpenAI-compatible APIs + - Support multiple models per profile + - Offline mode flag (Ollama integration) + - Bulk upsert from localStorage +- **Database schema**: +```typescript +{ + id, name, description, + config: JSON, // { model, token, baseUrl } + models: JSON, // Array of model names + isOffline: boolean, + createdAt, updatedAt +} +``` +- **Operations**: `list`, `get`, `create`, `update`, `delete`, `bulkUpsert`, `importFromLocalStorage` +- **Effort**: 🟢 Small (CRUD router) + +#### 7. **Workspace File System** +- **Files**: `file-crud.ts` (200 lines), `workspace-files/` directory (5 components) +- **What it does**: + - File tree viewer for `.ii/workspaces/{chatId}/` + - CRUD operations with security validation + - Document viewer (markdown/text) + - Code viewer with syntax highlighting +- **Backend (`file-crud.ts`)**: + - `listFiles` - Build recursive file tree + - `readFile` - Read with path validation + - Security: Directory traversal prevention +- **Frontend components**: + - `FileTree` - Folder/file navigation + - `DocumentViewer` - Markdown/text rendering + - `CodeViewer` - Syntax highlighting + - `useOpenFile` - Hook for file actions + - `file-types.ts` - File type detection +- **Effort**: 🟡 Medium (backend + UI) + +--- + +### 🔵 Low Priority Features (Nice to Have) + +#### 8. **Ollama Enhancements** +- **Files**: `ollama.ts` router (238 lines) +- **What it does**: + - Check Ollama status + internet connectivity + - Generate chat names (2-5 words) + - Generate commit messages (conventional format) + - **Input refinement**: Enhance vague prompts with context +- **Input refinement logic**: + 1. Identify vague terms ("this", "the file", etc.) + 2. Find relevant files from workspace + chat history + 3. Replace vague terms with specific file mentions + 4. Add @file mentions for context +- **Operations**: `getStatus`, `isOfflineModeAvailable`, `getModels`, `generateChatName`, `generateCommitMessage`, `refineInput` +- **Effort**: 🟢 Small (API wrapper) + +#### 9. **Feedback System** +- **Files**: `feedback.ts` router (78 lines), `feedback-dialog.tsx` (235 lines), `feedback-list-dialog.tsx` (287 lines), DB schema +- **What it does**: + - In-app feedback collection + - Screenshot upload support + - List/resolve feedback entries +- **Database schema**: +```typescript +{ + id, type, priority, description, + screenshots: JSON, // File paths + resolved: boolean, + createdAt, updatedAt +} +``` +- **Types**: bug, feature, enhancement, idea, usability, other +- **Priorities**: low, medium, high, critical +- **Effort**: 🟢 Small (CRUD + dialog) + +--- + +### ❌ Features to Discard (Obsolete) + +#### 1. **Tasks Feature** +- **Files**: `tasks/tasks-page.tsx`, `tasks/atoms.ts` +- **Why obsolete**: + - Client-side only (Jotai atoms) + - Not persisted to database + - Too trivial (name + description) + - Better alternatives exist (TodoWrite tool) +- **Action**: Don't migrate + +#### 2. **Multi-Account Support** +- **Files**: DB schema changes (removed `anthropicAccounts`, `anthropicSettings`) +- **Why obsolete**: Main branch likely has different auth approach +- **Action**: Check main's auth system before deciding + +--- + +## Critical Behavioral Changes from Internal + +### 1. **Claude Router Changes** + +#### Fork Detection & Message Replay +```typescript +// NEW: Detect forked chats (no sessionId + has history + not Ollama) +const isForkedChat = !existingSessionId && existingMessages.length > 0 +const needsMessageReplay = isForkedChat && !isUsingOllama + +if (needsMessageReplay) { + console.log(`[claude] Forked chat detected - replaying ${existingMessages.length} messages`) + prompt = createReplayMessages(existingMessages, userMessage, input.images) +} +``` + +#### Auto-Agent Registration +```typescript +// OLD (internal): Only register mentioned agents +const agentsOption = await buildAgentsOption(agentMentions, input.cwd) + +// NEW (internal): Register ALL agents for auto-invocation +const agentsOption = await buildAllAgentsOption(input.cwd) +console.log(`[claude] Registered ${Object.keys(agentsOption).length} agents with SDK`) + +// Fire-and-forget sync to database +syncAgentsToDatabase(input.cwd).catch(err => { + console.error('[claude] Failed to sync agents to database:', err) +}) +``` + +#### Agent Mode Types Expanded +```typescript +// OLD: "plan" | "agent" +// NEW: "plan" | "agent" | "agent-builder" | "read-only" | "ask" +mode: z.enum(["plan", "agent", "agent-builder", "read-only", "ask"]) +``` + +#### Timeout Handling for Tool Approvals +```typescript +// NEW: Clear timeouts when aborting sessions +for (const [toolUseId, pending] of pendingToolApprovals) { + if (pending.timeoutId) { + clearTimeout(pending.timeoutId) + } + pending.resolve({ approved: false, message }) +} +``` + +#### Offline Fallback Moved Earlier +```typescript +// CRITICAL: Check offline status BEFORE fork detection +// This ensures isUsingOllama is defined before fork logic runs +const offlineResult = await checkOfflineFallback(input.customConfig, claudeCodeToken) +const isUsingOllama = offlineResult.isUsingOllama + +// Now fork detection can safely use isUsingOllama +const needsMessageReplay = isForkedChat && !isUsingOllama +``` + +#### Message Part Conversion +```typescript +// NEW: Convert stored messages to SDK format +function convertStoredPartToSDKContent(part: any) { + // Text parts: direct conversion + if (part.type === "text" && part.text) { + return [{ type: "text", text: part.text }] + } + + // Tool results: convert to SDK format (skip tool calls) + if (part.type?.startsWith("tool-") && part.state === "result" && part.toolCallId) { + return [{ + type: "tool_result", + tool_use_id: part.toolCallId, + content: typeof part.result === "string" ? part.result : JSON.stringify(part.result || {}) + }] + } + + return [] // Skip tool calls, system messages, etc. +} +``` + +### 2. **Chat Input Area Changes** + +#### Input Refinement (Ollama) +```typescript +// NEW: State for input refinement +const [isRefining, setIsRefining] = useState(false) +const [previousInput, setPreviousInput] = useState(null) +const refineInputMutation = trpc.ollama.refineInput.useMutation() + +// Triggered before sending message to enhance vague prompts +``` + +#### Model Selector Enhancement +```typescript +// OLD: Only Claude models +const models = CLAUDE_MODELS + +// NEW: Claude + Custom + Ollama models +const availableModels = useAllModels() +``` + +#### Mode Toggle Hiding +```typescript +// NEW: Support hiding mode toggle (for agent builder) +interface ChatInputAreaProps { + // ... existing props + hideModeToggle?: boolean +} +``` + +#### Insert Text Event System +```typescript +// NEW: Global event for inserting text into input +export const INSERT_TEXT_EVENT = "insert-text-event" +export interface InsertTextPayload { text: string } + +// Listener in ChatInputArea +useEffect(() => { + const handleInsert = (e: CustomEvent) => { + // Insert text into input area + } + window.addEventListener(INSERT_TEXT_EVENT, handleInsert) + return () => window.removeEventListener(INSERT_TEXT_EVENT, handleInsert) +}, []) +``` + +### 3. **Agent Utils Changes** + +#### Build All Agents Option +```typescript +// NEW: Load ALL agents for auto-invocation (not just mentioned ones) +export async function buildAllAgentsOption(cwd: string): Promise> { + const userAgents = await listAgentsFromDir(path.join(os.homedir(), ".claude", "agents")) + const projectAgents = await listAgentsFromDir(path.join(cwd, ".claude", "agents")) + + const allAgents = [...userAgents, ...projectAgents] + + // Return SDK-compatible agents object + return Object.fromEntries( + allAgents.map(agent => [agent.name, { /* agent config */ }]) + ) +} +``` + +#### Sync Agents to Database +```typescript +// NEW: Background sync of filesystem agents to database +export async function syncAgentsToDatabase(cwd: string): Promise { + const userAgents = await listAgentsFromDir(path.join(os.homedir(), ".claude", "agents")) + const projectAgents = await listAgentsFromDir(path.join(cwd, ".claude", "agents")) + + // Upsert to database + for (const agent of [...userAgents, ...projectAgents]) { + await db.insert(agents).values({ /* ... */ }).onConflictDoUpdate({ /* ... */ }) + } +} +``` + +--- + +## Database Schema Changes + +### New Tables Required + +```sql +-- 1. Agents table +CREATE TABLE agents ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + prompt TEXT NOT NULL, + tools TEXT, -- JSON array of allowed tools + disallowed_tools TEXT, -- JSON array of disallowed tools + model TEXT, -- "sonnet" | "opus" | "haiku" | "inherit" + source TEXT NOT NULL, -- "user" | "project" + project_id TEXT, + file_path TEXT NOT NULL, -- Actual .md file location + created_via_chat INTEGER NOT NULL DEFAULT 0, + creation_chat_ids TEXT NOT NULL DEFAULT '[]', -- JSON array + modification_chat_ids TEXT NOT NULL DEFAULT '[]', -- JSON array + created_at INTEGER, + updated_at INTEGER +); + +CREATE INDEX idx_agents_source ON agents(source); + +-- 2. Model Profiles table +CREATE TABLE model_profiles ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + config TEXT NOT NULL, -- JSON: { model, token, baseUrl } + models TEXT NOT NULL, -- JSON array of model names + is_offline INTEGER DEFAULT 0, + created_at INTEGER, + updated_at INTEGER +); + +CREATE INDEX idx_model_profiles_is_offline ON model_profiles(is_offline); + +-- 3. Feedback table +CREATE TABLE feedback ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, -- bug, feature, enhancement, idea, usability, other + priority TEXT NOT NULL, -- low, medium, high, critical + description TEXT NOT NULL, + screenshots TEXT, -- JSON array of file paths + resolved INTEGER NOT NULL DEFAULT 0, + created_at INTEGER, + updated_at INTEGER +); +``` + +### Modified Tables + +```sql +-- Add to sub_chats +ALTER TABLE sub_chats ADD COLUMN is_saved_chat_state INTEGER NOT NULL DEFAULT 0; +``` + +--- + +## Implementation Phases + +### **Phase 1: Foundation (Week 1)** +**Goal**: Authentication infrastructure and database setup + +**Tasks**: +1. Create feature branch from main: `git checkout -b feature/mcp-integration main` +2. Database migrations: + - `drizzle/0006_add_agents.sql` + - `drizzle/0007_add_model_profiles.sql` + - `drizzle/0008_add_feedback.sql` + - Add `isSavedChatState` to `subChats` +3. OAuth infrastructure: + - Copy `oauth.ts`, `oauth-server.ts`, `oauth-utils.ts` + - Copy `credential-manager.ts` + - Write tests for PKCE generation + - Test OAuth callback server (ports 21300-21399) +4. Verify build: `bun run build && bun run dev` + +**Deliverable**: Working OAuth flows with mock provider + +--- + +### **Phase 2: MCP Integration (Week 2)** +**Goal**: Enable MCP server support + +**Tasks**: +1. MCP auth system: + - Copy `mcp-auth.ts` + - Implement token refresh logic + - Test HTTP/SSE transport + - Test stdio transport with environment +2. Claude config management: + - Copy `claude-config.ts` + - Test `~/.claude.json` read/write + - Test worktree path resolution +3. ii MCP servers: + - Copy `ii-mcp-servers.ts` + - Set up Python environment + - Test server spawn/kill lifecycle + - Test port allocation (11021-11027) +4. Integration with Claude router: + - Update `claude.ts` for MCP tool fetching + - Add `ensureMcpTokensFresh` before sessions + - Test end-to-end tool invocation + +**Deliverable**: MCP servers providing tools to Claude sessions + +--- + +### **Phase 3: File System & Prompts (Week 3)** +**Goal**: Workspace files and context injection + +**Tasks**: +1. System prompt extensions: + - Copy `system-prompt-extensions.ts` + - Test workspace context injection + - Verify file mention prompts +2. File CRUD backend: + - Copy `file-crud.ts` router + - Test `listFiles`, `readFile` endpoints + - Add path security tests (traversal prevention) +3. Workspace files UI: + - Copy `workspace-files/` directory + - Test file tree component + - Test document viewer + - Test code viewer with syntax highlighting +4. Integration: + - Add to tRPC router registry + - Test file operations from chat + - Verify @file mentions work + +**Deliverable**: Workspace-scoped file system accessible from chat + +--- + +### **Phase 4: Agent Management (Week 4)** +**Goal**: Custom agents with auto-invocation + +**Tasks**: +1. Agent backend: + - Copy `agents.ts` router + - Copy `agent-utils.ts` + - Test agent file parsing (gray-matter) + - Test CRUD operations + - Test sync from filesystem to database +2. Agent builder UI: + - Copy `agent-builder-modal.tsx` + - Copy `response-parser.ts` + - Test dual-pane modal + - Test phase detection (discovery → generate) +3. Claude router integration: + - Update to use `buildAllAgentsOption()` + - Add agent auto-invocation + - Test agent sync to database +4. Testing: + - Create agent via builder + - Invoke agent from chat + - Verify agent tools/prompts work + +**Deliverable**: Functional agent builder + auto-invocation + +--- + +### **Phase 5: Polish & Extras (Week 5)** +**Goal**: Model profiles, Ollama, feedback + +**Tasks**: +1. Model profiles: + - Copy `model-profiles.ts` router + - Test CRUD operations + - Test custom API endpoints + - Update model selector UI +2. Ollama enhancements: + - Copy enhanced `ollama.ts` router + - Test chat name generation + - Test commit message generation + - Test input refinement +3. Feedback system: + - Copy `feedback.ts` router + - Copy feedback dialogs + - Test screenshot upload + - Test list/resolution +4. Final testing: + - Integration tests for all features + - Security audit (OAuth, file paths) + - Cross-platform testing + - Performance testing + +**Deliverable**: Production-ready feature set + +--- + +## File Migration Checklist + +### Backend (Main Process) + +#### OAuth & Auth +- [ ] `src/main/lib/oauth.ts` (906 lines) +- [ ] `src/main/lib/oauth-server.ts` (431 lines) +- [ ] `src/main/lib/oauth-utils.ts` (111 lines) +- [ ] `src/main/lib/credential-manager.ts` (821 lines) + +#### MCP Integration +- [ ] `src/main/lib/mcp-auth.ts` (537 lines) +- [ ] `src/main/lib/ii-mcp-servers.ts` (215 lines) +- [ ] `src/main/lib/claude-config.ts` (204 lines) + +#### System & Prompts +- [ ] `src/main/lib/system-prompt-extensions.ts` (117 lines) + +#### Database +- [ ] `src/main/lib/db/schema/feedback.ts` (42 lines) +- [ ] `src/main/lib/db/schema/index.ts` (agents, modelProfiles tables) + +#### tRPC Routers +- [ ] `src/main/lib/trpc/routers/agents.ts` (25 lines) +- [ ] `src/main/lib/trpc/routers/agent-utils.ts` (152 lines) +- [ ] `src/main/lib/trpc/routers/file-crud.ts` (200 lines) +- [ ] `src/main/lib/trpc/routers/model-profiles.ts` (235 lines) +- [ ] `src/main/lib/trpc/routers/ollama.ts` (238 lines - enhanced) +- [ ] `src/main/lib/trpc/routers/feedback.ts` (78 lines) +- [ ] `src/main/lib/trpc/routers/index.ts` (router registration) + +#### Claude Router Modifications +- [ ] `src/main/lib/trpc/routers/claude.ts`: + - [ ] Fork detection logic + - [ ] Message replay conversion + - [ ] Auto-agent registration + - [ ] Agent mode enum expansion + - [ ] Timeout handling + - [ ] Offline fallback repositioning + +### Frontend (Renderer Process) + +#### Dialogs +- [ ] `src/renderer/components/dialogs/agent-builder-modal.tsx` (38 lines) +- [ ] `src/renderer/components/dialogs/feedback-dialog.tsx` (235 lines) +- [ ] `src/renderer/components/dialogs/feedback-list-dialog.tsx` (287 lines) + +#### Agent Builder +- [ ] `src/renderer/components/dialogs/agent-builder/chat-pane.tsx` (162 lines) +- [ ] `src/renderer/components/dialogs/agent-builder/document-pane.tsx` (153 lines) +- [ ] `src/renderer/components/dialogs/agent-builder/hooks/use-agent-builder-chat.ts` (204 lines) + +#### Workspace Files +- [ ] `src/renderer/features/workspace-files/components/file-tree.tsx` (215 lines) +- [ ] `src/renderer/features/workspace-files/components/document-viewer.tsx` (308 lines) +- [ ] `src/renderer/features/workspace-files/components/code-viewer.tsx` (100 lines) +- [ ] `src/renderer/features/workspace-files/hooks/use-open-file.ts` (56 lines) +- [ ] `src/renderer/features/workspace-files/utils/file-types.ts` (113 lines) +- [ ] `src/renderer/features/workspace-files/index.ts` (4 lines) + +#### Agent Builder Support +- [ ] `src/renderer/lib/agent-builder/response-parser.ts` (176 lines) +- [ ] `src/renderer/lib/atoms/agent-builder.ts` (70 lines) + +#### UI Enhancements +- [ ] `src/renderer/features/agents/ui/model-selector.tsx` (120 lines) +- [ ] `src/renderer/features/agents/lib/models.ts` (useAllModels) +- [ ] `src/renderer/features/agents/main/chat-input-area.tsx`: + - [ ] Input refinement state/mutation + - [ ] Model selector integration + - [ ] Mode toggle hiding + - [ ] Insert text event listener + +#### Atoms & State +- [ ] `src/renderer/lib/atoms/documents.ts` (174 lines) +- [ ] `src/renderer/lib/atoms/right-sidebar.ts` (24 lines) +- [ ] `src/renderer/features/agents/atoms/index.ts` (agentModeAtom) + +--- + +## Testing Strategy + +### Unit Tests +- [ ] OAuth PKCE generation (code verifier, challenge) +- [ ] Credential encryption/decryption (safeStorage) +- [ ] Path security validation (file-crud traversal prevention) +- [ ] Agent file parsing (gray-matter YAML frontmatter) +- [ ] Message replay conversion (stored → SDK format) +- [ ] Response parser (agent builder phases) + +### Integration Tests +- [ ] OAuth flow with mock provider +- [ ] MCP server lifecycle (spawn/kill/status) +- [ ] MCP tool fetching (HTTP/SSE and stdio) +- [ ] Token refresh flow (before expiry) +- [ ] Agent CRUD operations (filesystem ↔ DB sync) +- [ ] Workspace file operations (list/read/write) +- [ ] Input refinement with context + +### E2E Tests +- [ ] Create agent via builder +- [ ] Invoke agent from chat (auto and @mention) +- [ ] Add custom model profile +- [ ] Use workspace files in chat (@file mentions) +- [ ] Submit feedback with screenshots +- [ ] Fork chat at message +- [ ] Ollama offline mode + +### Security Tests +- [ ] Path traversal attacks (file-crud) +- [ ] OAuth state parameter tampering +- [ ] Token expiry edge cases +- [ ] File read outside project boundary +- [ ] PKCE challenge replay attack + +--- + +## Risks & Mitigations + +### Risk 1: OAuth Security Vulnerabilities +**Impact**: High (credential theft, unauthorized access) +**Mitigation**: +- Security audit of PKCE implementation +- Test with multiple providers (Google, Slack, Microsoft) +- Use Electron safeStorage for all tokens +- Validate state parameter on callback +- Implement request timeout (30s) + +### Risk 2: MCP Server Stability +**Impact**: Medium (tool failures, process crashes) +**Mitigation**: +- Process monitoring and health checks +- Auto-restart on crash (max 3 retries) +- Port conflict detection (11021-11027) +- Graceful degradation if servers fail +- Detailed logging for debugging + +### Risk 3: Path Traversal Attacks +**Impact**: High (read/write outside project) +**Mitigation**: +- Strict path validation in file-crud +- Test with malicious paths (`../../../etc/passwd`) +- Use `path.resolve` and validate against project root +- Reject absolute paths outside project +- Log all file access attempts + +### Risk 4: Database Migration Conflicts +**Impact**: Medium (data loss, schema inconsistency) +**Mitigation**: +- Review main branch migrations before numbering +- Coordinate migration sequence (0006, 0007, 0008) +- Test rollback scenarios +- Backup database before migration +- Use transactions for multi-step changes + +### Risk 5: Breaking Changes in Main +**Impact**: High (merge conflicts, feature regression) +**Mitigation**: +- Frequent rebases from main (daily) +- Incremental PRs (one phase at a time) +- Continuous integration testing +- Feature flags for experimental features +- Maintain compatibility layer if needed + +### Risk 6: Python Environment Issues +**Impact**: Medium (MCP servers fail to start) +**Mitigation**: +- Test with uvx, uv run, and system Python +- Detect .venv automatically +- Fallback to system Python if needed +- Clear error messages for missing dependencies +- Document Python setup requirements + +--- + +## Success Criteria + +### Must Have ✅ +- [ ] MCP servers start and register tools successfully +- [ ] OAuth flows complete without errors +- [ ] Agents can be created via builder +- [ ] Workspace files accessible in chat with @mentions +- [ ] Custom model profiles work with API calls +- [ ] No regressions in existing features +- [ ] All tests passing (unit, integration, E2E) +- [ ] Security audit passed + +### Should Have 🟡 +- [ ] Input refinement improves vague prompts +- [ ] Feedback system collects user input +- [ ] Ollama integration for offline ops +- [ ] Agent auto-invocation based on descriptions +- [ ] Token refresh happens automatically +- [ ] File operations are fast (<100ms) + +### Nice to Have 🔵 +- [ ] MCP server auto-restart on crash +- [ ] Workspace file search/grep +- [ ] Agent versioning and history +- [ ] Model profile templates +- [ ] Feedback analytics dashboard + +### Quality Gates 🚦 +- [ ] Code coverage >80% +- [ ] No critical security vulnerabilities +- [ ] Cross-platform compatibility (macOS, Linux, Windows) +- [ ] Documentation updated (CLAUDE.md, README) +- [ ] Code review approved (2+ reviewers) +- [ ] Performance benchmarks met (<2s startup) + +--- + +## Next Steps + +1. **Review & Approve** this plan with team +2. **Create feature branch** from main: + ```bash + git checkout main + git pull origin main + git checkout -b feature/mcp-integration + ``` +3. **Start Phase 1** (Foundation): + - Set up database migrations + - Implement OAuth infrastructure + - Write initial tests +4. **Daily standups** to track progress and blockers +5. **Incremental PRs** for each phase (not one giant merge) + +--- + +## Open Questions + +1. **Priority**: Which features are highest priority for users? + - Suggested: MCP integration > Agent builder > Model profiles +2. **Timeline**: Are 5-week phases realistic given team size? + - Adjust based on available resources +3. **Main branch**: What auth changes exist in main that conflict with OAuth? + - Need to review main's auth-manager.ts +4. **Python environment**: Where should MCP servers expect Python? + - Prefer .venv, fallback to system Python +5. **Deployment**: Beta users first or straight to production? + - Recommend beta for MCP features (complex) + +--- + +**Last Updated**: 2026-02-01 +**Status**: Awaiting team review +**Estimated Effort**: 5 weeks (1 dev full-time) +**Lines of Code**: ~5,000 new, ~2,000 modified From a32978bec5b80e9f98df928a19079b68d77e4e32 Mon Sep 17 00:00:00 2001 From: Sameeran Bandishti Date: Sun, 1 Feb 2026 17:53:05 +0530 Subject: [PATCH 02/15] feat(speckit): relocate ii-spec submodule to submodules/ii-spec Move ii-spec submodule from project root (spec-kit/) to organized location at submodules/ii-spec/ for better project structure. This completes Phase 0 of the SpecKit UI integration feature. Co-Authored-By: Claude Opus 4.5 --- .gitmodules | 3 +++ submodules/ii-spec | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 submodules/ii-spec diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..f13b54fff --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/ii-spec"] + path = submodules/ii-spec + url = git@github.com:SameeranB/ii-spec.git diff --git a/submodules/ii-spec b/submodules/ii-spec new file mode 160000 index 000000000..9111699cd --- /dev/null +++ b/submodules/ii-spec @@ -0,0 +1 @@ +Subproject commit 9111699cd27879e3e6301651a03e502ecb6dd65d From ccf5d22ca0e907e0c7eaaebc9dc91d2dfe8fc5f9 Mon Sep 17 00:00:00 2001 From: Sameeran Bandishti Date: Sun, 1 Feb 2026 17:55:36 +0530 Subject: [PATCH 03/15] feat(speckit): install markdown rendering dependencies for Phase 1 Add react-markdown, remark-gfm, and react-syntax-highlighter for rendering SpecKit artifacts in the UI. Verify project structure prerequisites (.specify/, specs/) are in place. Co-Authored-By: Claude Opus 4.5 --- bun.lockb | Bin 483005 -> 487961 bytes package.json | 5 +- specs/001-speckit-ui-integration/tasks.md | 515 ++++++++++++++++++++++ 3 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 specs/001-speckit-ui-integration/tasks.md diff --git a/bun.lockb b/bun.lockb index 6601c57c89350cbf3ed855a809ebd553dfbc7cfc..664d1ceeebf0fc195dd65a2c4d8d9ec6732e6c3c 100755 GIT binary patch delta 92715 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
&E^t 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

afh*({c23lwvoBB>h;ZVzrF68`<1m86{%R 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"$;~|{Y>g0 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 CmJ@3L diff --git a/package.json b/package.json index 48e649c67..e5f1cfe2c 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,8 @@ "react-dom": "19.2.1", "react-hotkeys-hook": "^4.6.1", "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.0", "react-zoom-pan-pinch": "^3.7.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", @@ -115,18 +117,19 @@ "devDependencies": { "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", + "@electron/rebuild": "^4.0.3", "@types/better-sqlite3": "^7.6.13", "@types/diff": "^8.0.0", "@types/node": "^20.17.50", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.4", "@welldone-software/why-did-you-render": "^10.0.1", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.31.8", "electron": "~39.4.0", "electron-builder": "^25.1.8", - "@electron/rebuild": "^4.0.3", "electron-vite": "^3.0.0", "postcss": "^8.5.1", "tailwindcss": "^3.4.17", diff --git a/specs/001-speckit-ui-integration/tasks.md b/specs/001-speckit-ui-integration/tasks.md new file mode 100644 index 000000000..7a4041009 --- /dev/null +++ b/specs/001-speckit-ui-integration/tasks.md @@ -0,0 +1,515 @@ +# Tasks: SpecKit UI Integration + +**Input**: Design documents from `/specs/001-speckit-ui-integration/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), data-model.md, contracts/trpc-router.ts + +**Tests**: No tests requested in the feature specification. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4, US5) +- Include exact file paths in descriptions + +## Path Conventions + +- Electron desktop app structure: `src/main/`, `src/renderer/`, `submodules/` +- Backend (main process): `src/main/lib/` +- Frontend (renderer): `src/renderer/features/` + +--- + +## Phase 0: Submodule Relocation (Infrastructure) + +**Purpose**: Relocate ii-spec submodule to organized location + +**⚠️ CRITICAL**: Must be completed before any other work can begin + +- [X] T001 Remove existing spec-kit submodule from project root using `git submodule deinit -f spec-kit && git rm -f spec-kit && rm -rf .git/modules/spec-kit` +- [X] T002 Create submodules directory at project root using `mkdir -p submodules` +- [X] T003 Add ii-spec submodule at submodules/ii-spec/ using `git submodule add git@github.com:SameeranB/ii-spec.git submodules/ii-spec` +- [X] T004 Initialize and update submodule using `git submodule update --init --recursive` +- [X] T005 Update .gitmodules file to reflect new submodule path at submodules/ii-spec +- [X] T006 Verify submodule accessibility by checking submodules/ii-spec/ directory structure + +**Checkpoint**: Submodule relocated - can now install dependencies + +--- + +## Phase 1: Dependencies & Setup + +**Purpose**: Install required packages and verify project structure + +- [X] T007 Install react-markdown for markdown rendering using `bun add react-markdown` +- [X] T008 [P] Install remark-gfm for GitHub Flavored Markdown support using `bun add remark-gfm` +- [X] T009 [P] Install react-syntax-highlighter for code highlighting using `bun add react-syntax-highlighter` +- [X] T010 [P] Install TypeScript types for react-syntax-highlighter using `bun add -D @types/react-syntax-highlighter` +- [X] T011 Verify .specify/memory/constitution.md exists in project +- [X] T012 [P] Verify .specify/templates/ directory exists +- [X] T013 [P] Verify specs/ directory exists + +**Checkpoint**: Dependencies installed - can now create foundational code + +--- + +## Phase 2: Foundational (Backend Infrastructure) + +**Purpose**: Core backend utilities and tRPC router that ALL user stories depend on + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +### Backend Utilities + +- [ ] T014 [P] Create src/main/lib/speckit/ directory for ii-spec integration utilities +- [ ] T015 [P] Implement getCurrentBranch() function in src/main/lib/speckit/file-utils.ts to read current Git branch using execSync +- [ ] T016 [P] Implement parseFeatureBranch() function in src/main/lib/speckit/file-utils.ts to extract feature number and name from branch pattern /^(\d{3})-(.+)$/ +- [ ] T017 [P] Implement checkFileExists() function in src/main/lib/speckit/file-utils.ts using fs.existsSync() +- [ ] T018 [P] Implement readFileContent() function in src/main/lib/speckit/file-utils.ts using fs.readFileSync() +- [ ] T019 [P] Implement listFeatureDirectories() function in src/main/lib/speckit/file-utils.ts to list specs/ directory and filter by /^\d{3}-/ pattern +- [ ] T020 [P] Implement detectWorkflowState() function in src/main/lib/speckit/state-detector.ts using file-based state detection logic from plan.md +- [ ] T021 [P] Implement parseClarificationQuestions() function in src/main/lib/speckit/state-detector.ts to extract [NEEDS CLARIFICATION: ...] markers from spec.md +- [ ] T022 [P] Implement executeCommand() function in src/main/lib/speckit/command-executor.ts to spawn ii-spec commands using child_process.spawn() +- [ ] T023 [P] Implement getExecutionEmitter() function in src/main/lib/speckit/command-executor.ts to retrieve EventEmitter for streaming output +- [ ] T024 [P] Implement cancelExecution() function in src/main/lib/speckit/command-executor.ts to kill running subprocess + +### tRPC Router (15 Procedures) + +- [ ] T025 Create src/main/lib/trpc/routers/speckit.ts with router skeleton and Zod schemas from contracts/trpc-router.ts +- [ ] T026 [P] Implement checkInitialization procedure in src/main/lib/trpc/routers/speckit.ts checking .specify/ directory structure +- [ ] T027 [P] Implement initializeSpecKit procedure in src/main/lib/trpc/routers/speckit.ts executing `specify init . --ai claude` +- [ ] T028 Implement getWorkflowState procedure in src/main/lib/trpc/routers/speckit.ts using detectWorkflowState() from state-detector.ts +- [ ] T029 [P] Implement getConstitution procedure in src/main/lib/trpc/routers/speckit.ts reading .specify/memory/constitution.md +- [ ] T030 [P] Implement getFeaturesList procedure in src/main/lib/trpc/routers/speckit.ts using listFeatureDirectories() and reading descriptions from spec.md files +- [ ] T031 [P] Implement getArtifact procedure in src/main/lib/trpc/routers/speckit.ts reading specs/{branch}/{artifactType}.md files +- [ ] T032 [P] Implement getFeatureDescription procedure in src/main/lib/trpc/routers/speckit.ts parsing spec.md first paragraph +- [ ] T033 Implement executeCommand procedure in src/main/lib/trpc/routers/speckit.ts using executeCommand() from command-executor.ts +- [ ] T034 Implement onCommandOutput subscription in src/main/lib/trpc/routers/speckit.ts using observable pattern and getExecutionEmitter() +- [ ] T035 [P] Implement cancelCommand procedure in src/main/lib/trpc/routers/speckit.ts using cancelExecution() +- [ ] T036 [P] Implement getCurrentBranch procedure in src/main/lib/trpc/routers/speckit.ts using getCurrentBranch() from file-utils.ts +- [ ] T037 [P] Implement getFeatureBranches procedure in src/main/lib/trpc/routers/speckit.ts listing all branches matching /^\d{3}-/ pattern +- [ ] T038 [P] Implement switchBranch procedure in src/main/lib/trpc/routers/speckit.ts executing `git checkout ` +- [ ] T039 [P] Implement openFileInEditor procedure in src/main/lib/trpc/routers/speckit.ts using Electron shell.openPath() +- [ ] T040 [P] Implement watchDirectory procedure in src/main/lib/trpc/routers/speckit.ts using fs.watch() on specs/ or .specify/ +- [ ] T041 [P] Implement onFileChange subscription in src/main/lib/trpc/routers/speckit.ts emitting file change events +- [ ] T042 Register speckit router in src/main/lib/trpc/index.ts appRouter + +### Frontend Types (Shared by All Stories) + +- [ ] T043 [P] Create src/renderer/features/speckit/types/ directory +- [ ] T044 [P] Create ArtifactPresenceSchema and type in src/renderer/features/speckit/types/feature.ts +- [ ] T045 [P] Create SpecKitFeatureSchema and type in src/renderer/features/speckit/types/feature.ts with Zod validation +- [ ] T046 [P] Create ConstitutionSchema and type in src/renderer/features/speckit/types/constitution.ts +- [ ] T047 [P] Create WorkflowStateSchema and type in src/renderer/features/speckit/types/workflow-state.ts with all workflow step names +- [ ] T048 [P] Create ClarificationQuestionSchema and type in src/renderer/features/speckit/types/workflow-state.ts +- [ ] T049 [P] Create InitializationStatusSchema and type in src/renderer/features/speckit/types/initialization.ts +- [ ] T050 [P] Create FeatureTableRow interface in src/renderer/features/speckit/types/ui-models.ts +- [ ] T051 [P] Create ConstitutionPreview interface in src/renderer/features/speckit/types/ui-models.ts +- [ ] T052 Export all types from src/renderer/features/speckit/types/index.ts + +### Frontend Atoms (Shared UI State) + +- [ ] T053 Create src/renderer/features/speckit/atoms/index.ts with speckitModalOpenAtom (boolean) +- [ ] T054 [P] Add speckitCurrentDocumentAtom to src/renderer/features/speckit/atoms/index.ts with type { type, content } | null +- [ ] T055 [P] Add speckitLoadingAtom to src/renderer/features/speckit/atoms/index.ts (boolean) + +### Shared Components & Utilities + +- [ ] T056 [P] Create extractPrincipleNames() utility function in src/renderer/features/speckit/utils/constitution-parser.ts to extract principle headers from markdown +- [ ] T057 [P] Create markdown rendering component MarkdownView in src/renderer/features/speckit/components/markdown-view.tsx using react-markdown with remark-gfm and syntax highlighting + +**Checkpoint**: Foundation complete - all user stories can now proceed in parallel + +--- + +## Phase 3: User Story 1 - Access SpecKit Workflow (Priority: P1) 🎯 MVP + +**Goal**: Users can click a SpecKit icon button in the top action bar to open a right drawer displaying the Plan page + +**Independent Test**: Click the SpecKit icon button and verify the right drawer opens with the Plan page; click again to toggle drawer closed + +### Implementation for User Story 1 + +- [ ] T058 [P] [US1] Create src/renderer/features/speckit/components/ directory +- [ ] T059 [US1] Create PlanPage component skeleton in src/renderer/features/speckit/components/plan-page.tsx with basic layout structure +- [ ] T060 [US1] Add SpecKit icon button (FileText from lucide-react) to top action bar in src/renderer/features/layout/top-action-bar.tsx in same group as Git and Terminal buttons +- [ ] T061 [US1] Create drawer state atom speckitDrawerOpenAtom in src/renderer/features/speckit/atoms/index.ts or reuse existing drawer atoms +- [ ] T062 [US1] Wire SpecKit icon button onClick to toggle drawer open/closed in src/renderer/features/layout/top-action-bar.tsx +- [ ] T063 [US1] Add 'speckit' case to drawer content switch statement in src/renderer/features/layout/drawer-content.tsx rendering PlanPage component +- [ ] T064 [US1] Implement drawer content switching logic to show PlanPage when activeView is 'speckit' in src/renderer/features/layout/drawer-content.tsx +- [ ] T065 [US1] Add toggle behavior so clicking SpecKit button when drawer is open with Plan page closes the drawer in src/renderer/features/layout/top-action-bar.tsx + +**Checkpoint**: User Story 1 complete - SpecKit icon button opens Plan page in right drawer with toggle functionality + +--- + +## Phase 4: User Story 4 - Create New Feature Workflow (Priority: P1) 🎯 MVP + +**Goal**: Users can initiate and complete the full SpecKit workflow (specify → clarify → plan → tasks) through a guided UI modal + +**Independent Test**: Click "New Feature" button, enter feature description, and be guided through each workflow step with visual feedback until tasks.md is generated + +**Note**: Implementing US4 before US2/US3 because it's P1 and provides core value proposition + +### Workflow Modal (Full-Screen Interface) + +- [ ] T066 [P] [US4] Create WorkflowModal component in src/renderer/features/speckit/components/workflow-modal.tsx with full-screen dialog layout +- [ ] T067 [P] [US4] Create workflow stepper UI in src/renderer/features/speckit/components/workflow-stepper.tsx showing: Constitution | Specify | Clarify | Plan | Tasks | Implement +- [ ] T068 [US4] Add dual-pane layout to WorkflowModal with chat pane (left) and document pane (right) in src/renderer/features/speckit/components/workflow-modal.tsx +- [ ] T069 [US4] Create chat pane component ChatPane in src/renderer/features/speckit/components/chat-pane.tsx for command execution and output streaming +- [ ] T070 [US4] Create document pane component DocumentPane in src/renderer/features/speckit/components/document-pane.tsx for live artifact preview using MarkdownView +- [ ] T071 [US4] Wire speckitModalOpenAtom to WorkflowModal open/close state in src/renderer/features/speckit/components/workflow-modal.tsx + +### Workflow State Management + +- [ ] T072 [P] [US4] Create useWorkflowState custom hook in src/renderer/features/speckit/hooks/use-workflow-state.ts wrapping trpc.speckit.getWorkflowState.useQuery +- [ ] T073 [P] [US4] Create useExecuteCommand custom hook in src/renderer/features/speckit/hooks/use-execute-command.ts wrapping trpc.speckit.executeCommand.useMutation +- [ ] T074 [P] [US4] Create useCommandOutput custom hook in src/renderer/features/speckit/hooks/use-command-output.ts wrapping trpc.speckit.onCommandOutput.useSubscription + +### Workflow Steps Implementation + +- [ ] T075 [US4] Create SpecifyStep component in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx with feature description input form +- [ ] T076 [US4] Implement form submit handler in SpecifyStep calling useExecuteCommand with `/speckit.specify` command in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx +- [ ] T077 [US4] Create ClarifyStep component in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx displaying clarification questions from WorkflowState +- [ ] T078 [US4] Implement clarification question answer form in ClarifyStep with textarea for each question in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx +- [ ] T079 [US4] Implement form submit handler in ClarifyStep calling useExecuteCommand with `/speckit.clarify` command and answers in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx +- [ ] T080 [US4] Create PlanStep component in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx with auto-initiate plan generation button +- [ ] T081 [US4] Implement plan approval UI in PlanStep showing generated plan.md with approve/regenerate actions in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx +- [ ] T082 [US4] Create TasksStep component in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx with auto-generate tasks button +- [ ] T083 [US4] Implement tasks generation completion UI in TasksStep showing success message and link to tasks.md in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx + +### Command Execution & Output Streaming + +- [ ] T084 [US4] Implement real-time command output streaming display in ChatPane using useCommandOutput hook in src/renderer/features/speckit/components/chat-pane.tsx +- [ ] T085 [US4] Add stdout/stderr differentiation styling in ChatPane (stdout: normal, stderr: error red) in src/renderer/features/speckit/components/chat-pane.tsx +- [ ] T086 [US4] Implement command cancellation button in ChatPane calling trpc.speckit.cancelCommand in src/renderer/features/speckit/components/chat-pane.tsx +- [ ] T087 [US4] Add loading/executing state indicators with progress spinners in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx + +### Live Artifact Preview + +- [ ] T088 [US4] Implement auto-refresh artifact content in DocumentPane polling trpc.speckit.getArtifact when workflow step completes in src/renderer/features/speckit/components/document-pane.tsx +- [ ] T089 [US4] Add artifact type tabs (Spec | Plan | Research | Tasks) to DocumentPane in src/renderer/features/speckit/components/document-pane.tsx +- [ ] T090 [US4] Wire artifact tabs to speckitCurrentDocumentAtom for display in src/renderer/features/speckit/components/document-pane.tsx + +### Implement Step (Task List with Copy Buttons) + +- [ ] T090.1 [US4] Create ImplementStep component in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx with task list layout +- [ ] T090.2 [US4] Implement task list parsing from tasks.md file in ImplementStep using trpc.speckit.getArtifact in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [ ] T090.3 [US4] Display each task with full description (task ID, description text, file paths) in ImplementStep in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [ ] T090.4 [US4] Add copy button per task that copies task reference (e.g., "T001") to clipboard using navigator.clipboard.writeText() in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [ ] T090.5 [US4] Show toast notification on successful copy with message "Task reference copied. Use /speckit.implement [task-id] in a new chat" in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx + +### Stale Warning & Skip Warning Banners + +- [ ] T090.6 [US4] Create StaleWarningBanner component in src/renderer/features/speckit/components/stale-warning-banner.tsx showing non-blocking warning when downstream artifacts exist +- [ ] T090.7 [US4] Add stale detection logic to WorkflowModal checking if current step has downstream artifacts (e.g., navigating to Specify when plan.md exists) in src/renderer/features/speckit/components/workflow-modal.tsx +- [ ] T090.8 [US4] Create SkipClarifyWarningBanner component in src/renderer/features/speckit/components/skip-clarify-warning.tsx showing warning when user tries to skip Clarify step +- [ ] T090.9 [US4] Add skip detection logic when user clicks Plan step before completing Clarify, showing warning but allowing continue in src/renderer/features/speckit/components/workflow-modal.tsx + +### Stepper Navigation (Free Movement Between Completed Steps) + +- [ ] T090.10 [US4] Make stepper steps clickable for completed steps in src/renderer/features/speckit/components/workflow-stepper.tsx +- [ ] T090.11 [US4] Implement step navigation handler that checks step completion before allowing navigation in src/renderer/features/speckit/components/workflow-stepper.tsx +- [ ] T090.12 [US4] Integrate stale warning trigger when navigating backward to a previous step in src/renderer/features/speckit/components/workflow-modal.tsx + +### Error Handling & Recovery + +- [ ] T091 [US4] Implement error message display in ChatPane showing ii-spec errors as-is from stderr in src/renderer/features/speckit/components/chat-pane.tsx +- [ ] T092 [US4] Add error recovery suggestions UI (e.g., "Command failed - try again" with retry button) in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx +- [ ] T093 [US4] Implement workflow step failure handling preserving state and allowing resume in WorkflowModal in src/renderer/features/speckit/components/workflow-modal.tsx + +### Integration with Plan Page + +- [ ] T094 [US4] Add "New Feature" button to PlanPage opening WorkflowModal in src/renderer/features/speckit/components/plan-page.tsx +- [ ] T095 [US4] Wire "New Feature" button onClick to set speckitModalOpenAtom to true in src/renderer/features/speckit/components/plan-page.tsx +- [ ] T096 [US4] Implement features list refresh after workflow completion using React Query invalidation in src/renderer/features/speckit/components/plan-page.tsx + +**Checkpoint**: User Story 4 complete - users can create new features through full guided workflow with real-time feedback + +--- + +## Phase 5: User Story 2 - View Constitution (Priority: P2) + +**Goal**: Users can view the project constitution document from the Plan page + +**Independent Test**: Open Plan page and click "View Constitution" to display constitution content + +### Implementation for User Story 2 + +- [ ] T097 [P] [US2] Create ConstitutionSection component in src/renderer/features/speckit/components/constitution-section.tsx with section heading and view button +- [ ] T098 [US2] Implement trpc.speckit.getConstitution query hook in ConstitutionSection component in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T099 [US2] Add constitution preview UI showing extracted principle names using extractPrincipleNames() utility in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T100 [US2] Create "View Constitution" button opening constitution in modal or expandable section in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T101 [US2] Implement full constitution modal dialog ConstitutionModal in src/renderer/features/speckit/components/constitution-modal.tsx rendering markdown using MarkdownView component +- [ ] T102 [US2] Wire "View Constitution" button to open ConstitutionModal in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T103 [US2] Add "Edit Constitution" button in ConstitutionModal calling trpc.speckit.openFileInEditor with .specify/memory/constitution.md path in src/renderer/features/speckit/components/constitution-modal.tsx +- [ ] T104 [US2] Implement "No constitution found" message when constitution doesn't exist with "Create Constitution" action in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T105 [US2] Wire "Create Constitution" button to execute `/speckit.constitution` command in src/renderer/features/speckit/components/constitution-section.tsx +- [ ] T106 [US2] Add ConstitutionSection to PlanPage layout above features section in src/renderer/features/speckit/components/plan-page.tsx + +**Checkpoint**: User Story 2 complete - users can view and edit constitution from Plan page + +--- + +## Phase 6: User Story 3 - Browse Previous Features (Priority: P2) + +**Goal**: Users can view a list of all previous SpecKit features and their associated artifacts + +**Independent Test**: Open Plan page, view features list, select a feature, and view its spec/plan/research/tasks artifacts + +### Implementation for User Story 3 + +- [ ] T107 [P] [US3] Create FeaturesTable component in src/renderer/features/speckit/components/features-table.tsx with table layout (ID | Name | Description | Branch | Artifacts) +- [ ] T108 [US3] Implement trpc.speckit.getFeaturesList query hook in FeaturesTable component in src/renderer/features/speckit/components/features-table.tsx +- [ ] T109 [US3] Add table row rendering mapping SpecKitFeature to FeatureTableRow display format in src/renderer/features/speckit/components/features-table.tsx +- [ ] T110 [US3] Implement artifact presence indicators showing checkmarks for existing artifacts (spec ✓, plan ✓, etc.) in src/renderer/features/speckit/components/features-table.tsx +- [ ] T111 [US3] Add "No features yet" empty state message when features list is empty in src/renderer/features/speckit/components/features-table.tsx +- [ ] T112 [US3] Create feature selection onClick handler opening FeatureDetailModal in src/renderer/features/speckit/components/features-table.tsx +- [ ] T113 [US3] Create FeatureDetailModal component in src/renderer/features/speckit/components/feature-detail-modal.tsx with tabs for Specification | Plan | Research | Tasks +- [ ] T114 [US3] Implement artifact tabs switching in FeatureDetailModal calling trpc.speckit.getArtifact for selected artifact type in src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T115 [US3] Add markdown rendering of artifact content in FeatureDetailModal using MarkdownView component in src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T116 [US3] Implement "Open in Editor" button per artifact in FeatureDetailModal calling trpc.speckit.openFileInEditor in src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T117 [US3] Add loading states for artifact content fetching in FeatureDetailModal in src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T118 [US3] Add error handling for missing artifacts showing "Artifact not found" message in FeatureDetailModal in src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T119 [US3] Add FeaturesTable to PlanPage layout below constitution section in src/renderer/features/speckit/components/plan-page.tsx + +**Checkpoint**: User Story 3 complete - users can browse all features and view their artifacts + +--- + +## Phase 7: User Story 5 - Submodule Integration (Priority: P3) + +**Goal**: Verify ii-spec submodule is properly integrated and accessible + +**Independent Test**: Verify .gitmodules file references forked repository, run `git submodule update --init`, and confirm app can access ii-spec functionality + +**Note**: Most of this was completed in Phase 0, this phase is verification only + +### Implementation for User Story 5 + +- [ ] T120 [US5] Verify .gitmodules file contains correct ii-spec submodule URL pointing to forked repository +- [ ] T121 [US5] Verify submodule initialization works by testing `git submodule update --init` command in fresh clone +- [ ] T122 [US5] Verify app can access ii-spec submodule by testing trpc.speckit.executeCommand with `/speckit.specify` command +- [ ] T123 [US5] Add submodule verification check to app startup sequence detecting missing/uninitialized submodule in src/main/index.ts +- [ ] T124 [US5] Implement user warning dialog when submodule not initialized showing instructions to run `git submodule update --init` in src/renderer/app.tsx or error boundary + +**Checkpoint**: User Story 5 complete - submodule integration verified and error handling in place + +--- + +## Phase 8: Initialization Detection & One-Click Setup + +**Purpose**: Handle uninitialized SpecKit projects gracefully with one-click initialization + +**Cross-cutting concern**: Affects Plan Page initial load (used by US2, US3, US4) + +### Implementation + +- [ ] T125 Create InitializationPrompt component in src/renderer/features/speckit/components/initialization-prompt.tsx with heading "Initialize SpecKit" and description +- [ ] T126 Add initialization detection logic to PlanPage checking initStatus.initialized before rendering constitution/features sections in src/renderer/features/speckit/components/plan-page.tsx +- [ ] T127 Implement "Initialize SpecKit" button in InitializationPrompt calling trpc.speckit.initializeSpecKit in src/renderer/features/speckit/components/initialization-prompt.tsx +- [ ] T128 Add loading state and progress indicator during initialization in InitializationPrompt in src/renderer/features/speckit/components/initialization-prompt.tsx +- [ ] T129 Implement initialization success handling refreshing Plan page UI to show constitution/features sections in src/renderer/features/speckit/components/initialization-prompt.tsx +- [ ] T130 Add initialization error handling showing error message from ii-spec with retry button in src/renderer/features/speckit/components/initialization-prompt.tsx +- [ ] T131 Add partial initialization detection showing "Re-initialize SpecKit" with missing components list in src/renderer/features/speckit/components/initialization-prompt.tsx + +**Checkpoint**: Initialization detection complete - users can initialize SpecKit with one click + +--- + +## Phase 9: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +### Performance Optimization + +- [ ] T132 [P] Implement features list pagination in FeaturesTable using limit/offset from trpc.speckit.getFeaturesList in src/renderer/features/speckit/components/features-table.tsx +- [ ] T133 [P] Add React.memo() to MarkdownView component preventing unnecessary re-renders in src/renderer/features/speckit/components/markdown-view.tsx +- [ ] T134 [P] Implement artifact content caching in DocumentPane using React Query cache in src/renderer/features/speckit/components/document-pane.tsx +- [ ] T135 [P] Add debouncing to workflow state polling reducing query frequency in src/renderer/features/speckit/hooks/use-workflow-state.ts + +### Accessibility + +- [ ] T136 [P] Add ARIA labels to SpecKit icon button in src/renderer/features/layout/top-action-bar.tsx +- [ ] T137 [P] Add keyboard shortcuts for workflow modal (Esc to close) in src/renderer/features/speckit/components/workflow-modal.tsx +- [ ] T138 [P] Implement focus management in modal dialogs trapping focus within modal in src/renderer/features/speckit/components/workflow-modal.tsx and src/renderer/features/speckit/components/feature-detail-modal.tsx +- [ ] T139 [P] Add screen reader announcements for workflow step transitions in src/renderer/features/speckit/components/workflow-stepper.tsx + +### Error Handling & Edge Cases + +- [ ] T140 [P] Add error boundary around PlanPage catching rendering errors in src/renderer/features/speckit/components/plan-page.tsx +- [ ] T141 [P] Implement graceful degradation when Git operations fail showing user-friendly error messages in all components calling Git procedures +- [ ] T142 [P] Add file watcher integration refreshing features list when specs/ directory changes using trpc.speckit.watchDirectory in src/renderer/features/speckit/components/plan-page.tsx +- [ ] T143 [P] Handle corrupted spec.md files showing parsing error instead of crashing in src/main/lib/speckit/state-detector.ts + +### Documentation & Developer Experience + +- [ ] T144 [P] Add JSDoc comments to all tRPC procedures in src/main/lib/trpc/routers/speckit.ts +- [ ] T145 [P] Add inline code comments explaining workflow state detection logic in src/main/lib/speckit/state-detector.ts +- [ ] T146 [P] Add component props TypeScript interfaces with JSDoc in all new components +- [ ] T147 Update quickstart.md with actual implementation file paths and verification steps + +### Code Cleanup + +- [ ] T148 [P] Remove any TODO comments and replace with proper implementations or GitHub issues +- [ ] T149 [P] Run linter and fix all warnings in src/main/lib/speckit/ and src/renderer/features/speckit/ +- [ ] T150 [P] Verify all imports use absolute paths via aliases (e.g., @/features/speckit) not relative paths +- [ ] T151 Run quickstart.md validation steps ensuring all commands execute successfully + +**Checkpoint**: Polish complete - feature ready for production use + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 0 (Submodule Relocation)**: No dependencies - MUST complete first (BLOCKING) +- **Phase 1 (Dependencies)**: Depends on Phase 0 completion +- **Phase 2 (Foundational)**: Depends on Phase 1 completion - BLOCKS all user stories +- **Phase 3 (US1 - Access)**: Depends on Phase 2 completion - Can run in parallel with other user stories +- **Phase 4 (US4 - Workflow)**: Depends on Phase 2 completion - Can run in parallel with US1, US2, US3 +- **Phase 5 (US2 - Constitution)**: Depends on Phase 2 completion - Can run in parallel with US1, US3, US4 +- **Phase 6 (US3 - Features)**: Depends on Phase 2 completion - Can run in parallel with US1, US2, US4 +- **Phase 7 (US5 - Submodule)**: Depends on Phase 0 completion - Verification only, can run anytime after Phase 0 +- **Phase 8 (Initialization)**: Depends on Phase 2 completion - Should complete before US2/US3/US4 integration testing +- **Phase 9 (Polish)**: Depends on all desired user stories being complete + +### User Story Dependencies + +- **User Story 1 (US1 - Access)**: Depends on Phase 2 - No dependencies on other stories - PROVIDES drawer access for US2/US3/US4 +- **User Story 4 (US4 - Workflow)**: Depends on Phase 2 and US1 (needs drawer) - Core value proposition +- **User Story 2 (US2 - Constitution)**: Depends on Phase 2 and US1 (needs Plan page) - Independent from US3/US4 +- **User Story 3 (US3 - Features)**: Depends on Phase 2 and US1 (needs Plan page) - Independent from US2/US4 +- **User Story 5 (US5 - Submodule)**: Depends on Phase 0 - Verification only, independent from all other stories + +### Within Each Phase + +**Phase 2 (Foundational)**: +- Backend utilities (T014-T024) can run in parallel after directory creation +- tRPC procedures (T026-T041) depend on backend utilities being complete +- Frontend types (T043-T052) can run in parallel with backend work +- Frontend atoms (T053-T055) can run in parallel with types +- Shared components (T056-T057) depend on types being complete + +**Phase 3 (US1)**: +- All tasks after component directory creation can proceed sequentially +- T061-T062 and T063-T064 can run in parallel (different files) + +**Phase 4 (US4)**: +- Modal components (T066-T071) can run in parallel after skeleton creation +- Hooks (T072-T074) can run in parallel +- Workflow step components (T075-T083) can run in parallel after hooks complete +- Command execution (T084-T087) depends on hooks +- Live preview (T088-T090) can run in parallel with command execution +- Error handling (T091-T093) can run in parallel with other modal features +- Integration (T094-T096) runs last after modal complete + +**Phase 5 (US2)**: +- T097-T099 can run together (same component) +- T100-T103 can run in parallel with T104-T105 (different modals) +- T106 runs last (integration) + +**Phase 6 (US3)**: +- T107-T112 can run together (same table component) +- T113-T118 can run together (same modal component) +- T119 runs last (integration) + +**Phase 8 (Initialization)**: +- T125-T126 can run in parallel +- T127-T131 run sequentially (same component logic) + +**Phase 9 (Polish)**: +- All performance tasks (T132-T135) can run in parallel +- All accessibility tasks (T136-T139) can run in parallel +- All error handling tasks (T140-T143) can run in parallel +- All documentation tasks (T144-T147) can run in parallel +- All cleanup tasks (T148-T151) can run in parallel + +### Parallel Opportunities + +**Maximum Parallelization After Phase 2 Complete**: +```bash +# Four developers can work simultaneously: +Developer A: Phase 3 (US1 - Access) → T058-T065 +Developer B: Phase 4 (US4 - Workflow) → T066-T096 +Developer C: Phase 5 (US2 - Constitution) → T097-T106 +Developer D: Phase 6 (US3 - Features) → T107-T119 +``` + +**Within Phase 2 (Foundational) - Two developers can parallelize**: +```bash +Developer A: Backend (T014-T042) +Developer B: Frontend types + atoms + shared components (T043-T057) +``` + +--- + +## Implementation Strategy + +### MVP First (US1 + US4 Only) + +**Goal**: Deliver core SpecKit functionality in fastest path to value + +1. Complete Phase 0: Submodule Relocation (T001-T006) +2. Complete Phase 1: Dependencies (T007-T013) +3. Complete Phase 2: Foundational (T014-T057) - CRITICAL BLOCKER +4. Complete Phase 3: User Story 1 (T058-T065) - Access to UI +5. Complete Phase 8: Initialization (T125-T131) - Required before US4 works +6. Complete Phase 4: User Story 4 (T066-T096) - Core workflow +7. **STOP and VALIDATE**: Create a new feature end-to-end using the workflow +8. Fix critical bugs if found +9. Demo/Deploy MVP + +**Estimated Tasks**: 131 tasks for MVP (T001-T065 + T125-T131 + T066-T096) + +### Full Feature (All User Stories) + +**Goal**: Complete all P1, P2, P3 user stories in priority order + +1. Complete MVP (above) +2. Complete Phase 5: User Story 2 (T097-T106) - Constitution viewing +3. Complete Phase 6: User Story 3 (T107-T119) - Features browsing +4. Complete Phase 7: User Story 5 (T120-T124) - Submodule verification +5. Complete Phase 9: Polish (T132-T151) - Production readiness +6. **STOP and VALIDATE**: Test all user stories independently +7. Run full E2E testing +8. Fix all bugs +9. Production deployment + +**Total Tasks**: 151 tasks + +### Parallel Team Strategy + +With 4 developers after Phase 2 complete: + +1. **Week 1**: All developers complete Phase 0-2 together (T001-T057) → Foundation ready +2. **Week 2-3**: Parallel user story development + - Dev A: US1 (T058-T065) + US5 verification (T120-T124) + - Dev B: US4 (T066-T096) + Initialization (T125-T131) + - Dev C: US2 (T097-T106) + Polish docs (T144-T147) + - Dev D: US3 (T107-T119) + Polish performance (T132-T135) +3. **Week 4**: Integration + Polish + - All devs: Cross-story testing + remaining polish tasks (T136-T151) + - Bug fixes and final validation + +--- + +## Notes + +- [P] tasks = different files/components, can execute in parallel +- [Story] label maps task to specific user story for traceability +- Each user story should be independently testable after completion +- Phase 2 (Foundational) is CRITICAL - blocks all user stories +- US1 provides access point - should complete before US2/US3/US4 can be tested +- US4 is highest value (P1) but requires US1 + Phase 8 (Initialization) +- All file paths are absolute from repository root +- **Total tasks: 163** (10 phases + 5 user stories + polish) +- **Workflow Completion**: tasks.md exists = workflow at "implement" step (complete) +- **Implement Step**: Shows task list with copy buttons; user runs `/speckit.implement [task-id]` in new chat +- **Free Navigation**: Users can click any completed stepper step to return and modify +- **Stale Warnings**: Non-blocking banners appear when navigating backward with downstream artifacts +- **Skip Clarify Warning**: Shows warning when skipping clarify, but allows user to continue +- Commit after each logical group of tasks +- Stop at any checkpoint to validate independently From 56e83646b5a3e3407131b3fc1b4a91c9e9cd7b10 Mon Sep 17 00:00:00 2001 From: Sameeran Bandishti Date: Mon, 2 Feb 2026 11:57:36 +0530 Subject: [PATCH 04/15] feat(speckit): implement Phase 4 workflow modal and step components Phase 4 implements User Story 4 - Create New Feature Workflow (P1 MVP): Workflow Modal: - Full-screen dialog with dual-pane layout (chat + document) - Workflow stepper with clickable navigation for completed steps - Real-time command output streaming with cancel support - Live artifact preview with tabs (Spec, Plan, Research, Tasks) Step Components: - ConstitutionStep: View/create project constitution - SpecifyStep: Feature description input form with validation - ClarifyStep: Clarification questions with option buttons - PlanStep: Plan generation with approve/regenerate actions - TasksStep: Task breakdown generation - ImplementStep: Task list with copy-to-clipboard for each task Supporting Features: - useWorkflowState, useExecuteCommand, useCommandOutput hooks - StaleWarningBanner for downstream artifact warnings - SkipClarifyWarningBanner for skipping clarification - Error handling with retry capability in ChatPane - Workflow button added to PlanPage header Tasks completed: T066-T096 Co-Authored-By: Claude Opus 4.5 --- specs/001-speckit-ui-integration/tasks.md | 190 ++++---- src/renderer/features/speckit/atoms/index.ts | 56 +++ .../features/speckit/components/chat-pane.tsx | 220 +++++++++ .../speckit/components/document-pane.tsx | 253 ++++++++++ .../speckit/components/markdown-view.tsx | 56 +++ .../features/speckit/components/plan-page.tsx | 353 ++++++++++++++ .../components/skip-clarify-warning.tsx | 132 +++++ .../speckit/components/speckit-sidebar.tsx | 98 ++++ .../components/stale-warning-banner.tsx | 105 ++++ .../speckit/components/workflow-modal.tsx | 454 ++++++++++++++++++ .../speckit/components/workflow-stepper.tsx | 264 ++++++++++ .../workflow-steps/clarify-step.tsx | 221 +++++++++ .../workflow-steps/constitution-step.tsx | 152 ++++++ .../workflow-steps/implement-step.tsx | 332 +++++++++++++ .../components/workflow-steps/index.ts | 12 + .../components/workflow-steps/plan-step.tsx | 170 +++++++ .../workflow-steps/specify-step.tsx | 157 ++++++ .../components/workflow-steps/tasks-step.tsx | 169 +++++++ src/renderer/features/speckit/hooks/index.ts | 9 + .../speckit/hooks/use-command-output.ts | 149 ++++++ .../speckit/hooks/use-execute-command.ts | 137 ++++++ .../speckit/hooks/use-workflow-state.ts | 76 +++ .../features/speckit/types/constitution.ts | 22 + .../features/speckit/types/feature.ts | 64 +++ src/renderer/features/speckit/types/index.ts | 50 ++ .../features/speckit/types/initialization.ts | 21 + .../features/speckit/types/ui-models.ts | 69 +++ .../features/speckit/types/workflow-state.ts | 91 ++++ .../speckit/utils/constitution-parser.ts | 81 ++++ 29 files changed, 4068 insertions(+), 95 deletions(-) create mode 100644 src/renderer/features/speckit/atoms/index.ts create mode 100644 src/renderer/features/speckit/components/chat-pane.tsx create mode 100644 src/renderer/features/speckit/components/document-pane.tsx create mode 100644 src/renderer/features/speckit/components/markdown-view.tsx create mode 100644 src/renderer/features/speckit/components/plan-page.tsx create mode 100644 src/renderer/features/speckit/components/skip-clarify-warning.tsx create mode 100644 src/renderer/features/speckit/components/speckit-sidebar.tsx create mode 100644 src/renderer/features/speckit/components/stale-warning-banner.tsx create mode 100644 src/renderer/features/speckit/components/workflow-modal.tsx create mode 100644 src/renderer/features/speckit/components/workflow-stepper.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/constitution-step.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/implement-step.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/index.ts create mode 100644 src/renderer/features/speckit/components/workflow-steps/plan-step.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/specify-step.tsx create mode 100644 src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx create mode 100644 src/renderer/features/speckit/hooks/index.ts create mode 100644 src/renderer/features/speckit/hooks/use-command-output.ts create mode 100644 src/renderer/features/speckit/hooks/use-execute-command.ts create mode 100644 src/renderer/features/speckit/hooks/use-workflow-state.ts create mode 100644 src/renderer/features/speckit/types/constitution.ts create mode 100644 src/renderer/features/speckit/types/feature.ts create mode 100644 src/renderer/features/speckit/types/index.ts create mode 100644 src/renderer/features/speckit/types/initialization.ts create mode 100644 src/renderer/features/speckit/types/ui-models.ts create mode 100644 src/renderer/features/speckit/types/workflow-state.ts create mode 100644 src/renderer/features/speckit/utils/constitution-parser.ts diff --git a/specs/001-speckit-ui-integration/tasks.md b/specs/001-speckit-ui-integration/tasks.md index 7a4041009..2af19ad21 100644 --- a/specs/001-speckit-ui-integration/tasks.md +++ b/specs/001-speckit-ui-integration/tasks.md @@ -62,62 +62,62 @@ ### Backend Utilities -- [ ] T014 [P] Create src/main/lib/speckit/ directory for ii-spec integration utilities -- [ ] T015 [P] Implement getCurrentBranch() function in src/main/lib/speckit/file-utils.ts to read current Git branch using execSync -- [ ] T016 [P] Implement parseFeatureBranch() function in src/main/lib/speckit/file-utils.ts to extract feature number and name from branch pattern /^(\d{3})-(.+)$/ -- [ ] T017 [P] Implement checkFileExists() function in src/main/lib/speckit/file-utils.ts using fs.existsSync() -- [ ] T018 [P] Implement readFileContent() function in src/main/lib/speckit/file-utils.ts using fs.readFileSync() -- [ ] T019 [P] Implement listFeatureDirectories() function in src/main/lib/speckit/file-utils.ts to list specs/ directory and filter by /^\d{3}-/ pattern -- [ ] T020 [P] Implement detectWorkflowState() function in src/main/lib/speckit/state-detector.ts using file-based state detection logic from plan.md -- [ ] T021 [P] Implement parseClarificationQuestions() function in src/main/lib/speckit/state-detector.ts to extract [NEEDS CLARIFICATION: ...] markers from spec.md -- [ ] T022 [P] Implement executeCommand() function in src/main/lib/speckit/command-executor.ts to spawn ii-spec commands using child_process.spawn() -- [ ] T023 [P] Implement getExecutionEmitter() function in src/main/lib/speckit/command-executor.ts to retrieve EventEmitter for streaming output -- [ ] T024 [P] Implement cancelExecution() function in src/main/lib/speckit/command-executor.ts to kill running subprocess +- [X] T014 [P] Create src/main/lib/speckit/ directory for ii-spec integration utilities +- [X] T015 [P] Implement getCurrentBranch() function in src/main/lib/speckit/file-utils.ts to read current Git branch using execSync +- [X] T016 [P] Implement parseFeatureBranch() function in src/main/lib/speckit/file-utils.ts to extract feature number and name from branch pattern /^(\d{3})-(.+)$/ +- [X] T017 [P] Implement checkFileExists() function in src/main/lib/speckit/file-utils.ts using fs.existsSync() +- [X] T018 [P] Implement readFileContent() function in src/main/lib/speckit/file-utils.ts using fs.readFileSync() +- [X] T019 [P] Implement listFeatureDirectories() function in src/main/lib/speckit/file-utils.ts to list specs/ directory and filter by /^\d{3}-/ pattern +- [X] T020 [P] Implement detectWorkflowState() function in src/main/lib/speckit/state-detector.ts using file-based state detection logic from plan.md +- [X] T021 [P] Implement parseClarificationQuestions() function in src/main/lib/speckit/state-detector.ts to extract [NEEDS CLARIFICATION: ...] markers from spec.md +- [X] T022 [P] Implement executeCommand() function in src/main/lib/speckit/command-executor.ts to spawn ii-spec commands using child_process.spawn() +- [X] T023 [P] Implement getExecutionEmitter() function in src/main/lib/speckit/command-executor.ts to retrieve EventEmitter for streaming output +- [X] T024 [P] Implement cancelExecution() function in src/main/lib/speckit/command-executor.ts to kill running subprocess ### tRPC Router (15 Procedures) -- [ ] T025 Create src/main/lib/trpc/routers/speckit.ts with router skeleton and Zod schemas from contracts/trpc-router.ts -- [ ] T026 [P] Implement checkInitialization procedure in src/main/lib/trpc/routers/speckit.ts checking .specify/ directory structure -- [ ] T027 [P] Implement initializeSpecKit procedure in src/main/lib/trpc/routers/speckit.ts executing `specify init . --ai claude` -- [ ] T028 Implement getWorkflowState procedure in src/main/lib/trpc/routers/speckit.ts using detectWorkflowState() from state-detector.ts -- [ ] T029 [P] Implement getConstitution procedure in src/main/lib/trpc/routers/speckit.ts reading .specify/memory/constitution.md -- [ ] T030 [P] Implement getFeaturesList procedure in src/main/lib/trpc/routers/speckit.ts using listFeatureDirectories() and reading descriptions from spec.md files -- [ ] T031 [P] Implement getArtifact procedure in src/main/lib/trpc/routers/speckit.ts reading specs/{branch}/{artifactType}.md files -- [ ] T032 [P] Implement getFeatureDescription procedure in src/main/lib/trpc/routers/speckit.ts parsing spec.md first paragraph -- [ ] T033 Implement executeCommand procedure in src/main/lib/trpc/routers/speckit.ts using executeCommand() from command-executor.ts -- [ ] T034 Implement onCommandOutput subscription in src/main/lib/trpc/routers/speckit.ts using observable pattern and getExecutionEmitter() -- [ ] T035 [P] Implement cancelCommand procedure in src/main/lib/trpc/routers/speckit.ts using cancelExecution() -- [ ] T036 [P] Implement getCurrentBranch procedure in src/main/lib/trpc/routers/speckit.ts using getCurrentBranch() from file-utils.ts -- [ ] T037 [P] Implement getFeatureBranches procedure in src/main/lib/trpc/routers/speckit.ts listing all branches matching /^\d{3}-/ pattern -- [ ] T038 [P] Implement switchBranch procedure in src/main/lib/trpc/routers/speckit.ts executing `git checkout ` -- [ ] T039 [P] Implement openFileInEditor procedure in src/main/lib/trpc/routers/speckit.ts using Electron shell.openPath() -- [ ] T040 [P] Implement watchDirectory procedure in src/main/lib/trpc/routers/speckit.ts using fs.watch() on specs/ or .specify/ -- [ ] T041 [P] Implement onFileChange subscription in src/main/lib/trpc/routers/speckit.ts emitting file change events -- [ ] T042 Register speckit router in src/main/lib/trpc/index.ts appRouter +- [X] T025 Create src/main/lib/trpc/routers/speckit.ts with router skeleton and Zod schemas from contracts/trpc-router.ts +- [X] T026 [P] Implement checkInitialization procedure in src/main/lib/trpc/routers/speckit.ts checking .specify/ directory structure +- [X] T027 [P] Implement initializeSpecKit procedure in src/main/lib/trpc/routers/speckit.ts executing `specify init . --ai claude` +- [X] T028 Implement getWorkflowState procedure in src/main/lib/trpc/routers/speckit.ts using detectWorkflowState() from state-detector.ts +- [X] T029 [P] Implement getConstitution procedure in src/main/lib/trpc/routers/speckit.ts reading .specify/memory/constitution.md +- [X] T030 [P] Implement getFeaturesList procedure in src/main/lib/trpc/routers/speckit.ts using listFeatureDirectories() and reading descriptions from spec.md files +- [X] T031 [P] Implement getArtifact procedure in src/main/lib/trpc/routers/speckit.ts reading specs/{branch}/{artifactType}.md files +- [X] T032 [P] Implement getFeatureDescription procedure in src/main/lib/trpc/routers/speckit.ts parsing spec.md first paragraph +- [X] T033 Implement executeCommand procedure in src/main/lib/trpc/routers/speckit.ts using executeCommand() from command-executor.ts +- [X] T034 Implement onCommandOutput subscription in src/main/lib/trpc/routers/speckit.ts using observable pattern and getExecutionEmitter() +- [X] T035 [P] Implement cancelCommand procedure in src/main/lib/trpc/routers/speckit.ts using cancelExecution() +- [X] T036 [P] Implement getCurrentBranch procedure in src/main/lib/trpc/routers/speckit.ts using getCurrentBranch() from file-utils.ts +- [X] T037 [P] Implement getFeatureBranches procedure in src/main/lib/trpc/routers/speckit.ts listing all branches matching /^\d{3}-/ pattern +- [X] T038 [P] Implement switchBranch procedure in src/main/lib/trpc/routers/speckit.ts executing `git checkout ` +- [X] T039 [P] Implement openFileInEditor procedure in src/main/lib/trpc/routers/speckit.ts using Electron shell.openPath() +- [X] T040 [P] Implement watchDirectory procedure in src/main/lib/trpc/routers/speckit.ts using fs.watch() on specs/ or .specify/ +- [X] T041 [P] Implement onFileChange subscription in src/main/lib/trpc/routers/speckit.ts emitting file change events +- [X] T042 Register speckit router in src/main/lib/trpc/index.ts appRouter ### Frontend Types (Shared by All Stories) -- [ ] T043 [P] Create src/renderer/features/speckit/types/ directory -- [ ] T044 [P] Create ArtifactPresenceSchema and type in src/renderer/features/speckit/types/feature.ts -- [ ] T045 [P] Create SpecKitFeatureSchema and type in src/renderer/features/speckit/types/feature.ts with Zod validation -- [ ] T046 [P] Create ConstitutionSchema and type in src/renderer/features/speckit/types/constitution.ts -- [ ] T047 [P] Create WorkflowStateSchema and type in src/renderer/features/speckit/types/workflow-state.ts with all workflow step names -- [ ] T048 [P] Create ClarificationQuestionSchema and type in src/renderer/features/speckit/types/workflow-state.ts -- [ ] T049 [P] Create InitializationStatusSchema and type in src/renderer/features/speckit/types/initialization.ts -- [ ] T050 [P] Create FeatureTableRow interface in src/renderer/features/speckit/types/ui-models.ts -- [ ] T051 [P] Create ConstitutionPreview interface in src/renderer/features/speckit/types/ui-models.ts -- [ ] T052 Export all types from src/renderer/features/speckit/types/index.ts +- [X] T043 [P] Create src/renderer/features/speckit/types/ directory +- [X] T044 [P] Create ArtifactPresenceSchema and type in src/renderer/features/speckit/types/feature.ts +- [X] T045 [P] Create SpecKitFeatureSchema and type in src/renderer/features/speckit/types/feature.ts with Zod validation +- [X] T046 [P] Create ConstitutionSchema and type in src/renderer/features/speckit/types/constitution.ts +- [X] T047 [P] Create WorkflowStateSchema and type in src/renderer/features/speckit/types/workflow-state.ts with all workflow step names +- [X] T048 [P] Create ClarificationQuestionSchema and type in src/renderer/features/speckit/types/workflow-state.ts +- [X] T049 [P] Create InitializationStatusSchema and type in src/renderer/features/speckit/types/initialization.ts +- [X] T050 [P] Create FeatureTableRow interface in src/renderer/features/speckit/types/ui-models.ts +- [X] T051 [P] Create ConstitutionPreview interface in src/renderer/features/speckit/types/ui-models.ts +- [X] T052 Export all types from src/renderer/features/speckit/types/index.ts ### Frontend Atoms (Shared UI State) -- [ ] T053 Create src/renderer/features/speckit/atoms/index.ts with speckitModalOpenAtom (boolean) -- [ ] T054 [P] Add speckitCurrentDocumentAtom to src/renderer/features/speckit/atoms/index.ts with type { type, content } | null -- [ ] T055 [P] Add speckitLoadingAtom to src/renderer/features/speckit/atoms/index.ts (boolean) +- [X] T053 Create src/renderer/features/speckit/atoms/index.ts with speckitModalOpenAtom (boolean) +- [X] T054 [P] Add speckitCurrentDocumentAtom to src/renderer/features/speckit/atoms/index.ts with type { type, content } | null +- [X] T055 [P] Add speckitLoadingAtom to src/renderer/features/speckit/atoms/index.ts (boolean) ### Shared Components & Utilities -- [ ] T056 [P] Create extractPrincipleNames() utility function in src/renderer/features/speckit/utils/constitution-parser.ts to extract principle headers from markdown -- [ ] T057 [P] Create markdown rendering component MarkdownView in src/renderer/features/speckit/components/markdown-view.tsx using react-markdown with remark-gfm and syntax highlighting +- [X] T056 [P] Create extractPrincipleNames() utility function in src/renderer/features/speckit/utils/constitution-parser.ts to extract principle headers from markdown +- [X] T057 [P] Create markdown rendering component MarkdownView in src/renderer/features/speckit/components/markdown-view.tsx using react-markdown with remark-gfm and syntax highlighting **Checkpoint**: Foundation complete - all user stories can now proceed in parallel @@ -131,14 +131,14 @@ ### Implementation for User Story 1 -- [ ] T058 [P] [US1] Create src/renderer/features/speckit/components/ directory -- [ ] T059 [US1] Create PlanPage component skeleton in src/renderer/features/speckit/components/plan-page.tsx with basic layout structure -- [ ] T060 [US1] Add SpecKit icon button (FileText from lucide-react) to top action bar in src/renderer/features/layout/top-action-bar.tsx in same group as Git and Terminal buttons -- [ ] T061 [US1] Create drawer state atom speckitDrawerOpenAtom in src/renderer/features/speckit/atoms/index.ts or reuse existing drawer atoms -- [ ] T062 [US1] Wire SpecKit icon button onClick to toggle drawer open/closed in src/renderer/features/layout/top-action-bar.tsx -- [ ] T063 [US1] Add 'speckit' case to drawer content switch statement in src/renderer/features/layout/drawer-content.tsx rendering PlanPage component -- [ ] T064 [US1] Implement drawer content switching logic to show PlanPage when activeView is 'speckit' in src/renderer/features/layout/drawer-content.tsx -- [ ] T065 [US1] Add toggle behavior so clicking SpecKit button when drawer is open with Plan page closes the drawer in src/renderer/features/layout/top-action-bar.tsx +- [X] T058 [P] [US1] Create src/renderer/features/speckit/components/ directory +- [X] T059 [US1] Create PlanPage component skeleton in src/renderer/features/speckit/components/plan-page.tsx with basic layout structure +- [X] T060 [US1] Add SpecKit icon button (FileText from lucide-react) to sub-chat-selector.tsx in same group as Terminal button +- [X] T061 [US1] Create drawer state atom speckitDrawerOpenAtom in src/renderer/features/speckit/atoms/index.ts +- [X] T062 [US1] Wire SpecKit icon button onClick to toggle drawer open/closed via props from active-chat.tsx +- [X] T063 [US1] Create SpecKitSidebar component in src/renderer/features/speckit/components/speckit-sidebar.tsx using ResizableSidebar pattern +- [X] T064 [US1] Add SpecKitSidebar to active-chat.tsx rendering PlanPage when drawer is open +- [X] T065 [US1] Add toggle behavior so clicking SpecKit button when drawer is open closes it **Checkpoint**: User Story 1 complete - SpecKit icon button opens Plan page in right drawer with toggle functionality @@ -154,76 +154,76 @@ ### Workflow Modal (Full-Screen Interface) -- [ ] T066 [P] [US4] Create WorkflowModal component in src/renderer/features/speckit/components/workflow-modal.tsx with full-screen dialog layout -- [ ] T067 [P] [US4] Create workflow stepper UI in src/renderer/features/speckit/components/workflow-stepper.tsx showing: Constitution | Specify | Clarify | Plan | Tasks | Implement -- [ ] T068 [US4] Add dual-pane layout to WorkflowModal with chat pane (left) and document pane (right) in src/renderer/features/speckit/components/workflow-modal.tsx -- [ ] T069 [US4] Create chat pane component ChatPane in src/renderer/features/speckit/components/chat-pane.tsx for command execution and output streaming -- [ ] T070 [US4] Create document pane component DocumentPane in src/renderer/features/speckit/components/document-pane.tsx for live artifact preview using MarkdownView -- [ ] T071 [US4] Wire speckitModalOpenAtom to WorkflowModal open/close state in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T066 [P] [US4] Create WorkflowModal component in src/renderer/features/speckit/components/workflow-modal.tsx with full-screen dialog layout +- [X] T067 [P] [US4] Create workflow stepper UI in src/renderer/features/speckit/components/workflow-stepper.tsx showing: Constitution | Specify | Clarify | Plan | Tasks | Implement +- [X] T068 [US4] Add dual-pane layout to WorkflowModal with chat pane (left) and document pane (right) in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T069 [US4] Create chat pane component ChatPane in src/renderer/features/speckit/components/chat-pane.tsx for command execution and output streaming +- [X] T070 [US4] Create document pane component DocumentPane in src/renderer/features/speckit/components/document-pane.tsx for live artifact preview using MarkdownView +- [X] T071 [US4] Wire speckitModalOpenAtom to WorkflowModal open/close state in src/renderer/features/speckit/components/workflow-modal.tsx ### Workflow State Management -- [ ] T072 [P] [US4] Create useWorkflowState custom hook in src/renderer/features/speckit/hooks/use-workflow-state.ts wrapping trpc.speckit.getWorkflowState.useQuery -- [ ] T073 [P] [US4] Create useExecuteCommand custom hook in src/renderer/features/speckit/hooks/use-execute-command.ts wrapping trpc.speckit.executeCommand.useMutation -- [ ] T074 [P] [US4] Create useCommandOutput custom hook in src/renderer/features/speckit/hooks/use-command-output.ts wrapping trpc.speckit.onCommandOutput.useSubscription +- [X] T072 [P] [US4] Create useWorkflowState custom hook in src/renderer/features/speckit/hooks/use-workflow-state.ts wrapping trpc.speckit.getWorkflowState.useQuery +- [X] T073 [P] [US4] Create useExecuteCommand custom hook in src/renderer/features/speckit/hooks/use-execute-command.ts wrapping trpc.speckit.executeCommand.useMutation +- [X] T074 [P] [US4] Create useCommandOutput custom hook in src/renderer/features/speckit/hooks/use-command-output.ts wrapping trpc.speckit.onCommandOutput.useSubscription ### Workflow Steps Implementation -- [ ] T075 [US4] Create SpecifyStep component in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx with feature description input form -- [ ] T076 [US4] Implement form submit handler in SpecifyStep calling useExecuteCommand with `/speckit.specify` command in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx -- [ ] T077 [US4] Create ClarifyStep component in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx displaying clarification questions from WorkflowState -- [ ] T078 [US4] Implement clarification question answer form in ClarifyStep with textarea for each question in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx -- [ ] T079 [US4] Implement form submit handler in ClarifyStep calling useExecuteCommand with `/speckit.clarify` command and answers in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx -- [ ] T080 [US4] Create PlanStep component in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx with auto-initiate plan generation button -- [ ] T081 [US4] Implement plan approval UI in PlanStep showing generated plan.md with approve/regenerate actions in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx -- [ ] T082 [US4] Create TasksStep component in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx with auto-generate tasks button -- [ ] T083 [US4] Implement tasks generation completion UI in TasksStep showing success message and link to tasks.md in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx +- [X] T075 [US4] Create SpecifyStep component in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx with feature description input form +- [X] T076 [US4] Implement form submit handler in SpecifyStep calling useExecuteCommand with `/speckit.specify` command in src/renderer/features/speckit/components/workflow-steps/specify-step.tsx +- [X] T077 [US4] Create ClarifyStep component in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx displaying clarification questions from WorkflowState +- [X] T078 [US4] Implement clarification question answer form in ClarifyStep with textarea for each question in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx +- [X] T079 [US4] Implement form submit handler in ClarifyStep calling useExecuteCommand with `/speckit.clarify` command and answers in src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx +- [X] T080 [US4] Create PlanStep component in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx with auto-initiate plan generation button +- [X] T081 [US4] Implement plan approval UI in PlanStep showing generated plan.md with approve/regenerate actions in src/renderer/features/speckit/components/workflow-steps/plan-step.tsx +- [X] T082 [US4] Create TasksStep component in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx with auto-generate tasks button +- [X] T083 [US4] Implement tasks generation completion UI in TasksStep showing success message and link to tasks.md in src/renderer/features/speckit/components/workflow-steps/tasks-step.tsx ### Command Execution & Output Streaming -- [ ] T084 [US4] Implement real-time command output streaming display in ChatPane using useCommandOutput hook in src/renderer/features/speckit/components/chat-pane.tsx -- [ ] T085 [US4] Add stdout/stderr differentiation styling in ChatPane (stdout: normal, stderr: error red) in src/renderer/features/speckit/components/chat-pane.tsx -- [ ] T086 [US4] Implement command cancellation button in ChatPane calling trpc.speckit.cancelCommand in src/renderer/features/speckit/components/chat-pane.tsx -- [ ] T087 [US4] Add loading/executing state indicators with progress spinners in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T084 [US4] Implement real-time command output streaming display in ChatPane using useCommandOutput hook in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T085 [US4] Add stdout/stderr differentiation styling in ChatPane (stdout: normal, stderr: error red) in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T086 [US4] Implement command cancellation button in ChatPane calling trpc.speckit.cancelCommand in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T087 [US4] Add loading/executing state indicators with progress spinners in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx ### Live Artifact Preview -- [ ] T088 [US4] Implement auto-refresh artifact content in DocumentPane polling trpc.speckit.getArtifact when workflow step completes in src/renderer/features/speckit/components/document-pane.tsx -- [ ] T089 [US4] Add artifact type tabs (Spec | Plan | Research | Tasks) to DocumentPane in src/renderer/features/speckit/components/document-pane.tsx -- [ ] T090 [US4] Wire artifact tabs to speckitCurrentDocumentAtom for display in src/renderer/features/speckit/components/document-pane.tsx +- [X] T088 [US4] Implement auto-refresh artifact content in DocumentPane polling trpc.speckit.getArtifact when workflow step completes in src/renderer/features/speckit/components/document-pane.tsx +- [X] T089 [US4] Add artifact type tabs (Spec | Plan | Research | Tasks) to DocumentPane in src/renderer/features/speckit/components/document-pane.tsx +- [X] T090 [US4] Wire artifact tabs to speckitCurrentDocumentAtom for display in src/renderer/features/speckit/components/document-pane.tsx ### Implement Step (Task List with Copy Buttons) -- [ ] T090.1 [US4] Create ImplementStep component in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx with task list layout -- [ ] T090.2 [US4] Implement task list parsing from tasks.md file in ImplementStep using trpc.speckit.getArtifact in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx -- [ ] T090.3 [US4] Display each task with full description (task ID, description text, file paths) in ImplementStep in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx -- [ ] T090.4 [US4] Add copy button per task that copies task reference (e.g., "T001") to clipboard using navigator.clipboard.writeText() in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx -- [ ] T090.5 [US4] Show toast notification on successful copy with message "Task reference copied. Use /speckit.implement [task-id] in a new chat" in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [X] T090.1 [US4] Create ImplementStep component in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx with task list layout +- [X] T090.2 [US4] Implement task list parsing from tasks.md file in ImplementStep using trpc.speckit.getArtifact in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [X] T090.3 [US4] Display each task with full description (task ID, description text, file paths) in ImplementStep in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [X] T090.4 [US4] Add copy button per task that copies task reference (e.g., "T001") to clipboard using navigator.clipboard.writeText() in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx +- [X] T090.5 [US4] Show toast notification on successful copy with message "Task reference copied. Use /speckit.implement [task-id] in a new chat" in src/renderer/features/speckit/components/workflow-steps/implement-step.tsx ### Stale Warning & Skip Warning Banners -- [ ] T090.6 [US4] Create StaleWarningBanner component in src/renderer/features/speckit/components/stale-warning-banner.tsx showing non-blocking warning when downstream artifacts exist -- [ ] T090.7 [US4] Add stale detection logic to WorkflowModal checking if current step has downstream artifacts (e.g., navigating to Specify when plan.md exists) in src/renderer/features/speckit/components/workflow-modal.tsx -- [ ] T090.8 [US4] Create SkipClarifyWarningBanner component in src/renderer/features/speckit/components/skip-clarify-warning.tsx showing warning when user tries to skip Clarify step -- [ ] T090.9 [US4] Add skip detection logic when user clicks Plan step before completing Clarify, showing warning but allowing continue in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T090.6 [US4] Create StaleWarningBanner component in src/renderer/features/speckit/components/stale-warning-banner.tsx showing non-blocking warning when downstream artifacts exist +- [X] T090.7 [US4] Add stale detection logic to WorkflowModal checking if current step has downstream artifacts (e.g., navigating to Specify when plan.md exists) in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T090.8 [US4] Create SkipClarifyWarningBanner component in src/renderer/features/speckit/components/skip-clarify-warning.tsx showing warning when user tries to skip Clarify step +- [X] T090.9 [US4] Add skip detection logic when user clicks Plan step before completing Clarify, showing warning but allowing continue in src/renderer/features/speckit/components/workflow-modal.tsx ### Stepper Navigation (Free Movement Between Completed Steps) -- [ ] T090.10 [US4] Make stepper steps clickable for completed steps in src/renderer/features/speckit/components/workflow-stepper.tsx -- [ ] T090.11 [US4] Implement step navigation handler that checks step completion before allowing navigation in src/renderer/features/speckit/components/workflow-stepper.tsx -- [ ] T090.12 [US4] Integrate stale warning trigger when navigating backward to a previous step in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T090.10 [US4] Make stepper steps clickable for completed steps in src/renderer/features/speckit/components/workflow-stepper.tsx +- [X] T090.11 [US4] Implement step navigation handler that checks step completion before allowing navigation in src/renderer/features/speckit/components/workflow-stepper.tsx +- [X] T090.12 [US4] Integrate stale warning trigger when navigating backward to a previous step in src/renderer/features/speckit/components/workflow-modal.tsx ### Error Handling & Recovery -- [ ] T091 [US4] Implement error message display in ChatPane showing ii-spec errors as-is from stderr in src/renderer/features/speckit/components/chat-pane.tsx -- [ ] T092 [US4] Add error recovery suggestions UI (e.g., "Command failed - try again" with retry button) in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx -- [ ] T093 [US4] Implement workflow step failure handling preserving state and allowing resume in WorkflowModal in src/renderer/features/speckit/components/workflow-modal.tsx +- [X] T091 [US4] Implement error message display in ChatPane showing ii-spec errors as-is from stderr in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T092 [US4] Add error recovery suggestions UI (e.g., "Command failed - try again" with retry button) in ChatPane in src/renderer/features/speckit/components/chat-pane.tsx +- [X] T093 [US4] Implement workflow step failure handling preserving state and allowing resume in WorkflowModal in src/renderer/features/speckit/components/workflow-modal.tsx ### Integration with Plan Page -- [ ] T094 [US4] Add "New Feature" button to PlanPage opening WorkflowModal in src/renderer/features/speckit/components/plan-page.tsx -- [ ] T095 [US4] Wire "New Feature" button onClick to set speckitModalOpenAtom to true in src/renderer/features/speckit/components/plan-page.tsx -- [ ] T096 [US4] Implement features list refresh after workflow completion using React Query invalidation in src/renderer/features/speckit/components/plan-page.tsx +- [X] T094 [US4] Add "New Feature" button to PlanPage opening WorkflowModal in src/renderer/features/speckit/components/plan-page.tsx +- [X] T095 [US4] Wire "New Feature" button onClick to set speckitModalOpenAtom to true in src/renderer/features/speckit/components/plan-page.tsx +- [X] T096 [US4] Implement features list refresh after workflow completion using React Query invalidation in src/renderer/features/speckit/components/plan-page.tsx **Checkpoint**: User Story 4 complete - users can create new features through full guided workflow with real-time feedback diff --git a/src/renderer/features/speckit/atoms/index.ts b/src/renderer/features/speckit/atoms/index.ts new file mode 100644 index 000000000..cf66e6cae --- /dev/null +++ b/src/renderer/features/speckit/atoms/index.ts @@ -0,0 +1,56 @@ +/** + * SpecKit Jotai Atoms + * + * Ephemeral UI state for SpecKit components. + * These atoms track UI-only state - file system is the source of truth. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { atom } from "jotai" +import type { DocumentDisplayState } from "../types" + +/** + * Whether the SpecKit workflow modal is open + */ +export const speckitModalOpenAtom = atom(false) + +/** + * Whether the SpecKit drawer is open + * + * Controls the right drawer showing the Plan page + */ +export const speckitDrawerOpenAtom = atom(false) + +/** + * Current document being displayed in the document pane + * + * Used by the workflow modal to show artifact content + */ +export const speckitCurrentDocumentAtom = atom(null) + +/** + * Loading state for SpecKit operations + */ +export const speckitLoadingAtom = atom(false) + +/** + * Current execution ID for command streaming + * + * Used to track active command execution + */ +export const speckitExecutionIdAtom = atom(null) + +/** + * Currently selected feature for viewing + * + * Used in the features table to track selection + */ +export const speckitSelectedFeatureAtom = atom(null) + +/** + * Active workflow step (for manual navigation) + * + * null means follow the detected step from workflow state + */ +export const speckitActiveStepAtom = atom(null) diff --git a/src/renderer/features/speckit/components/chat-pane.tsx b/src/renderer/features/speckit/components/chat-pane.tsx new file mode 100644 index 000000000..9e07a8889 --- /dev/null +++ b/src/renderer/features/speckit/components/chat-pane.tsx @@ -0,0 +1,220 @@ +/** + * ChatPane Component + * + * Left pane of the workflow modal for command execution and output streaming. + * Displays command output in real-time with stdout/stderr differentiation. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useRef, useEffect } from "react" +import { Terminal, StopCircle, RefreshCw, AlertCircle, CheckCircle2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import { useFormattedOutput } from "../hooks/use-command-output" + +interface OutputLine { + id: string + stream: "stdout" | "stderr" + content: string + timestamp: number +} + +interface ChatPaneProps { + /** Output lines to display */ + outputLines: OutputLine[] + /** Whether command is currently streaming */ + isStreaming: boolean + /** Whether command has completed */ + isComplete: boolean + /** Whether there were errors */ + hasError: boolean + /** Last error message */ + lastError?: string | null + /** Callback to cancel command */ + onCancel?: () => void + /** Callback to retry command */ + onRetry?: () => void + /** Title for the pane */ + title?: string + /** Additional header content */ + headerContent?: React.ReactNode +} + +/** + * ChatPane - Command output display with streaming support + */ +export const ChatPane = memo(function ChatPane({ + outputLines, + isStreaming, + isComplete, + hasError, + lastError, + onCancel, + onRetry, + title = "Command Output", + headerContent, +}: ChatPaneProps) { + const scrollRef = useRef(null) + const formattedLines = useFormattedOutput(outputLines) + + // Auto-scroll to bottom when new output arrives + useEffect(() => { + if (scrollRef.current && isStreaming) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [formattedLines.length, isStreaming]) + + return ( +

+ {/* Header */} +
+
+ + {title} + {isStreaming && ( + + + Running... + + )} + {isComplete && !hasError && ( + + + Complete + + )} + {isComplete && hasError && ( + + + Error + + )} +
+ +
+ {headerContent} + {isStreaming && onCancel && ( + + )} +
+
+ + {/* Output Area */} +
+ {formattedLines.length === 0 && !isStreaming && !lastError && ( +
+
+ +

Waiting for command output...

+
+
+ )} + + {formattedLines.map((line) => ( +
+ {line.content} +
+ ))} + + {/* Streaming indicator */} + {isStreaming && ( +
+ ▌ +
+ )} +
+ + {/* Error Footer */} + {lastError && ( +
+
+ + {lastError} +
+ {onRetry && ( + + )} +
+ )} +
+ ) +}) + +ChatPane.displayName = "ChatPane" + +/** + * Minimal output display for inline use + */ +export const OutputDisplay = memo(function OutputDisplay({ + outputLines, + isStreaming, + maxHeight = 200, +}: { + outputLines: OutputLine[] + isStreaming: boolean + maxHeight?: number +}) { + const scrollRef = useRef(null) + const formattedLines = useFormattedOutput(outputLines) + + useEffect(() => { + if (scrollRef.current && isStreaming) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [formattedLines.length, isStreaming]) + + if (formattedLines.length === 0 && !isStreaming) { + return null + } + + return ( +
+ {formattedLines.map((line) => ( +
+ {line.content} +
+ ))} + {isStreaming && ( + + )} +
+ ) +}) + +OutputDisplay.displayName = "OutputDisplay" diff --git a/src/renderer/features/speckit/components/document-pane.tsx b/src/renderer/features/speckit/components/document-pane.tsx new file mode 100644 index 000000000..7cb81688f --- /dev/null +++ b/src/renderer/features/speckit/components/document-pane.tsx @@ -0,0 +1,253 @@ +/** + * DocumentPane Component + * + * Right pane of the workflow modal for live artifact preview. + * Displays markdown content with tabs for different artifact types. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useMemo } from "react" +import { FileText, ExternalLink, RefreshCw } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" +import { cn } from "@/lib/utils" +import { trpc } from "@/lib/trpc" +import { MarkdownView } from "./markdown-view" +import type { ArtifactType } from "../types" + +interface DocumentPaneProps { + /** Project path */ + projectPath: string + /** Feature branch name */ + featureBranch: string + /** Currently selected artifact type */ + selectedArtifact: ArtifactType + /** Callback when artifact tab changes */ + onArtifactChange?: (artifact: ArtifactType) => void + /** Whether to show the constitution tab */ + showConstitution?: boolean + /** Callback to open file in editor */ + onOpenInEditor?: (filePath: string) => void +} + +/** + * DocumentPane - Artifact preview with tabs + */ +export const DocumentPane = memo(function DocumentPane({ + projectPath, + featureBranch, + selectedArtifact, + onArtifactChange, + showConstitution = true, + onOpenInEditor, +}: DocumentPaneProps) { + // Query artifacts based on branch + const { data: specArtifact, isLoading: specLoading, refetch: refetchSpec } = + trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch, artifactType: "spec" }, + { enabled: !!featureBranch, refetchOnWindowFocus: false } + ) + + const { data: planArtifact, isLoading: planLoading, refetch: refetchPlan } = + trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch, artifactType: "plan" }, + { enabled: !!featureBranch, refetchOnWindowFocus: false } + ) + + const { data: researchArtifact, isLoading: researchLoading, refetch: refetchResearch } = + trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch, artifactType: "research" }, + { enabled: !!featureBranch, refetchOnWindowFocus: false } + ) + + const { data: tasksArtifact, isLoading: tasksLoading, refetch: refetchTasks } = + trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch, artifactType: "tasks" }, + { enabled: !!featureBranch, refetchOnWindowFocus: false } + ) + + const { data: constitution, isLoading: constitutionLoading, refetch: refetchConstitution } = + trpc.speckit.getConstitution.useQuery( + { projectPath }, + { enabled: showConstitution, refetchOnWindowFocus: false } + ) + + // Get current artifact data + const currentArtifact = useMemo(() => { + switch (selectedArtifact) { + case "spec": + return { data: specArtifact, loading: specLoading, refetch: refetchSpec } + case "plan": + return { data: planArtifact, loading: planLoading, refetch: refetchPlan } + case "research": + return { data: researchArtifact, loading: researchLoading, refetch: refetchResearch } + case "tasks": + return { data: tasksArtifact, loading: tasksLoading, refetch: refetchTasks } + case "constitution": + return { + data: constitution ? { content: constitution.content, exists: constitution.exists, filePath: "" } : null, + loading: constitutionLoading, + refetch: refetchConstitution, + } + default: + return { data: null, loading: false, refetch: () => {} } + } + }, [ + selectedArtifact, + specArtifact, specLoading, refetchSpec, + planArtifact, planLoading, refetchPlan, + researchArtifact, researchLoading, refetchResearch, + tasksArtifact, tasksLoading, refetchTasks, + constitution, constitutionLoading, refetchConstitution, + ]) + + // Available tabs based on what exists + const availableTabs = useMemo(() => { + const tabs: { value: ArtifactType; label: string; exists: boolean }[] = [] + + if (showConstitution) { + tabs.push({ + value: "constitution", + label: "Constitution", + exists: constitution?.exists ?? false, + }) + } + + tabs.push( + { value: "spec", label: "Spec", exists: specArtifact?.exists ?? false }, + { value: "plan", label: "Plan", exists: planArtifact?.exists ?? false }, + { value: "research", label: "Research", exists: researchArtifact?.exists ?? false }, + { value: "tasks", label: "Tasks", exists: tasksArtifact?.exists ?? false } + ) + + return tabs + }, [showConstitution, constitution, specArtifact, planArtifact, researchArtifact, tasksArtifact]) + + const handleOpenInEditor = () => { + if (currentArtifact.data?.filePath && onOpenInEditor) { + onOpenInEditor(currentArtifact.data.filePath) + } + } + + return ( +
+ {/* Header with Tabs */} +
+ onArtifactChange?.(value as ArtifactType)} + > + + {availableTabs.map((tab) => ( + + {tab.label} + {tab.exists && ( + + )} + + ))} + + + +
+ + {currentArtifact.data?.filePath && onOpenInEditor && ( + + )} +
+
+ + {/* Content Area */} +
+ {currentArtifact.loading ? ( +
+ +
+ ) : currentArtifact.data?.exists && currentArtifact.data.content ? ( + + ) : ( +
+
+ +

+ {selectedArtifact === "constitution" + ? "Constitution not found" + : `No ${selectedArtifact}.md file yet`} +

+

+ Complete the workflow step to generate this artifact +

+
+
+ )} +
+
+ ) +}) + +DocumentPane.displayName = "DocumentPane" + +/** + * Simplified artifact viewer for single artifact display + */ +export const ArtifactViewer = memo(function ArtifactViewer({ + content, + exists, + isLoading, + emptyMessage = "No content available", +}: { + content?: string + exists?: boolean + isLoading?: boolean + emptyMessage?: string +}) { + if (isLoading) { + return ( +
+ +
+ ) + } + + if (!exists || !content) { + return ( +
+
+ +

{emptyMessage}

+
+
+ ) + } + + return +}) + +ArtifactViewer.displayName = "ArtifactViewer" diff --git a/src/renderer/features/speckit/components/markdown-view.tsx b/src/renderer/features/speckit/components/markdown-view.tsx new file mode 100644 index 000000000..eabd3443b --- /dev/null +++ b/src/renderer/features/speckit/components/markdown-view.tsx @@ -0,0 +1,56 @@ +/** + * MarkdownView Component + * + * Renders markdown content for SpecKit artifacts (spec, plan, tasks, constitution). + * Wraps the existing ChatMarkdownRenderer with memoization for performance. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo } from "react" +import { ChatMarkdownRenderer } from "@/components/chat-markdown-renderer" +import { cn } from "@/lib/utils" + +interface MarkdownViewProps { + /** Markdown content to render */ + content: string + /** Size variant: sm for compact, md for normal, lg for fullscreen */ + size?: "sm" | "md" | "lg" + /** Additional CSS classes */ + className?: string + /** Whether content is being streamed (disables certain optimizations) */ + isStreaming?: boolean +} + +/** + * MarkdownView - Renders markdown content for SpecKit + * + * Uses the existing ChatMarkdownRenderer with additional memoization + * to prevent unnecessary re-renders when viewing artifacts. + */ +export const MarkdownView = memo(function MarkdownView({ + content, + size = "md", + className, + isStreaming = false, +}: MarkdownViewProps) { + if (!content) { + return ( +
+ No content available +
+ ) + } + + return ( +
+ +
+ ) +}) + +MarkdownView.displayName = "MarkdownView" diff --git a/src/renderer/features/speckit/components/plan-page.tsx b/src/renderer/features/speckit/components/plan-page.tsx new file mode 100644 index 000000000..b11910d30 --- /dev/null +++ b/src/renderer/features/speckit/components/plan-page.tsx @@ -0,0 +1,353 @@ +/** + * PlanPage Component + * + * Main SpecKit workflow page displayed in the right drawer. + * Shows workflow status, feature info, and artifact previews. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useCallback } from "react" +import { useSetAtom } from "jotai" +import { FileText, Plus, RefreshCw, Sparkles, ExternalLink } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import { trpc } from "@/lib/trpc" +import { MarkdownView } from "./markdown-view" +import { WorkflowModal } from "./workflow-modal" +import { speckitModalOpenAtom } from "../atoms" +import { + WORKFLOW_STEP_LABELS, + WORKFLOW_STEPS_ORDER, + type WorkflowStepName, +} from "../types" + +interface PlanPageProps { + /** Chat/workspace ID */ + chatId?: string + /** Project path (required) */ + projectPath?: string + /** Callback when close button is clicked */ + onClose?: () => void +} + +/** + * PlanPage - SpecKit workflow management page + * + * Displays: + * - Current workflow state (detected from Git branch + files) + * - Feature info with artifacts + * - Workflow stepper showing progress + * - Action buttons for workflow steps + */ +export const PlanPage = memo(function PlanPage({ + chatId, + projectPath, + onClose, +}: PlanPageProps) { + const setModalOpen = useSetAtom(speckitModalOpenAtom) + + // Open workflow modal + const handleOpenWorkflow = useCallback(() => { + setModalOpen(true) + }, [setModalOpen]) + + // Query workflow state + const { + data: workflowState, + isLoading: isWorkflowLoading, + refetch: refetchWorkflow, + } = trpc.speckit.getWorkflowState.useQuery( + { projectPath: projectPath || "" }, + { enabled: !!projectPath, refetchOnWindowFocus: false } + ) + + // Query initialization status + const { data: initStatus, isLoading: isInitLoading } = + trpc.speckit.checkInitialization.useQuery( + { projectPath: projectPath || "" }, + { enabled: !!projectPath } + ) + + // Query current artifact content based on workflow state + const { data: specArtifact } = trpc.speckit.getArtifact.useQuery( + { + projectPath: projectPath || "", + featureBranch: workflowState?.branchName || "", + artifactType: "spec", + }, + { + enabled: + !!projectPath && + !!workflowState?.branchName && + workflowState.artifactsPresent.spec, + } + ) + + const { data: planArtifact } = trpc.speckit.getArtifact.useQuery( + { + projectPath: projectPath || "", + featureBranch: workflowState?.branchName || "", + artifactType: "plan", + }, + { + enabled: + !!projectPath && + !!workflowState?.branchName && + workflowState.artifactsPresent.plan, + } + ) + + // Initialize mutation + const initMutation = trpc.speckit.initializeSpecKit.useMutation({ + onSuccess: () => { + refetchWorkflow() + }, + }) + + // No project selected + if (!projectPath) { + return ( +
+
+
+ +

+ Select a project to view SpecKit workflow +

+
+
+
+ ) + } + + // Loading state + if (isWorkflowLoading || isInitLoading) { + return ( +
+
+ +
+
+ ) + } + + // Not initialized - show init prompt + if (initStatus && !initStatus.initialized) { + return ( +
+
+
+ +

SpecKit Not Initialized

+

+ Initialize ii-spec to enable feature specification workflows. +

+ {initStatus.missingComponents.length > 0 && ( +
+

Missing:

+
    + {initStatus.missingComponents.slice(0, 3).map((c) => ( +
  • + {c} +
  • + ))} + {initStatus.missingComponents.length > 3 && ( +
  • +{initStatus.missingComponents.length - 3} more
  • + )} +
+
+ )} + +
+
+
+ ) + } + + // Main workflow view + return ( +
+ {/* Header */} +
+
+ + SpecKit +
+
+ + +
+
+ + {/* Workflow Modal */} + {projectPath && } + + {/* Content */} +
+ {/* Current Feature Info */} + {workflowState?.branchName && ( +
+
+ + Feature Branch + +
+

+ {workflowState.branchName} +

+ {workflowState.featureName && ( +

+ {workflowState.featureName} +

+ )} +
+ )} + + {/* Workflow Stepper */} +
+ + Workflow Progress + +
+ {WORKFLOW_STEPS_ORDER.map((step) => { + const isActive = workflowState?.currentStep === step + const isPassed = isStepPassed( + step, + workflowState?.currentStep || "no-feature" + ) + + return ( +
+
+ {WORKFLOW_STEP_LABELS[step]} +
+ ) + })} +
+
+ + {/* Artifact Previews */} + {specArtifact?.exists && specArtifact.content && ( +
+
+ + Specification + +
+
+ +
+
+ )} + + {planArtifact?.exists && planArtifact.content && ( +
+
+ + Implementation Plan + +
+
+ +
+
+ )} + + {/* No Feature State */} + {workflowState?.currentStep === "no-feature" && ( +
+

+ No feature branch checked out +

+

+ Checkout a feature branch (e.g., 001-my-feature) to start the + workflow +

+ +
+ )} + + {/* Continue Workflow Button */} + {workflowState?.currentStep && + workflowState.currentStep !== "no-feature" && + workflowState.currentStep !== "implement" && ( +
+ +
+ )} +
+
+ ) +}) + +/** + * Check if a step is passed (completed) relative to current step + */ +function isStepPassed( + step: WorkflowStepName, + currentStep: WorkflowStepName +): boolean { + const stepIndex = WORKFLOW_STEPS_ORDER.indexOf(step) + const currentIndex = WORKFLOW_STEPS_ORDER.indexOf(currentStep) + + // If current step is not in the ordered list, nothing is passed + if (currentIndex === -1) return false + + return stepIndex < currentIndex +} + +PlanPage.displayName = "PlanPage" diff --git a/src/renderer/features/speckit/components/skip-clarify-warning.tsx b/src/renderer/features/speckit/components/skip-clarify-warning.tsx new file mode 100644 index 000000000..3958db4b0 --- /dev/null +++ b/src/renderer/features/speckit/components/skip-clarify-warning.tsx @@ -0,0 +1,132 @@ +/** + * SkipClarifyWarningBanner Component + * + * Warning shown when user tries to skip the Clarify step + * when there are unresolved clarification questions. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo } from "react" +import { AlertCircle, X, SkipForward } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +interface SkipClarifyWarningBannerProps { + /** Number of unanswered questions */ + questionCount: number + /** Callback when user confirms skip */ + onConfirmSkip: () => void + /** Callback to dismiss and stay on clarify */ + onCancel: () => void + /** Additional CSS classes */ + className?: string +} + +/** + * SkipClarifyWarningBanner - Warning for skipping clarification + */ +export const SkipClarifyWarningBanner = memo(function SkipClarifyWarningBanner({ + questionCount, + onConfirmSkip, + onCancel, + className, +}: SkipClarifyWarningBannerProps) { + return ( +
+ +
+

+ There {questionCount === 1 ? "is" : "are"}{" "} + {questionCount}{" "} + unanswered clarification {questionCount === 1 ? "question" : "questions"}. + Skipping may result in a less accurate specification. +

+
+
+ + +
+
+ ) +}) + +SkipClarifyWarningBanner.displayName = "SkipClarifyWarningBanner" + +/** + * Simple inline skip confirmation dialog + */ +export const SkipConfirmationDialog = memo(function SkipConfirmationDialog({ + isOpen, + questionCount, + onConfirm, + onCancel, +}: { + isOpen: boolean + questionCount: number + onConfirm: () => void + onCancel: () => void +}) { + if (!isOpen) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+
+
+ +
+
+

Skip Clarification?

+

+ There {questionCount === 1 ? "is" : "are"}{" "} + {questionCount}{" "} + unanswered {questionCount === 1 ? "question" : "questions"}. + Proceeding without answering may result in a less accurate specification + and implementation plan. +

+
+ + +
+
+
+
+
+ ) +}) + +SkipConfirmationDialog.displayName = "SkipConfirmationDialog" diff --git a/src/renderer/features/speckit/components/speckit-sidebar.tsx b/src/renderer/features/speckit/components/speckit-sidebar.tsx new file mode 100644 index 000000000..23bdf0be7 --- /dev/null +++ b/src/renderer/features/speckit/components/speckit-sidebar.tsx @@ -0,0 +1,98 @@ +/** + * SpecKit Sidebar Component + * + * Right sidebar drawer for SpecKit workflow management. + * Opens when the SpecKit button is clicked in the toolbar. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useMemo } from "react" +import { useAtom, useAtomValue } from "jotai" +import { X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { IconDoubleChevronRight } from "@/components/ui/icons" +import { ResizableSidebar } from "@/components/ui/resizable-sidebar" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { atomWithStorage } from "jotai/utils" +import { PlanPage } from "./plan-page" + +// Sidebar width atom (persisted) +export const speckitSidebarWidthAtom = atomWithStorage( + "speckit:sidebarWidth", + 400, + undefined, + { getOnInit: true } +) + +interface SpecKitSidebarProps { + /** Whether the sidebar is open */ + isOpen: boolean + /** Callback when sidebar should close */ + onClose: () => void + /** Chat/workspace ID */ + chatId?: string + /** Project path */ + projectPath?: string | null +} + +/** + * SpecKitSidebar - Right drawer containing the SpecKit workflow page + */ +export const SpecKitSidebar = memo(function SpecKitSidebar({ + isOpen, + onClose, + chatId, + projectPath, +}: SpecKitSidebarProps) { + return ( + +
+ {/* Header with close button */} +
+
+ + + + + Close SpecKit + + SpecKit +
+
+ + {/* Plan Page Content */} +
+ +
+
+
+ ) +}) + +SpecKitSidebar.displayName = "SpecKitSidebar" diff --git a/src/renderer/features/speckit/components/stale-warning-banner.tsx b/src/renderer/features/speckit/components/stale-warning-banner.tsx new file mode 100644 index 000000000..3a196ec27 --- /dev/null +++ b/src/renderer/features/speckit/components/stale-warning-banner.tsx @@ -0,0 +1,105 @@ +/** + * StaleWarningBanner Component + * + * Non-blocking warning shown when navigating to a previous step + * that has downstream artifacts (e.g., spec has plan.md downstream). + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo } from "react" +import { AlertTriangle, X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import type { WorkflowStepName } from "../types" + +interface StaleWarningBannerProps { + /** Current step being navigated to */ + step: WorkflowStepName + /** Downstream artifacts that exist */ + downstreamArtifacts: string[] + /** Callback to dismiss the warning */ + onDismiss: () => void + /** Additional CSS classes */ + className?: string +} + +/** + * Map of steps to their downstream artifacts + */ +const DOWNSTREAM_ARTIFACTS: Record = { + "no-feature": [], + constitution: ["spec.md", "plan.md", "tasks.md"], + specify: ["plan.md", "tasks.md"], + clarify: ["plan.md", "tasks.md"], + plan: ["tasks.md"], + tasks: [], + analyze: [], + implement: [], +} + +/** + * Check if navigating to a step would make downstream artifacts stale + */ +export function checkStaleArtifacts( + targetStep: WorkflowStepName, + existingArtifacts: { spec: boolean; plan: boolean; tasks: boolean } +): string[] { + const downstream = DOWNSTREAM_ARTIFACTS[targetStep] + const stale: string[] = [] + + if (downstream.includes("spec.md") && existingArtifacts.spec) { + stale.push("spec.md") + } + if (downstream.includes("plan.md") && existingArtifacts.plan) { + stale.push("plan.md") + } + if (downstream.includes("tasks.md") && existingArtifacts.tasks) { + stale.push("tasks.md") + } + + return stale +} + +/** + * StaleWarningBanner - Warning for stale downstream artifacts + */ +export const StaleWarningBanner = memo(function StaleWarningBanner({ + step, + downstreamArtifacts, + onDismiss, + className, +}: StaleWarningBannerProps) { + if (downstreamArtifacts.length === 0) { + return null + } + + return ( +
+ +

+ Modifying this step may make the following artifacts outdated:{" "} + + {downstreamArtifacts.join(", ")} + +

+ +
+ ) +}) + +StaleWarningBanner.displayName = "StaleWarningBanner" diff --git a/src/renderer/features/speckit/components/workflow-modal.tsx b/src/renderer/features/speckit/components/workflow-modal.tsx new file mode 100644 index 000000000..04a49a7cf --- /dev/null +++ b/src/renderer/features/speckit/components/workflow-modal.tsx @@ -0,0 +1,454 @@ +/** + * WorkflowModal Component + * + * Full-screen modal for the SpecKit workflow. + * Dual-pane interface with chat pane (left) and document pane (right). + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useState, useCallback, useEffect, useMemo } from "react" +import { useAtom, useAtomValue, useSetAtom } from "jotai" +import { X } from "lucide-react" +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import { trpc } from "@/lib/trpc" + +import { + speckitModalOpenAtom, + speckitCurrentDocumentAtom, + speckitExecutionIdAtom, + speckitActiveStepAtom, +} from "../atoms" +import { useWorkflowState, useExecuteCommand, useCommandOutput } from "../hooks" +import { WORKFLOW_STEPS_ORDER, type WorkflowStepName, type ArtifactType } from "../types" + +import { WorkflowStepper } from "./workflow-stepper" +import { ChatPane } from "./chat-pane" +import { DocumentPane } from "./document-pane" +import { StaleWarningBanner, checkStaleArtifacts } from "./stale-warning-banner" +import { SkipClarifyWarningBanner } from "./skip-clarify-warning" + +import { + ConstitutionStep, + SpecifyStep, + ClarifyStep, + PlanStep, + TasksStep, + ImplementStep, +} from "./workflow-steps" + +interface WorkflowModalProps { + /** Project path */ + projectPath: string +} + +/** + * Map workflow steps to artifact types for document pane + */ +const STEP_TO_ARTIFACT: Record = { + "no-feature": "spec", + constitution: "constitution", + specify: "spec", + clarify: "spec", + plan: "plan", + tasks: "tasks", + analyze: "tasks", + implement: "tasks", +} + +/** + * WorkflowModal - Main workflow interface + */ +export const WorkflowModal = memo(function WorkflowModal({ + projectPath, +}: WorkflowModalProps) { + const [isOpen, setIsOpen] = useAtom(speckitModalOpenAtom) + const [activeStep, setActiveStep] = useAtom(speckitActiveStepAtom) + const executionId = useAtomValue(speckitExecutionIdAtom) + const setCurrentDocument = useSetAtom(speckitCurrentDocumentAtom) + + // Local state + const [selectedArtifact, setSelectedArtifact] = useState("spec") + const [staleWarningDismissed, setStaleWarningDismissed] = useState(false) + const [showSkipWarning, setShowSkipWarning] = useState(false) + const [showOutput, setShowOutput] = useState(false) + + // Fetch workflow state + const { + workflowState, + isLoading: workflowLoading, + refetch: refetchWorkflow, + currentStep, + branchName, + artifactsPresent, + needsClarification, + clarificationQuestions, + } = useWorkflowState({ projectPath, enabled: isOpen }) + + // Command execution + const { + execute, + cancel, + reset: resetExecution, + isExecuting, + lastError, + executionId: currentExecutionId, + } = useExecuteCommand({ + projectPath, + onStart: () => setShowOutput(true), + onSuccess: () => { + // Refetch workflow state after command completes + setTimeout(() => refetchWorkflow(), 500) + }, + }) + + // Command output streaming + const { + outputLines, + isComplete: outputComplete, + hasError: outputHasError, + clearOutput, + } = useCommandOutput({ + executionId: currentExecutionId, + onComplete: () => { + refetchWorkflow() + }, + }) + + // Query artifacts + const { data: constitution } = trpc.speckit.getConstitution.useQuery( + { projectPath }, + { enabled: isOpen } + ) + + const { data: specArtifact } = trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch: branchName || "", artifactType: "spec" }, + { enabled: isOpen && !!branchName } + ) + + const { data: planArtifact } = trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch: branchName || "", artifactType: "plan" }, + { enabled: isOpen && !!branchName } + ) + + const { data: tasksArtifact } = trpc.speckit.getArtifact.useQuery( + { projectPath, featureBranch: branchName || "", artifactType: "tasks" }, + { enabled: isOpen && !!branchName } + ) + + // Open file in editor mutation + const openInEditorMutation = trpc.speckit.openFileInEditor.useMutation() + + // Determine effective step (active override or detected) + const effectiveStep = (activeStep as WorkflowStepName) || currentStep || "specify" + + // Check for stale artifacts when navigating + const staleArtifacts = useMemo(() => { + if (!artifactsPresent || staleWarningDismissed) return [] + return checkStaleArtifacts(effectiveStep, { + spec: artifactsPresent.spec, + plan: artifactsPresent.plan, + tasks: artifactsPresent.tasks, + }) + }, [effectiveStep, artifactsPresent, staleWarningDismissed]) + + // Update selected artifact when step changes + useEffect(() => { + const newArtifact = STEP_TO_ARTIFACT[effectiveStep] + if (newArtifact) { + setSelectedArtifact(newArtifact) + } + }, [effectiveStep]) + + // Reset state when modal closes + useEffect(() => { + if (!isOpen) { + setActiveStep(null) + setStaleWarningDismissed(false) + setShowSkipWarning(false) + setShowOutput(false) + clearOutput() + resetExecution() + } + }, [isOpen, setActiveStep, clearOutput, resetExecution]) + + // Handle step navigation + const handleStepClick = useCallback( + (step: WorkflowStepName) => { + // Check if trying to skip clarify + if ( + effectiveStep === "clarify" && + step === "plan" && + needsClarification && + (clarificationQuestions?.length || 0) > 0 + ) { + setShowSkipWarning(true) + return + } + + setActiveStep(step) + setStaleWarningDismissed(false) + setShowOutput(false) + }, + [effectiveStep, needsClarification, clarificationQuestions, setActiveStep] + ) + + // Handle step actions + const handleSpecifySubmit = useCallback( + (description: string) => { + execute("/speckit.specify", description) + }, + [execute] + ) + + const handleClarifySubmit = useCallback( + (answers: Record) => { + const answersStr = Object.entries(answers) + .map(([q, a]) => `${q}: ${a}`) + .join("\n") + execute("/speckit.clarify", answersStr) + }, + [execute] + ) + + const handleClarifySkip = useCallback(() => { + setShowSkipWarning(true) + }, []) + + const handleConfirmSkip = useCallback(() => { + setShowSkipWarning(false) + setActiveStep("plan") + }, [setActiveStep]) + + const handleGeneratePlan = useCallback(() => { + execute("/speckit.plan", "") + }, [execute]) + + const handleGenerateTasks = useCallback(() => { + execute("/speckit.tasks", "") + }, [execute]) + + const handleCreateConstitution = useCallback(() => { + execute("/speckit.constitution", "") + }, [execute]) + + const handleProceedToStep = useCallback( + (step: WorkflowStepName) => { + setActiveStep(step) + setShowOutput(false) + }, + [setActiveStep] + ) + + const handleOpenInEditor = useCallback( + (filePath: string) => { + openInEditorMutation.mutate({ filePath }) + }, + [openInEditorMutation] + ) + + // Render step content + const renderStepContent = () => { + switch (effectiveStep) { + case "constitution": + return ( + handleProceedToStep("specify")} + onOpenInEditor={() => + handleOpenInEditor(`${projectPath}/.specify/memory/constitution.md`) + } + isExecuting={isExecuting} + isCompleted={constitution?.exists ?? false} + /> + ) + + case "specify": + return ( + + ) + + case "clarify": + return ( + + ) + + case "plan": + return ( + handleProceedToStep("tasks")} + isExecuting={isExecuting} + isCompleted={artifactsPresent?.plan ?? false} + /> + ) + + case "tasks": + return ( + handleProceedToStep("implement")} + isExecuting={isExecuting} + isCompleted={artifactsPresent?.tasks ?? false} + /> + ) + + case "implement": + return ( + + ) + + default: + return ( +
+

Select a workflow step to continue

+
+ ) + } + } + + return ( + + + {/* Header */} +
+
+

SpecKit Workflow

+ {branchName && ( + + {branchName} + + )} +
+ +
+ {/* Stepper */} + + + {/* Close Button */} + +
+
+ + {/* Warning Banners */} + {staleArtifacts.length > 0 && ( + setStaleWarningDismissed(true)} + /> + )} + + {showSkipWarning && ( + setShowSkipWarning(false)} + /> + )} + + {/* Main Content */} +
+ {/* Left Pane - Step Content or Command Output */} +
+ {showOutput && outputLines.length > 0 ? ( + { + clearOutput() + // Re-run based on current step + switch (effectiveStep) { + case "specify": + // Can't retry specify without description + break + case "plan": + handleGeneratePlan() + break + case "tasks": + handleGenerateTasks() + break + case "constitution": + handleCreateConstitution() + break + } + }} + title={`Running ${effectiveStep}...`} + headerContent={ + + } + /> + ) : ( + renderStepContent() + )} +
+ + {/* Right Pane - Document Preview */} +
+ +
+
+
+
+ ) +}) + +WorkflowModal.displayName = "WorkflowModal" diff --git a/src/renderer/features/speckit/components/workflow-stepper.tsx b/src/renderer/features/speckit/components/workflow-stepper.tsx new file mode 100644 index 000000000..4cd36f72e --- /dev/null +++ b/src/renderer/features/speckit/components/workflow-stepper.tsx @@ -0,0 +1,264 @@ +/** + * WorkflowStepper Component + * + * Displays workflow progress with clickable steps for navigation. + * Shows: Constitution | Specify | Clarify | Plan | Tasks | Implement + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useCallback } from "react" +import { Check, Circle, ChevronRight } from "lucide-react" +import { cn } from "@/lib/utils" +import { + WORKFLOW_STEP_LABELS, + WORKFLOW_STEPS_ORDER, + type WorkflowStepName, +} from "../types" + +interface WorkflowStepperProps { + /** Current workflow step (detected from files) */ + currentStep: WorkflowStepName + /** Active step being displayed (may differ from currentStep during navigation) */ + activeStep?: WorkflowStepName + /** Callback when a step is clicked */ + onStepClick?: (step: WorkflowStepName) => void + /** Whether navigation is enabled */ + enableNavigation?: boolean + /** Compact mode for smaller displays */ + compact?: boolean +} + +/** + * Determine if a step is completed based on current step + */ +function isStepCompleted(step: WorkflowStepName, currentStep: WorkflowStepName): boolean { + const stepIndex = WORKFLOW_STEPS_ORDER.indexOf(step) + const currentIndex = WORKFLOW_STEPS_ORDER.indexOf(currentStep) + + if (stepIndex === -1 || currentIndex === -1) return false + return stepIndex < currentIndex +} + +/** + * Determine if a step is clickable (completed steps can be revisited) + */ +function isStepClickable( + step: WorkflowStepName, + currentStep: WorkflowStepName, + enableNavigation: boolean +): boolean { + if (!enableNavigation) return false + return isStepCompleted(step, currentStep) || step === currentStep +} + +/** + * WorkflowStepper - Progress indicator with navigation + */ +export const WorkflowStepper = memo(function WorkflowStepper({ + currentStep, + activeStep, + onStepClick, + enableNavigation = true, + compact = false, +}: WorkflowStepperProps) { + const handleStepClick = useCallback( + (step: WorkflowStepName) => { + if (isStepClickable(step, currentStep, enableNavigation)) { + onStepClick?.(step) + } + }, + [currentStep, enableNavigation, onStepClick] + ) + + // Filter out 'analyze' step as it's not shown in the UI + const visibleSteps = WORKFLOW_STEPS_ORDER.filter((step) => step !== "analyze") + + return ( +
+ {visibleSteps.map((step, index) => { + const isCompleted = isStepCompleted(step, currentStep) + const isCurrent = step === currentStep + const isActive = step === (activeStep || currentStep) + const isClickable = isStepClickable(step, currentStep, enableNavigation) + const isLast = index === visibleSteps.length - 1 + + return ( +
+ {/* Step Button */} + + + {/* Connector */} + {!isLast && ( + + )} +
+ ) + })} +
+ ) +}) + +WorkflowStepper.displayName = "WorkflowStepper" + +/** + * Vertical variant of the stepper for sidebar display + */ +export const WorkflowStepperVertical = memo(function WorkflowStepperVertical({ + currentStep, + activeStep, + onStepClick, + enableNavigation = true, +}: Omit) { + const handleStepClick = useCallback( + (step: WorkflowStepName) => { + if (isStepClickable(step, currentStep, enableNavigation)) { + onStepClick?.(step) + } + }, + [currentStep, enableNavigation, onStepClick] + ) + + // Filter out 'analyze' step + const visibleSteps = WORKFLOW_STEPS_ORDER.filter((step) => step !== "analyze") + + return ( +
+ {visibleSteps.map((step, index) => { + const isCompleted = isStepCompleted(step, currentStep) + const isCurrent = step === currentStep + const isActive = step === (activeStep || currentStep) + const isClickable = isStepClickable(step, currentStep, enableNavigation) + const isLast = index === visibleSteps.length - 1 + + return ( +
+ {/* Vertical line connector */} +
+ {/* Step Indicator */} +
+ {isCompleted ? ( + + ) : ( + {index + 1} + )} +
+ + {/* Connector Line */} + {!isLast && ( +
+ )} +
+ + {/* Step Content */} + +
+ ) + })} +
+ ) +}) + +WorkflowStepperVertical.displayName = "WorkflowStepperVertical" diff --git a/src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx b/src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx new file mode 100644 index 000000000..c7d201591 --- /dev/null +++ b/src/renderer/features/speckit/components/workflow-steps/clarify-step.tsx @@ -0,0 +1,221 @@ +/** + * ClarifyStep Component + * + * Second workflow step for answering clarification questions. + * Displays questions parsed from spec.md [NEEDS CLARIFICATION] markers. + * + * @see specs/001-speckit-ui-integration/plan.md + */ + +import { memo, useState, useCallback, useMemo } from "react" +import { HelpCircle, Send, Loader2, CheckCircle2, SkipForward } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" +import type { ClarificationQuestion } from "../../types" + +interface ClarifyStepProps { + /** Clarification questions parsed from spec.md */ + questions: ClarificationQuestion[] + /** Callback when user submits answers */ + onSubmit: (answers: Record) => void + /** Callback to skip clarification */ + onSkip?: () => void + /** Whether the command is executing */ + isExecuting: boolean + /** Whether clarification is already complete */ + isCompleted: boolean +} + +/** + * ClarifyStep - Clarification question answering + */ +export const ClarifyStep = memo(function ClarifyStep({ + questions, + onSubmit, + onSkip, + isExecuting, + isCompleted, +}: ClarifyStepProps) { + // Initialize answers state + const [answers, setAnswers] = useState>(() => + questions.reduce((acc, q, index) => { + acc[`Q${index + 1}`] = "" + return acc + }, {} as Record) + ) + + // Track which questions are answered + const answeredCount = useMemo( + () => Object.values(answers).filter((a) => a.trim().length > 0).length, + [answers] + ) + + const allAnswered = answeredCount === questions.length + + const handleAnswerChange = useCallback((questionId: string, value: string) => { + setAnswers((prev) => ({ ...prev, [questionId]: value })) + }, []) + + const handleSubmit = useCallback(() => { + // Filter out empty answers + const filledAnswers = Object.fromEntries( + Object.entries(answers).filter(([_, v]) => v.trim().length > 0) + ) + + if (Object.keys(filledAnswers).length === 0) { + return + } + + onSubmit(filledAnswers) + }, [answers, onSubmit]) + + // No questions state + if (questions.length === 0) { + return ( +
+ +

No Clarifications Needed

+

+ The specification is complete and doesn't require any clarifications. + You can proceed to the next step. +

+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +

Clarify Requirements

+
+

+ Answer the following questions to clarify the specification. + The answers will be incorporated into the spec. +

+
+ + {/* Questions */} +
+ {questions.map((question, index) => { + const questionId = `Q${index + 1}` + const answer = answers[questionId] || "" + const isAnswered = answer.trim().length > 0 + + return ( +
+ {/* Question Header */} +
+ + {isAnswered ? : index + 1} + +
+ {question.topic && ( + + {question.topic} + + )} +

{question.question}

+
+
+ + {/* Options (if any) */} + {question.options && question.options.length > 0 && ( +
+ {question.options.map((option, optIndex) => ( + + ))} +
+ )} + + {/* Answer Input */} +
+ +