diff --git a/AGENTS.md b/AGENTS.md index 13c5b8e..0423dfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,43 +1,49 @@ -# AGENTS.md - Development Guidelines for VSCode Extension +# AGENTS.md - Development Guidelines for SealCode VSCode Extension + +## Overview +SealCode is a VSCode extension for smart code review management. This document provides guidelines for agents working on this codebase. ## Package Management -- Package manager: **pnpm** -- Use `nr` alias (preferred) or `pnpm` for running scripts -- No Prettier - ESLint handles all formatting +- **Package manager**: pnpm +- **Preferred alias**: Use `nr` instead of `pnpm` for running scripts +- **No Prettier**: ESLint handles all formatting automatically ## Essential Commands ### Development -- `nr build` - Build the extension with tsdown -- `nr dev` - Watch mode with sourcemaps +- `nr build` - Build the extension with tsdown (outputs to `./dist`) +- `nr dev` - Watch mode with sourcemaps for development - `nr update` - Regenerate VSCode metadata from package.json ### Quality Assurance - `nr lint` - Run ESLint (auto-fixes on save in VSCode) - `nr typecheck` - Run TypeScript compiler with noEmit - `nr test` - Run all tests with vitest -- `nr test run ` - Run a specific test file (e.g., `nr test run test/storage.test.ts`) +- `nr test run ` - Run a specific test file (e.g., `nr test run test/unit/aiTools.test.ts`) - `nr test watch` - Run tests in watch mode -**CRITICAL**: After any JS/TS changes, always run: `nr lint` && `nr typecheck` - -## Code Style Guidelines +**Critical**: After any JS/TS changes, always run: `nr lint && nr typecheck` -### TypeScript Configuration -- Strict mode enabled +## TypeScript Configuration +- Strict mode enabled with `strictNullChecks: true` - Target: ES2017 - Module: ESNext -- No lib files checked (skipLibCheck: true) +- Module resolution: node with JSON module support +- `skipLibCheck: true` for faster builds + +## Code Style Guidelines ### Imports - Use Node protocol imports: `import { x } from 'node:module'` - Use reactive-vscode for VSCode extension APIs: `import { defineExtension } from 'reactive-vscode'` - Import vscode types: `import { window, workspace } from 'vscode'` +- Import test utilities: `import { describe, expect, it } from 'vitest'` ### Naming Conventions -- Variables and functions: **camelCase** (`getCommentsFilePath`, `ensureCodeReviewDir`) -- Types and interfaces: **PascalCase** (`Comment`, `CommentStorage`) -- Constants: **SCREAMING_SNAKE_CASE** (`CODEREVIEW_DIR`, `COMMENTS_FILE`) +- **Variables and functions**: camelCase (`getCommentsFilePath`, `ensureCodeReviewDir`) +- **Types and interfaces**: PascalCase (`Comment`, `CommentStorage`) +- **Constants**: SCREAMING_SNAKE_CASE (`CODEREVIEW_DIR`, `COMMENTS_FILE`) +- **Enum members**: UPPER_SNAKE_CASE or PascalCase based on context ### Code Patterns - Use **guard clauses** (early returns) instead of deep nesting @@ -45,35 +51,73 @@ - Export types explicitly: `export type`, `export interface` - Prefer `async/await` over promises - Use try/catch blocks for error handling (catch without variable is acceptable) +- Use `crypto.randomUUID()` for generating unique IDs + +### Error Handling +- Use guard clauses for null/undefined checks early in functions +- Catch errors at appropriate boundaries without swallowing them +- Show user-facing errors with `window.showErrorMessage()` +- Log errors appropriately for debugging ### VSCode Extension Patterns - Use `Uri.joinPath()` for path manipulation -- Use `workspace.fs` for file operations (createDirectory, readFile, writeFile) +- Use `workspace.fs` for file operations (`createDirectory`, `readFile`, `writeFile`) - Use `window.showInformationMessage()` and `window.showErrorMessage()` for user feedback - Use `crypto.randomUUID()` for generating IDs - Use `new TextEncoder().encode()` and `new TextDecoder().decode()` for file content - -### Testing -- Framework: **vitest** -- Import: `import { describe, expect, it } from 'vitest'` -- Structure: `describe('feature', () => { it('should do X', () => { ... }) })` -- Coverage: Comprehensive test suite covering storage, AI integration, filtering, and data processing -- Test Files: - - `test/unit/aiTools.test.ts` - AI tool validation and default models - - `test/unit/aiConfig.test.ts` - AI configuration and validation logic - - `test/unit/aiReview.test.ts` - Comment formatting for AI export - - `test/unit/commandBuilder.test.ts` - Command building and interpolation - - `test/unit/filtering.test.ts` - Comment filtering and sorting logic - - `test/helpers/` - Test utilities and mock helpers - - `test/fixtures/` - Test data factories +- Subscribe to VSCode events in the extension activation function +- Return a dispose function from activation for cleanup ### Formatting - No semicolons (style preference from @antfu/eslint-config) - ESLint handles auto-fix on save -- Run `nr lint` to fix all issues +- Run `nr lint` to fix all issues automatically +- Use consistent line breaks and whitespace + +## File Structure +- `src/` - Main source code +- `src/index.ts` - Extension entry point with activation/deactivation +- `src/commands.ts` - Command registration and handlers +- `src/treeView.ts` - Tree view provider implementation +- `src/decorations.ts` - Inline decoration rendering +- `src/storage.ts` - File storage operations +- `src/CommentStorage.ts` - Comment data management +- `src/types.ts` - TypeScript type definitions +- `src/config.ts` - Configuration management +- `src/aiTools.ts` - AI tool integrations +- `src/aiConfig.ts` - AI configuration handling +- `src/aiReview.ts` - AI review functionality +- `src/commandBuilder.ts` - Command building utilities +- `src/terminalProfile.ts` - Terminal profile handling +- `test/` - Test files following source structure +- `test/unit/` - Unit tests matching source file names + +## Testing Guidelines +- **Framework**: vitest +- **Structure**: `describe('feature', () => { it('should do X', () => { ... }) })` +- **Test files**: Mirror source file structure in `test/unit/` +- **Helpers**: Place utilities in `test/helpers/` +- **Fixtures**: Place test data factories in `test/fixtures/` +- **Test patterns**: + - Test constants and utilities directly + - Test validation and transformation functions + - Use descriptive test names that explain expected behavior + - Group related tests with nested `describe` blocks + +### Key Test Files +- `test/unit/aiTools.test.ts` - AI tool validation and default models +- `test/unit/aiConfig.test.ts` - AI configuration and validation logic +- `test/unit/aiReview.test.ts` - Comment formatting for AI export +- `test/unit/commandBuilder.test.ts` - Command building and interpolation +- `test/unit/filtering.test.ts` - Comment filtering and sorting logic +- `test/unit/storage.test.ts` - Storage path and directory operations +- `test/unit/CommentStorage.test.ts` - Comment CRUD operations +- `test/unit/terminalProfile.test.ts` - Terminal profile handling -### Additional Rules +## Additional Rules - Keep code modular and self-documenting - Use meaningful variable names that explain intent - Avoid over-engineering - keep solutions simple - No comments unless necessary for complex logic +- Maintain consistent patterns across similar files +- Follow existing code style when making changes diff --git a/README.md b/README.md index 1fdc26f..630424c 100644 --- a/README.md +++ b/README.md @@ -128,20 +128,21 @@ Choose from built-in templates or create your own: -| Key | Description | Type | Default | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | -| `seal-code.showInlineDecorations` | Show inline text decorations (after-line text preview) | `boolean` | `true` | -| `seal-code.showGutterIcons` | Show gutter icons for comments | `boolean` | `true` | -| `seal-code.showLineBackground` | Show colored background on commented lines | `boolean` | `true` | -| `seal-code.categoryColors` | Custom colors for each comment category | `object` | `{"bug":"#f44336","question":"#2196f3","suggestion":"#4caf50","nitpick":"#ff9800","note":"#9e9e9e"}` | -| `seal-code.aiTool` | AI CLI tool to use for sending review comments | `string` | `"opencode"` | -| `seal-code.aiToolCommand` | Custom command path for AI tool (used when aiTool is 'custom', e.g., 'ccs glm') | `string` | `""` | -| `seal-code.aiToolClaudeModel` | Claude model to use (e.g., haiku, sonnet, opus) | `string` | `"haiku"` | -| `seal-code.aiToolOpenCodeModel` | OpenCode model to use (e.g., opencode/big-pickle, opencode/claude) | `string` | `"opencode/big-pickle"` | -| `seal-code.aiToolCopilotModel` | Copilot model to use (e.g., gpt-4.1, gpt-4o, o3) | `string` | `"gpt-4.1"` | -| `seal-code.aiToolAmpModel` | Amp mode to use (rush or smart). Execute mode requires rush or smart. | `string` | `"smart"` | -| `seal-code.promptTemplates` | Named prompt templates for AI review. Use {{comments}} for formatted comments and {{files}} for affected file list. | `object` | See package.json | -| `seal-code.showAIQuickPick` | Show quick pick menu for AI tool selection before sending to AI | `boolean` | `false` | +| Key | Description | Type | Default | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | +| `seal-code.showInlineDecorations` | Show inline text decorations (after-line text preview) | `boolean` | `true` | +| `seal-code.showGutterIcons` | Show gutter icons for comments | `boolean` | `true` | +| `seal-code.showLineBackground` | Show colored background on commented lines | `boolean` | `true` | +| `seal-code.categoryColors` | Custom colors for each comment category | `object` | `{"bug":"#f44336","question":"#2196f3","suggestion":"#4caf50","nitpick":"#ff9800","note":"#9e9e9e"}` | +| `seal-code.aiTool` | AI CLI tool to use for sending review comments | `string` | `"opencode"` | +| `seal-code.aiToolCommand` | Custom command path for AI tool (used when aiTool is 'custom', e.g., 'ccs glm') | `string` | `""` | +| `seal-code.aiToolClaudeModel` | Claude model to use (e.g., haiku, sonnet, opus) | `string` | `"haiku"` | +| `seal-code.aiToolOpenCodeModel` | OpenCode model to use (e.g., opencode/big-pickle, opencode/claude) | `string` | `"opencode/big-pickle"` | +| `seal-code.aiToolCopilotModel` | Copilot model to use (e.g., gpt-4.1, gpt-4o, o3) | `string` | `"gpt-4.1"` | +| `seal-code.aiToolAmpModel` | Amp mode to use (rush or smart). Execute mode requires rush or smart. | `string` | `"smart"` | +| `seal-code.promptTemplates` | Named prompt templates for AI review. Use {{comments}} for formatted comments and {{files}} for affected file list. | `object` | See package.json | +| `seal-code.showAIQuickPick` | Show quick pick menu for AI tool selection before sending to AI | `boolean` | `false` | +| `seal-code.terminalProfile` | Terminal profile to use for AI reviews. 'default' uses VSCode terminal, 'tmux' runs in a tmux session per workspace. | `string` | `"default"` | diff --git a/package.json b/package.json index 79131a2..89bec54 100644 --- a/package.json +++ b/package.json @@ -330,6 +330,12 @@ "type": "boolean", "default": false, "description": "Show quick pick menu for AI tool selection before sending to AI" + }, + "seal-code.terminalProfile": { + "type": "string", + "enum": ["default", "tmux"], + "default": "default", + "description": "Terminal profile to use for AI reviews. 'default' uses VSCode terminal, 'tmux' runs in a tmux session per workspace." } } } diff --git a/ralph/prd.json b/ralph/prd.json new file mode 100644 index 0000000..9e950d6 --- /dev/null +++ b/ralph/prd.json @@ -0,0 +1,96 @@ +{ + "project": "SealCode", + "branchName": "ralph/terminal-profile", + "description": "Terminal Profile for Session Management - Run AI reviews in tmux with one session per workspace", + "userStories": [ + { + "id": "US-001", + "title": "Add terminal profile configuration setting", + "description": "As a user, I want to configure my preferred terminal profile so that all AI reviews run within my chosen session manager.", + "acceptanceCriteria": [ + "Add `seal-code.terminalProfile` setting to package.json with enum: default, tmux", + "Default value is 'default' (current VSCode terminal behavior)", + "Run `nr update` to regenerate VSCode metadata", + "Typecheck passes" + ], + "priority": 1, + "passes": true, + "notes": "Implemented in package.json lines 334-340" + }, + { + "id": "US-002", + "title": "Create terminalProfile module with binary detection", + "description": "As a developer, I need a module to detect tmux availability and manage session names.", + "acceptanceCriteria": [ + "Create src/terminalProfile.ts module", + "Add TerminalProfile type: 'default' | 'tmux'", + "Implement isTmuxAvailable() using 'which tmux' command", + "Implement getSessionName(workspaceName) returning 'sealcode-{sanitized-name}'", + "Sanitize workspace name: lowercase, replace spaces/special chars with hyphens", + "Typecheck passes" + ], + "priority": 2, + "passes": true, + "notes": "Implemented in src/terminalProfile.ts" + }, + { + "id": "US-003", + "title": "Implement tmux session execution", + "description": "As a user, I want AI reviews to run in a tmux session so I can manage multiple reviews in one place.", + "acceptanceCriteria": [ + "Add buildTmuxCommand(sessionName, windowName, command) to terminalProfile.ts", + "Use format: tmux new-session -A -s {session} -n {window} -d && tmux send-keys -t {session}:{window} '{command}' Enter && tmux attach -t {session}", + "Window name derived from template name and AI tool", + "Typecheck passes" + ], + "priority": 3, + "passes": true, + "notes": "Implemented in src/terminalProfile.ts lines 29-32" + }, + { + "id": "US-004", + "title": "Handle missing tmux with fallback prompt", + "description": "As a user, I want to be prompted to use default terminal when tmux is not installed.", + "acceptanceCriteria": [ + "Add promptForFallback() showing quick pick when tmux unavailable", + "Quick pick options: 'Use Default Terminal', 'Cancel'", + "Returns 'default' or undefined if cancelled", + "Typecheck passes" + ], + "priority": 4, + "passes": true, + "notes": "Implemented in src/terminalProfile.ts lines 34-46" + }, + { + "id": "US-005", + "title": "Integrate terminal profile into executeAIReview", + "description": "As a user, I want the AI review command to use my configured terminal profile.", + "acceptanceCriteria": [ + "Read terminalProfile setting in executeAIReview function", + "If profile is 'default', use existing terminal creation logic", + "If profile is 'tmux', check availability and build wrapped command", + "If tmux unavailable, call promptForFallback", + "Execute the final command in VSCode terminal", + "Typecheck passes" + ], + "priority": 5, + "passes": true, + "notes": "Implemented in src/commands.ts lines 808-826" + }, + { + "id": "US-006", + "title": "Add unit tests for terminalProfile module", + "description": "As a developer, I need tests to verify terminal profile logic.", + "acceptanceCriteria": [ + "Create test/unit/terminalProfile.test.ts", + "Test getSessionName sanitizes workspace names correctly", + "Test buildTmuxCommand generates correct command format", + "Typecheck passes", + "Tests pass" + ], + "priority": 6, + "passes": true, + "notes": "14 tests passing in test/unit/terminalProfile.test.ts" + } + ] +} diff --git a/ralph/progress.txt b/ralph/progress.txt new file mode 100644 index 0000000..917675d --- /dev/null +++ b/ralph/progress.txt @@ -0,0 +1,81 @@ +# Ralph Progress - Terminal Profile Feature + +## Codebase Patterns +- Use `pnpm run update` (or `nr update`) after modifying package.json configuration to regenerate src/generated/meta.ts +- Config settings are typed via vscode-ext-gen in src/generated/meta.ts +- Type imports must come before value imports and be sorted alphabetically (perfectionist/sort-imports) +- For testing files that import vscode, mock it with `vi.mock('vscode', ...)` and place imports before the vi.mock call (vitest hoists mocks automatically) + +## Completed Stories + +## 2026-01-12 - US-001 +Thread: https://ampcode.com/threads/T-019bb1c7-072e-732b-8952-6b9a0f4ecc37 +- Added `seal-code.terminalProfile` setting with enum ["default", "tmux"] +- Default value set to "default" (preserves existing VSCode terminal behavior) +- Ran `nr update` to regenerate VSCode metadata in src/generated/meta.ts +- Files changed: package.json, src/generated/meta.ts +- **Learnings for future iterations:** + - The vscode-ext-gen tool auto-generates TypeScript types for all config settings + - New config appears as `configs.terminalProfile` with proper typing +--- + +## 2026-01-12 - US-002 +Thread: https://ampcode.com/threads/T-019bb1c8-bda7-756b-88d3-fc1b94c7de87 +- Created src/terminalProfile.ts module with TerminalProfile type +- Implemented isTmuxAvailable() using 'which tmux' with promisified exec +- Implemented getSessionName() with sanitization: lowercase, replace special chars with hyphens, trim hyphens +- Files changed: src/terminalProfile.ts +- **Learnings for future iterations:** + - Use `node:child_process` and `node:util` Node protocol imports + - Promisify exec for async/await usage + - Sanitization pattern: lowercase -> replace non-alphanumeric with hyphens -> collapse multiple hyphens -> trim leading/trailing hyphens +--- + +## 2026-01-12 - US-003 +Thread: https://ampcode.com/threads/T-019bb1c9-f22d-72d8-8c8b-642a6ca6a8c4 +- Added buildTmuxCommand(sessionName, windowName, command) to terminalProfile.ts +- Command format: tmux new-session -A -s {session} -n {window} -d && tmux send-keys -t {session}:{window} '{command}' Enter && tmux attach -t {session} +- Added single-quote escaping for command safety (replace ' with '\'' pattern) +- Files changed: src/terminalProfile.ts +- **Learnings for future iterations:** + - Escape single quotes in shell commands using the '\'' pattern (end quote, escaped quote, start quote) + - tmux -A flag attaches to existing session or creates new one (idempotent) +--- + +## 2026-01-12 - US-004 +Thread: https://ampcode.com/threads/T-019bb1cb-3d2b-744e-8f73-c37584c88a77 +- Added promptForFallback() function to terminalProfile.ts +- Shows quick pick with 'Use Default Terminal' and 'Cancel' options +- Returns 'default' or undefined if cancelled +- Files changed: src/terminalProfile.ts +- **Learnings for future iterations:** + - Use vscode window.showQuickPick with object items containing label and value + - Return type can include undefined for cancelled actions +--- + +## 2026-01-12 - US-005 +Thread: https://ampcode.com/threads/T-019bb1cc-8748-762a-ab32-04d5a780da61 +- Integrated terminal profile into executeAIReview function in src/commands.ts +- Reads terminalProfile setting from config +- For 'tmux' profile: checks availability, prompts for fallback if unavailable +- Builds tmux wrapped command with session and window names derived from workspace and template +- Default behavior unchanged when profile is 'default' +- Files changed: src/commands.ts +- **Learnings for future iterations:** + - Type imports must come before value imports and be sorted alphabetically (perfectionist/sort-imports) + - Use `configs.terminalProfile.key` and `configs.terminalProfile.default` for type-safe config access + - Window name sanitization: lowercase + replace non-alphanumeric with hyphens +--- + +## 2026-01-12 - US-006 +Thread: https://ampcode.com/threads/T-019bb1cf-a1a7-718d-b4c4-79370592cc4e +- Created test/unit/terminalProfile.test.ts with 14 unit tests +- Tests for getSessionName: prefix, lowercase, spaces, special chars, hyphen collapsing, trimming, empty string, special-chars-only +- Tests for buildTmuxCommand: format, single quote escaping, multiple quotes, double quotes, complex real-world command +- Mocked vscode module to allow tests to run +- Files changed: test/unit/terminalProfile.test.ts +- **Learnings for future iterations:** + - Must mock vscode module using `vi.mock('vscode', ...)` for testing files that import vscode + - Import the module under test BEFORE calling vi.mock to satisfy eslint import/first rule + - vitest hoists vi.mock calls automatically so import order is correct at runtime +--- diff --git a/src/commands.ts b/src/commands.ts index aa362dc..14deeb1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,4 +1,5 @@ import type { Terminal } from 'vscode' +import type { TerminalProfile } from './terminalProfile' import type { Comment, CommentCategory } from './types' import * as path from 'node:path' import { commands, Position, Range, Selection, Uri, window, workspace } from 'vscode' @@ -15,6 +16,7 @@ import { showCommentUpdatedMessage, } from './messages' import { getCodeReviewDir } from './storage' +import { buildTmuxCommand, getSessionName, isTmuxAvailable, promptForFallback } from './terminalProfile' import { getTreeDataProvider, refreshTreeView } from './treeView' interface CommentItemLike { @@ -802,10 +804,32 @@ async function executeAIReview(comments: Comment[], context: string = ''): Promi const command = buildAICommand(aiTool, aiToolCommand, prompt) const terminalName = generateTerminalName(templateName, aiTool) + + let terminalProfile = config.get(configs.terminalProfile.key, configs.terminalProfile.default as TerminalProfile) + + if (terminalProfile === 'tmux') { + const tmuxAvailable = await isTmuxAvailable() + if (!tmuxAvailable) { + const fallback = await promptForFallback() + if (!fallback) { + return + } + terminalProfile = fallback + } + } + + let finalCommand = command + if (terminalProfile === 'tmux') { + const workspaceName = workspace.workspaceFolders?.[0]?.name ?? 'workspace' + const sessionName = getSessionName(workspaceName) + const windowName = `${templateName ?? 'review'}-${aiTool ?? 'ai'}`.toLowerCase().replace(/[^a-z0-9-]/g, '-') + finalCommand = buildTmuxCommand(sessionName, windowName, command) + } + const terminal = window.createTerminal(terminalName) terminal.show() - await sendCommandToTerminal(terminal, command) + await sendCommandToTerminal(terminal, finalCommand) showCommentsSentMessage(comments.length, context) } diff --git a/src/terminalProfile.ts b/src/terminalProfile.ts new file mode 100644 index 0000000..7634187 --- /dev/null +++ b/src/terminalProfile.ts @@ -0,0 +1,46 @@ +import { exec } from 'node:child_process' +import { promisify } from 'node:util' +import { window } from 'vscode' + +const execAsync = promisify(exec) + +export type TerminalProfile = 'default' | 'tmux' + +export async function isTmuxAvailable(): Promise { + try { + await execAsync('which tmux') + return true + } + catch { + return false + } +} + +export function getSessionName(workspaceName: string): string { + const sanitized = workspaceName + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + + return `sealcode-${sanitized || 'workspace'}` +} + +export function buildTmuxCommand(sessionName: string, windowName: string, command: string): string { + const escapedCommand = command.replace(/'/g, `'\\''`) + return `tmux new-session -A -s ${sessionName} -n ${windowName} -d && tmux send-keys -t ${sessionName}:${windowName} '${escapedCommand}' Enter && tmux attach -t ${sessionName}` +} + +export async function promptForFallback(): Promise { + const selection = await window.showQuickPick( + [ + { label: 'Use Default Terminal', value: 'default' as const }, + { label: 'Cancel', value: undefined }, + ], + { + placeHolder: 'tmux is not available. Choose an alternative:', + }, + ) + + return selection?.value +} diff --git a/tasks/prd-terminal-profile.md b/tasks/prd-terminal-profile.md new file mode 100644 index 0000000..ae00b64 --- /dev/null +++ b/tasks/prd-terminal-profile.md @@ -0,0 +1,111 @@ +# PRD: Terminal Profile for Session Management + +## Introduction + +Add terminal profile support to run AI reviews within terminal multiplexers (tmux, screen, zellij). This allows users to group all AI review sessions within a single managed session per workspace, making it easier to monitor multiple reviews and maintain session persistence. + +## Goals + +- Support running AI reviews in tmux, screen, or zellij sessions +- Maintain one session per workspace for grouped review management +- Provide graceful handling when preferred multiplexer is unavailable +- Keep backward compatibility with default VSCode terminal behavior + +## User Stories + +### US-001: Add terminal profile configuration ✅ +**Description:** As a user, I want to configure my preferred terminal profile so that all AI reviews run within my chosen session manager. + +**Acceptance Criteria:** +- [x] Add `seal-code.terminalProfile` setting with options: `default`, `tmux` *(screen, zellij deferred)* +- [x] Default value is `default` (current behavior) +- [x] Setting is documented in package.json contributes.configuration +- [x] Typecheck passes + +### US-002: Implement tmux session management ✅ +**Description:** As a user, I want AI reviews to run in a tmux session so I can manage multiple reviews in one place. + +**Acceptance Criteria:** +- [x] Create or attach to workspace-specific tmux session (e.g., `sealcode-{workspace-name}`) +- [x] Each AI review creates a new tmux window within the session +- [x] Window is named with template name and AI tool (e.g., `review-claude`) +- [x] Command executes within the tmux window +- [x] Typecheck passes + +### US-003: Implement screen session management 🔜 (Deferred) +**Description:** As a user, I want AI reviews to run in a GNU screen session as an alternative to tmux. + +**Acceptance Criteria:** +- [ ] Create or attach to workspace-specific screen session +- [ ] Each AI review creates a new screen window +- [ ] Window is titled appropriately +- [ ] Command executes within the screen window +- [ ] Typecheck passes + +### US-004: Implement zellij session management 🔜 (Deferred) +**Description:** As a user, I want AI reviews to run in a zellij session for modern terminal multiplexing. + +**Acceptance Criteria:** +- [ ] Create or attach to workspace-specific zellij session +- [ ] Each AI review creates a new zellij pane or tab +- [ ] Pane/tab is named appropriately +- [ ] Command executes within zellij +- [ ] Typecheck passes + +### US-005: Handle missing multiplexer gracefully ✅ +**Description:** As a user, I want to be prompted to choose an alternative when my preferred multiplexer is not installed. + +**Acceptance Criteria:** +- [x] Check if configured multiplexer binary exists before use +- [x] If not found, show quick pick with available alternatives + default option +- [ ] Remember user's choice for session (optional: persist preference) *(not implemented)* +- [x] Proceed with selected alternative +- [x] Typecheck passes + +## Functional Requirements + +- FR-1: ✅ Add `seal-code.terminalProfile` enum setting: `default` | `tmux` *(screen, zellij deferred)* +- FR-2: ✅ Session naming convention: `sealcode-{workspace-basename}` (sanitized for shell) +- FR-3: ✅ For tmux: use `tmux new-session -A -s {session} -n {window-name} '{command}'` +- FR-4: 🔜 For screen: use `screen -S {session} -X screen -t {window-name}` then send command *(deferred)* +- FR-5: 🔜 For zellij: use `zellij attach {session} --create` with action commands *(deferred)* +- FR-6: ✅ Check binary availability via `which {binary}` before execution +- FR-7: ✅ Quick pick for fallback shows "Default Terminal" option + +## Non-Goals + +- No per-AI-tool terminal profile settings +- No custom session naming configuration +- No automatic installation of multiplexers +- No support for Windows-specific terminal multiplexers (ConEmu, etc.) + +## Technical Considerations + +- ✅ Create new `terminalProfile.ts` module for multiplexer logic +- ✅ Reuse existing `sendCommandToTerminal` pattern for default behavior +- ✅ Binary detection should be async and cached per session +- ✅ Workspace name sanitization needed (remove spaces, special chars) +- ✅ Consider macOS vs Linux path differences for binary detection + +## Success Metrics + +- ✅ Users can run multiple AI reviews grouped in single multiplexer session +- ✅ Fallback prompt appears within 1 second when multiplexer missing +- ✅ No regression in default terminal behavior + +## Open Questions + +- Should we add a "Don't ask again" option in the fallback prompt? *(not implemented)* +- Should session attach behavior open in integrated terminal or external? *(uses integrated terminal)* + +## Implementation Status + +| Component | Status | +|-----------|--------| +| Config setting | ✅ Complete | +| terminalProfile.ts module | ✅ Complete | +| tmux support | ✅ Complete | +| screen support | 🔜 Deferred | +| zellij support | 🔜 Deferred | +| Fallback prompt | ✅ Complete | +| Unit tests (14 tests) | ✅ Passing | diff --git a/test/unit/terminalProfile.test.ts b/test/unit/terminalProfile.test.ts new file mode 100644 index 0000000..5f6c7aa --- /dev/null +++ b/test/unit/terminalProfile.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it, vi } from 'vitest' +import { buildTmuxCommand, getSessionName } from '../../src/terminalProfile' + +vi.mock('vscode', () => ({ + window: { + showQuickPick: vi.fn(), + }, +})) + +describe('terminalProfile', () => { + describe('getSessionName', () => { + it('should prefix with sealcode-', () => { + const result = getSessionName('myproject') + expect(result).toBe('sealcode-myproject') + }) + + it('should convert to lowercase', () => { + const result = getSessionName('MyProject') + expect(result).toBe('sealcode-myproject') + }) + + it('should replace spaces with hyphens', () => { + const result = getSessionName('my project name') + expect(result).toBe('sealcode-my-project-name') + }) + + it('should replace special characters with hyphens', () => { + const result = getSessionName('my_project@v1.0') + expect(result).toBe('sealcode-my-project-v1-0') + }) + + it('should collapse multiple hyphens into one', () => { + const result = getSessionName('my---project') + expect(result).toBe('sealcode-my-project') + }) + + it('should trim leading and trailing hyphens', () => { + const result = getSessionName('-my-project-') + expect(result).toBe('sealcode-my-project') + }) + + it('should handle complex workspace names', () => { + const result = getSessionName(' My Complex__Project (v2.0) ') + expect(result).toBe('sealcode-my-complex-project-v2-0') + }) + + it('should return sealcode-workspace for empty string', () => { + const result = getSessionName('') + expect(result).toBe('sealcode-workspace') + }) + + it('should return sealcode-workspace for string with only special chars', () => { + const result = getSessionName('___') + expect(result).toBe('sealcode-workspace') + }) + }) + + describe('buildTmuxCommand', () => { + it('should build correct tmux command format', () => { + const result = buildTmuxCommand('mysession', 'mywindow', 'echo hello') + expect(result).toBe( + `tmux new-session -A -s mysession -n mywindow -d && tmux send-keys -t mysession:mywindow 'echo hello' Enter && tmux attach -t mysession`, + ) + }) + + it('should escape single quotes in command', () => { + const result = buildTmuxCommand('session', 'window', `echo 'hello world'`) + expect(result).toBe( + `tmux new-session -A -s session -n window -d && tmux send-keys -t session:window 'echo '\\''hello world'\\''' Enter && tmux attach -t session`, + ) + }) + + it('should handle commands with multiple single quotes', () => { + const result = buildTmuxCommand('session', 'window', `echo 'a' && echo 'b'`) + expect(result).toContain(`'\\''a'\\''`) + expect(result).toContain(`'\\''b'\\''`) + }) + + it('should preserve double quotes in command', () => { + const result = buildTmuxCommand('session', 'window', 'echo "hello"') + expect(result).toContain(`'echo "hello"'`) + }) + + it('should handle complex real-world command', () => { + const result = buildTmuxCommand( + 'sealcode-vscode-seal-code', + 'claude-review', + `claude --model sonnet "Review this code for bugs"`, + ) + expect(result).toBe( + `tmux new-session -A -s sealcode-vscode-seal-code -n claude-review -d && tmux send-keys -t sealcode-vscode-seal-code:claude-review 'claude --model sonnet "Review this code for bugs"' Enter && tmux attach -t sealcode-vscode-seal-code`, + ) + }) + }) +})