chore: remove obsolete Cursor CLI integration documentation

- Deleted the Cursor CLI integration analysis document, phase prompt, README, and related phase files as they are no longer relevant to the current project structure.
- This cleanup helps streamline the project and remove outdated references, ensuring a more maintainable codebase.
This commit is contained in:
Kacper
2026-01-02 21:02:40 +01:00
parent 3c8ee5b714
commit 9071f89ec8
15 changed files with 0 additions and 6812 deletions

View File

@@ -1,89 +0,0 @@
# Global Prompt for Cursor CLI Integration Phases
Copy the prompt below when starting a new Claude session for any phase.
---
## Prompt Template
```
I'm implementing the Cursor CLI integration for AutoMaker.
## Context
- Plan location: `P:\automaker\plan\cursor-cli-integration\`
- Read the README.md first for architecture overview and design decisions
- Then read the specific phase file I mention below
## Phase to Implement
[REPLACE THIS LINE WITH: Phase X - phases/phase-X-*.md]
## Critical Requirements
### 1. Use @automaker/* Packages (see docs\llm-shared-packages.md)
**From @automaker/types:**
- Reuse `InstallationStatus` (don't create new status types)
- Use `ModelProvider` type ('claude' | 'cursor')
- Use `CursorModelId`, `CURSOR_MODEL_MAP` for Cursor models
**From @automaker/utils:**
import { createLogger, isAbortError, mkdirSafe, existsSafe } from '@automaker/utils';
**From @automaker/platform:**
import { spawnJSONLProcess, getAutomakerDir } from '@automaker/platform';
### 2. UI Components (apps/ui)
All UI must use components from `@/components/ui/*`:
- Card, CardHeader, CardTitle, CardContent, CardFooter
- Button, Badge, Label, Input, Textarea
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue
- Checkbox, Alert, AlertDescription
- Tabs, TabsList, TabsTrigger, TabsContent
Icons from `lucide-react`: Terminal (Cursor), Bot (Claude), CheckCircle2, XCircle, Loader2, RefreshCw, ExternalLink
### 3. API Requests (apps/ui)
Use HttpApiClient, NOT raw fetch():
import { api } from '@/lib/http-api-client';
const result = await api.setup.getCursorStatus();
### 4. Do NOT Extend @automaker/model-resolver
Cursor models use `CURSOR_MODEL_MAP` in @automaker/types instead.
## Instructions
1. Read the phase file completely
2. Implement each task in order
3. Run the verification steps before marking complete
4. Update the phase status in the markdown file when done
```
---
## Quick Reference: Phase Order
| Phase | File | Description |
| ----- | -------------------------------- | ------------------------------- |
| 0 | `phases/phase-0-analysis.md` | Analysis & Documentation |
| 1 | `phases/phase-1-types.md` | Core Types & Configuration |
| 2 | `phases/phase-2-provider.md` | Cursor Provider Implementation |
| 3 | `phases/phase-3-factory.md` | Provider Factory Integration |
| 4 | `phases/phase-4-routes.md` | Setup Routes & Status Endpoints |
| 5 | `phases/phase-5-log-parser.md` | Log Parser Integration |
| 6 | `phases/phase-6-setup-wizard.md` | UI Setup Wizard |
| 7 | `phases/phase-7-settings.md` | Settings View Provider Tabs |
| 8 | `phases/phase-8-profiles.md` | AI Profiles Integration |
| 9 | `phases/phase-9-execution.md` | Task Execution Integration |
| 10 | `phases/phase-10-testing.md` | Testing & Validation |
## Dependencies
```
Phase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 6
↘ ↘ Phase 7
Phase 5 → Phase 8 → Phase 9 → Phase 10
```
Phases 4-7 can run in parallel after Phase 3.
Phase 8 depends on Phase 1 and Phase 7.
Phase 9 depends on Phase 8.
Phase 10 is final integration testing.

View File

@@ -1,396 +0,0 @@
# Cursor CLI Integration Plan
> Integration of Cursor Agent CLI (`cursor-agent`) as an alternative AI provider in AutoMaker
## Status Overview
| Phase | Name | Status | Test Status |
| ----- | ------------------------------------------------------------ | ----------- | ----------- |
| 0 | [Analysis & Documentation](phases/phase-0-analysis.md) | `completed` | ✅ |
| 1 | [Core Types & Configuration](phases/phase-1-types.md) | `completed` | ✅ |
| 2 | [Cursor Provider Implementation](phases/phase-2-provider.md) | `completed` | ✅ |
| 3 | [Provider Factory Integration](phases/phase-3-factory.md) | `completed` | ✅ |
| 4 | [Setup Routes & Status Endpoints](phases/phase-4-routes.md) | `completed` | ✅ |
| 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `completed` | ✅ |
| 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ |
| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ |
| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `completed` | ✅ |
| 9 | [Task Execution Integration](phases/phase-9-execution.md) | `completed` | ✅ |
| 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - |
**Status Legend:** `pending` | `in_progress` | `completed` | `blocked`
---
## Quick Links
- **Reference PR**: [#279](https://github.com/AutoMaker-Org/automaker/pull/279) (incomplete, patterns only)
- **Cursor CLI Docs**: [cursor.com/docs/cli](https://cursor.com/docs/cli)
- **Output Format Spec**: [Output Format Reference](https://cursor.com/docs/cli/reference/output-format)
---
## Architecture Summary
### Existing Provider Pattern
AutoMaker uses an extensible provider architecture:
```
BaseProvider (abstract)
├── getName(): string
├── executeQuery(options): AsyncGenerator<ProviderMessage>
├── detectInstallation(): Promise<InstallationStatus>
└── getAvailableModels(): ModelDefinition[]
ClaudeProvider extends BaseProvider
└── Uses @anthropic-ai/claude-agent-sdk
ProviderFactory
└── getProviderForModel(modelId) → routes to correct provider
```
### Target Architecture
```
BaseProvider
├── ClaudeProvider (existing)
└── CursorProvider (new)
├── Spawns cursor-agent CLI process
├── Uses --output-format stream-json
└── Normalizes events to ProviderMessage format
```
---
## Key Requirements
1. **Model Selection**: Explicit model selection via config (not just "auto" mode)
2. **Authentication**: Browser login (`cursor-agent login`) as primary method
3. **Setup Wizard**: Optional CLI status check (skippable, configure later)
4. **AI Profiles**: Separate Cursor profiles with embedded thinking mode (e.g., `claude-sonnet-4-thinking`)
5. **Settings View**: Separate tabs/sections per provider
6. **Streaming**: Full `stream-json` parsing with tool call events for log-viewer
7. **Error Handling**: Detailed error mapping with recovery suggestions
---
## Cursor CLI Reference
### Installation
```bash
curl https://cursor.com/install -fsS | bash
```
### Authentication Methods
1. **Browser Login** (Recommended): `cursor-agent login`
2. **API Key**: `CURSOR_API_KEY` environment variable
### CLI Flags for Integration
```bash
cursor-agent \
-p "prompt" # Print/non-interactive mode
--model gpt-4o # Explicit model selection
--output-format stream-json # NDJSON streaming
--stream-partial-output # Real-time character streaming
--force # Allow file modifications
```
### Available Models (from Cursor docs)
| Model ID | Description | Thinking |
| -------------------------- | -------------------------- | -------- |
| `auto` | Auto-select best model | - |
| `claude-sonnet-4` | Claude Sonnet 4 | No |
| `claude-sonnet-4-thinking` | Claude Sonnet 4 + Thinking | Yes |
| `gpt-4o` | GPT-4o | No |
| `gpt-4o-mini` | GPT-4o Mini | No |
| `gemini-2.5-pro` | Gemini 2.5 Pro | No |
| `o3-mini` | O3 Mini (reasoning) | Built-in |
---
## Stream JSON Event Types
### System Init
```json
{
"type": "system",
"subtype": "init",
"apiKeySource": "login",
"cwd": "/path",
"session_id": "uuid",
"model": "Claude 4 Sonnet",
"permissionMode": "default"
}
```
### User Message
```json
{
"type": "user",
"message": { "role": "user", "content": [{ "type": "text", "text": "prompt" }] },
"session_id": "uuid"
}
```
### Assistant Message
```json
{
"type": "assistant",
"message": { "role": "assistant", "content": [{ "type": "text", "text": "response" }] },
"session_id": "uuid"
}
```
### Tool Call Started
```json
{
"type": "tool_call",
"subtype": "started",
"call_id": "id",
"tool_call": { "readToolCall": { "args": { "path": "file.txt" } } },
"session_id": "uuid"
}
```
### Tool Call Completed
```json
{
"type": "tool_call",
"subtype": "completed",
"call_id": "id",
"tool_call": {
"readToolCall": {
"args": { "path": "file.txt" },
"result": { "success": { "content": "...", "totalLines": 54 } }
}
},
"session_id": "uuid"
}
```
### Result (Final)
```json
{
"type": "result",
"subtype": "success",
"duration_ms": 1234,
"is_error": false,
"result": "full text",
"session_id": "uuid"
}
```
---
## File Map
### Files to Create
| File | Phase | Description |
| ------------------------------------------------------------------------------ | ----- | ---------------------------- |
| `libs/types/src/cursor-models.ts` | 1 | Cursor model definitions |
| `apps/server/src/providers/cursor-provider.ts` | 2 | Main provider implementation |
| `apps/server/src/providers/cursor-config-manager.ts` | 2 | Config file management |
| `apps/server/src/routes/setup/routes/cursor-status.ts` | 4 | CLI status endpoint |
| `apps/server/src/routes/setup/routes/cursor-config.ts` | 4 | Config management endpoints |
| `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx` | 6 | Setup wizard step |
| `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx` | 7 | Settings tab |
| `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx` | 7 | Tab container |
| `apps/server/tests/unit/providers/cursor-provider.test.ts` | 10 | Unit tests |
### Files to Modify
| File | Phase | Changes |
| ------------------------------------------------------------------------ | ----- | ------------------------------ |
| `libs/types/src/index.ts` | 1 | Export Cursor types |
| `libs/types/src/settings.ts` | 1 | Extend `ModelProvider` type |
| `apps/server/src/providers/provider-factory.ts` | 3 | Add Cursor routing |
| `apps/server/src/routes/setup/index.ts` | 4 | Register Cursor routes |
| `apps/ui/src/lib/log-parser.ts` | 5 | Add Cursor event normalization |
| `apps/ui/src/components/views/setup-view.tsx` | 6 | Add Cursor setup step |
| `apps/ui/src/components/views/profiles-view/components/profile-form.tsx` | 8 | Add Cursor provider fields |
| `apps/server/src/services/agent-service.ts` | 9 | Use ProviderFactory |
---
## Dependencies
### Between Phases
```
Phase 0 ─────────────────────────────────────────────┐
│ │
Phase 1 (Types) ─────────────────────────────────────┤
│ │
Phase 2 (Provider) ──────────────────────────────────┤
│ │
Phase 3 (Factory) ───────────────────────────────────┤
│ │
├── Phase 4 (Routes) ────────────────────────────┤
│ │ │
│ ├── Phase 6 (Setup Wizard) ──────────────┤
│ │ │
│ └── Phase 7 (Settings View) ─────────────┤
│ │
├── Phase 5 (Log Parser) ────────────────────────┤
│ │
└── Phase 8 (Profiles) ──────────────────────────┤
│ │
Phase 9 (Execution) ─────────────────────┤
Phase 10 (Tests) ┘
```
### External Dependencies
- `cursor-agent` CLI must be installed for testing
- Cursor account for authentication testing
---
## Design Decisions
### 1. Use HttpApiClient for All API Requests
All UI components must use `HttpApiClient` from `@/lib/http-api-client.ts` instead of raw `fetch()`:
```typescript
// ✓ Correct - uses HttpApiClient
import { api } from '@/lib/http-api-client';
const result = await api.setup.getCursorStatus();
// ✗ Incorrect - raw fetch
const response = await fetch('/api/setup/cursor-status');
```
New Cursor API methods added to `HttpApiClient.setup`:
- `getCursorStatus()` - Installation and auth status
- `getCursorConfig()` - Configuration settings
- `setCursorDefaultModel(model)` - Update default model
- `setCursorModels(models)` - Update enabled models
### 2. Use Existing UI Components
All UI must use components from `@/components/ui/*`:
- `Card`, `CardHeader`, `CardTitle`, `CardContent` - Layout
- `Button`, `Badge`, `Label` - Controls
- `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` - Selection
- `Checkbox` - Toggle inputs
- `Alert`, `AlertDescription` - Messages
- `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` - Navigation
Icons from `lucide-react`:
- `Terminal` - Cursor provider
- `Bot` - Claude provider
- `CheckCircle2`, `XCircle` - Status indicators
- `Loader2` - Loading states
- `RefreshCw` - Refresh action
- `ExternalLink` - External links
### 3. Cursor CLI Installation Paths
Based on official cursor-agent install script:
**Linux/macOS:**
- Primary symlink: `~/.local/bin/cursor-agent`
- Versions directory: `~/.local/share/cursor-agent/versions/<version>/cursor-agent`
- Fallback: `/usr/local/bin/cursor-agent`
**Windows:**
- Primary: `%APPDATA%\Local\Programs\cursor-agent\cursor-agent.exe`
- Fallback: `~/.local/bin/cursor-agent.exe`
- Fallback: `C:\Program Files\cursor-agent\cursor-agent.exe`
### 4. Use @automaker/\* Packages
All server-side code must use shared packages from `libs/`:
**From `@automaker/types`:**
- Reuse existing `InstallationStatus` (don't create `CursorInstallationStatus`)
- Extend `ModelProvider` type to include `'cursor'`
- Extend `DEFAULT_MODELS` to include `cursor: 'auto'`
- Update `ModelOption.provider` from `'claude'` to `ModelProvider`
**From `@automaker/utils`:**
```typescript
import { createLogger, isAbortError } from '@automaker/utils';
const logger = createLogger('CursorProvider');
// Use isAbortError() for abort signal detection
```
**From `@automaker/platform`:**
```typescript
import { spawnJSONLProcess, getAutomakerDir } from '@automaker/platform';
// Use spawnJSONLProcess for JSONL streaming (handles buffering, timeout, abort)
// Use getAutomakerDir for consistent .automaker path resolution
```
### 5. Do NOT Extend @automaker/model-resolver
The model-resolver is Claude-specific and should **not** be extended for Cursor:
- Claude uses aliases (`sonnet``claude-sonnet-4-5-20250929`)
- Cursor model IDs are final-form (`claude-sonnet-4` passed directly to CLI)
- Cursor models have metadata (`hasThinking`, `tier`) that doesn't fit the string-only map
Cursor models use their own `CURSOR_MODEL_MAP` in `@automaker/types`.
---
## Risk Mitigation
1. **Phase Isolation**: Each phase can be tested independently
2. **Feature Flags**: Cursor provider can be disabled if issues arise
3. **Fallback**: Default to Claude provider for unknown models
4. **Graceful Degradation**: UI shows "not installed" state clearly
---
## How to Use This Plan
1. **Start with Phase 0** - Read and understand existing patterns
2. **Complete phases sequentially** - Dependencies require order
3. **Test each phase** - Run the verification steps before moving on
4. **Update status** - Mark phases as `in_progress`, `completed`, or `blocked`
5. **Document issues** - Add notes to individual phase files
---
## Changelog
| Date | Phase | Change |
| ---------- | ----- | ---------------------------------------------------------------------------------- |
| 2025-12-27 | - | Initial plan created |
| 2025-12-27 | 2 | Updated findCliPath() with platform-specific paths and versions directory scanning |
| 2025-12-27 | 4 | Updated to use HttpApiClient instead of raw fetch |
| 2025-12-27 | 6 | Updated to use HttpApiClient and existing UI components |
| 2025-12-27 | 7 | Updated to use HttpApiClient and existing UI components |
| 2025-12-27 | - | Added Design Decisions section to README |
| 2025-12-27 | 2 | Updated to use `createLogger` from `@automaker/utils` |
| 2025-12-27 | 4 | Updated to use `createLogger` from `@automaker/utils` |
| 2025-12-27 | 8 | Added proper UI component imports from `@/components/ui/*` |
| 2025-12-27 | 1 | Added tasks 1.5-1.7: ModelOption, DEFAULT_MODELS, reuse InstallationStatus |
| 2025-12-27 | 2 | Refactored to use `spawnJSONLProcess` and `isAbortError` from @automaker packages |
| 2025-12-27 | - | Added design decisions 4-5: @automaker packages usage, model-resolver note |
| 2025-12-28 | 9 | Completed: ModelSelector with Cursor models, provider tracking in execution events |

View File

@@ -1,392 +0,0 @@
# Cursor CLI Integration Analysis
> Phase 0 Analysis Document for AutoMaker Cursor Integration
> Generated: 2025-12-28
## Executive Summary
This document analyzes the existing Claude CLI integration architecture in AutoMaker and documents the Cursor CLI (cursor-agent) behavior to plan a parallel provider implementation.
## 1. Current Architecture Analysis
### 1.1 Provider System (`apps/server/src/providers/`)
#### BaseProvider (`base-provider.ts`)
- Abstract class with core interface for all providers
- Key methods:
- `execute(options)` - Runs the CLI process
- `buildCommand(options)` - Constructs CLI command
- `parseOutput(output)` - Parses CLI response
- `buildSystemPrompt(options)` - Constructs system prompts
- Uses `@automaker/platform.spawnJSONLProcess()` for CLI execution
- Handles abort signals for cancellation
#### ClaudeProvider (`claude-provider.ts`)
- Extends BaseProvider for Claude CLI
- Uses Claude Agent SDK via `@anthropic-ai/claude-code` package
- Key features:
- Thinking levels (none, low, medium, high, ultrathink)
- Model selection (haiku, sonnet, opus)
- System prompt injection
- Session/conversation management
- MCP server support
- SDK options built via `buildSdkOptions()` in `sdk-options.ts`
#### ProviderFactory (`provider-factory.ts`)
- Creates provider instances based on type
- Currently only supports 'claude' provider
- Extension point for adding new providers
#### Types (`types.ts`)
- `ProviderType`: Currently `'claude'`
- `ProviderConfig`: Configuration for providers
- `ExecuteResult`: Standardized result format
### 1.2 Service Integration
#### AgentService (`agent-service.ts`)
- Manages chat agent sessions
- Uses ClaudeProvider for execution
- Handles streaming output to clients
- Session persistence and history
#### AutoModeService (`auto-mode-service.ts`)
- Orchestrates feature generation workflow
- Manages concurrent feature execution
- Uses provider for each feature task
- Handles planning, execution, verification phases
#### SDK Options (`sdk-options.ts`)
- Builds Claude SDK options from settings
- Handles thinking level configuration
- Maps model aliases to full model IDs
- Configures MCP servers
### 1.3 UI Components
#### LogParser (`log-parser.ts`)
- Parses agent output into structured entries
- Detects entry types: tool_call, tool_result, phase, error, success, etc.
- Extracts metadata: tool name, file path, summary
- Claude-specific patterns (🔧 Tool:, etc.)
#### LogViewer (`log-viewer.tsx`)
- Renders parsed log entries
- Collapsible sections
- Filtering by type/category
- Tool-specific icons and colors
### 1.4 Setup Flow
#### Claude Status Detection (`get-claude-status.ts`)
- Checks CLI installation via `which`/`where`
- Searches common installation paths
- Detects authentication via:
- `~/.claude/stats-cache.json` (activity)
- `~/.claude/.credentials.json` (OAuth)
- Environment variables (`ANTHROPIC_API_KEY`)
- Stored API keys in AutoMaker
#### Setup View (`setup-view.tsx`)
- Multi-step wizard: welcome → theme → claude → github → complete
- `ClaudeSetupStep` handles CLI detection and auth
- Skip option for users without Claude
#### HTTP API Client (`http-api-client.ts`)
- Client-side API wrapper
- `setup.getClaudeStatus()` - Get Claude CLI status
- `setup.installClaude()` - Trigger installation
- `setup.authClaude()` - Trigger authentication
### 1.5 Types Package (`@automaker/types`)
#### Model Types (`model.ts`)
```typescript
CLAUDE_MODEL_MAP = {
haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929',
opus: 'claude-opus-4-5-20251101',
};
```
#### Settings Types (`settings.ts`)
- `ModelProvider`: Currently `'claude'` only
- `AIProfile`: Provider field for profiles
- `ThinkingLevel`: Reasoning intensity levels
#### Model Display (`model-display.ts`)
- `CLAUDE_MODELS`: UI metadata for model selection
- `getModelDisplayName()`: Human-readable names
## 2. Cursor CLI Behavior Analysis
### 2.1 Installation & Location
- **Binary**: `cursor-agent`
- **Installed at**: `~/.local/bin/cursor-agent`
- **Version tested**: `2025.12.17-996666f`
- **Config directory**: `~/.cursor/`
- **Config file**: `~/.cursor/cli-config.json`
### 2.2 Authentication
```bash
# Login command
cursor-agent login
# Status check
cursor-agent status # or `cursor-agent whoami`
# Logout
cursor-agent logout
```
Authentication is browser-based (OAuth) by default. Status output:
```
✓ Logged in as user@example.com
```
### 2.3 CLI Options
| Option | Description |
| -------------------------- | ------------------------------------------- |
| `--print` | Non-interactive mode, outputs to stdout |
| `--output-format <format>` | `text`, `json`, or `stream-json` |
| `--model <model>` | Model selection (e.g., `gpt-5`, `sonnet-4`) |
| `--workspace <path>` | Working directory |
| `--resume [chatId]` | Resume previous session |
| `--api-key <key>` | API key (or `CURSOR_API_KEY` env) |
| `--force` | Auto-approve tool calls |
| `--approve-mcps` | Auto-approve MCP servers |
### 2.4 Output Formats
#### JSON Format (`--output-format json`)
Single JSON object on completion:
```json
{
"type": "result",
"subtype": "success",
"is_error": false,
"duration_ms": 2691,
"result": "Response text here",
"session_id": "uuid",
"request_id": "uuid"
}
```
#### Stream-JSON Format (`--output-format stream-json`)
JSONL (one JSON per line) during execution:
1. **Init event**:
```json
{
"type": "system",
"subtype": "init",
"apiKeySource": "login",
"cwd": "/path",
"session_id": "uuid",
"model": "Composer 1"
}
```
2. **User message**:
```json
{
"type": "user",
"message": { "role": "user", "content": [{ "type": "text", "text": "prompt" }] },
"session_id": "uuid"
}
```
3. **Thinking events**:
```json
{"type":"thinking","subtype":"delta","text":"","session_id":"uuid","timestamp_ms":123}
{"type":"thinking","subtype":"completed","session_id":"uuid","timestamp_ms":123}
```
4. **Assistant response**:
```json
{
"type": "assistant",
"message": { "role": "assistant", "content": [{ "type": "text", "text": "response" }] },
"session_id": "uuid"
}
```
5. **Result**:
```json
{
"type": "result",
"subtype": "success",
"duration_ms": 1825,
"is_error": false,
"result": "response",
"session_id": "uuid"
}
```
### 2.5 Config File Structure
`~/.cursor/cli-config.json`:
```json
{
"permissions": {
"allow": ["Shell(ls)"],
"deny": []
},
"version": 1,
"model": {
"modelId": "composer-1",
"displayModelId": "composer-1",
"displayName": "Composer 1"
},
"approvalMode": "allowlist",
"sandbox": {
"mode": "disabled",
"networkAccess": "allowlist"
}
}
```
### 2.6 Available Models
From `--help`:
- `gpt-5`
- `sonnet-4`
- `sonnet-4-thinking`
- `composer-1` (default)
### 2.7 MCP Support
```bash
cursor-agent mcp list # List configured servers
cursor-agent mcp login <id> # Authenticate with server
cursor-agent mcp list-tools <id> # List tools for server
cursor-agent mcp disable <id> # Remove from approved list
```
## 3. Integration Strategy
### 3.1 Types to Add (`@automaker/types`)
```typescript
// settings.ts
export type ModelProvider = 'claude' | 'cursor';
// model.ts (new: cursor-models.ts)
export type CursorModelId = 'composer-1' | 'gpt-5' | 'sonnet-4' | 'sonnet-4-thinking';
export const CURSOR_MODEL_MAP: Record<string, CursorModelId> = {
composer: 'composer-1',
gpt5: 'gpt-5',
sonnet: 'sonnet-4',
'sonnet-thinking': 'sonnet-4-thinking',
};
```
### 3.2 Provider Implementation
New `CursorProvider` class extending `BaseProvider`:
- Override `buildCommand()` for cursor-agent CLI
- Override `parseOutput()` for Cursor's JSONL format
- Handle stream-json output parsing
- Map thinking events to existing log format
### 3.3 Setup Flow Changes
1. Add `CursorSetupStep` component
2. Add `get-cursor-status.ts` route handler
3. Update setup wizard to detect/choose providers
4. Add Cursor authentication flow
### 3.4 UI Updates
1. Extend `LogParser` for Cursor event types
2. Add Cursor icon (Terminal from lucide-react)
3. Update model selectors for Cursor models
4. Provider toggle in settings
## 4. Key Differences: Claude vs Cursor
| Aspect | Claude CLI | Cursor CLI |
| ---------- | --------------------------- | ----------------------- |
| Binary | `claude` | `cursor-agent` |
| SDK | `@anthropic-ai/claude-code` | Direct CLI spawn |
| Config dir | `~/.claude/` | `~/.cursor/` |
| Auth file | `.credentials.json` | `cli-config.json` |
| Output | SDK events | JSONL stream |
| Thinking | Extended thinking API | `thinking` events |
| Models | haiku/sonnet/opus | composer/gpt-5/sonnet-4 |
| Session | Conversation system | `session_id` in output |
## 5. Files to Create/Modify
### Phase 1: Types
- [ ] `libs/types/src/cursor-models.ts` - Cursor model definitions
- [ ] `libs/types/src/settings.ts` - Update ModelProvider type
- [ ] `libs/types/src/index.ts` - Export new types
### Phase 2: Provider
- [ ] `apps/server/src/providers/cursor-provider.ts` - New provider
- [ ] `apps/server/src/providers/provider-factory.ts` - Add cursor
- [ ] `apps/server/src/providers/types.ts` - Update ProviderType
### Phase 3: Setup
- [ ] `apps/server/src/routes/setup/get-cursor-status.ts` - Detection
- [ ] `apps/server/src/routes/setup/routes/cursor-status.ts` - Route
- [ ] `apps/server/src/routes/setup/index.ts` - Register route
### Phase 4: UI
- [ ] `apps/ui/src/components/views/setup-view/steps/CursorSetupStep.tsx`
- [ ] `apps/ui/src/lib/log-parser.ts` - Cursor event parsing
- [ ] `apps/ui/src/lib/http-api-client.ts` - Add cursor endpoints
## 6. Verification Checklist
- [x] Read all core provider files
- [x] Read service integration files
- [x] Read UI streaming/logging files
- [x] Read setup flow files
- [x] Read types package files
- [x] Document Cursor CLI behavior
- [x] Create this analysis document
## 7. Next Steps
Proceed to **Phase 1: Types & Interfaces** to:
1. Add Cursor model types to `@automaker/types`
2. Update `ModelProvider` type
3. Create cursor model display constants

View File

@@ -1,201 +0,0 @@
# Phase 0: Analysis & Documentation
**Status:** `complete`
**Dependencies:** None
**Estimated Effort:** Research only (no code changes)
---
## Objective
Understand existing AutoMaker architecture patterns before writing any code. Document findings to ensure consistent implementation.
---
## Tasks
### Task 0.1: Read Core Provider Files
**Status:** `complete`
Read and understand these files:
| File | Purpose | Key Patterns |
| ----------------------------------------------- | ------------------------ | --------------------------------------- |
| `apps/server/src/providers/base-provider.ts` | Abstract base class | `executeQuery()` AsyncGenerator pattern |
| `apps/server/src/providers/claude-provider.ts` | Reference implementation | SDK integration, streaming |
| `apps/server/src/providers/provider-factory.ts` | Model routing | `getProviderForModel()` pattern |
| `apps/server/src/providers/types.ts` | Type definitions | `ProviderMessage`, `ExecuteOptions` |
**Verification:**
```bash
# Files should exist and be readable
cat apps/server/src/providers/base-provider.ts | head -50
cat apps/server/src/providers/claude-provider.ts | head -100
```
### Task 0.2: Read Service Integration
**Status:** `complete`
Understand how providers are consumed:
| File | Purpose | Key Patterns |
| ----------------------------------------------- | --------------------- | ---------------------------------- |
| `apps/server/src/services/agent-service.ts` | Chat sessions | Provider streaming, event emission |
| `apps/server/src/services/auto-mode-service.ts` | Autonomous tasks | executeOptions, tool handling |
| `apps/server/src/lib/sdk-options.ts` | Configuration factory | Tool presets, max turns |
### Task 0.3: Read UI Streaming/Logging
**Status:** `complete`
Understand log parsing and display:
| File | Purpose | Key Patterns |
| ------------------------------------------ | ------------------ | ---------------------------- |
| `apps/ui/src/lib/log-parser.ts` | Parse agent output | Entry types, tool categories |
| `apps/ui/src/components/ui/log-viewer.tsx` | Display logs | Collapsible entries, search |
### Task 0.4: Read Setup Flow
**Status:** `complete`
Understand setup wizard patterns:
| File | Purpose | Key Patterns |
| --------------------------------------------------- | ------------------ | ------------------------ |
| `apps/server/src/routes/setup/index.ts` | Route registration | Handler patterns |
| `apps/server/src/routes/setup/get-claude-status.ts` | CLI detection | Installation check logic |
| `apps/ui/src/components/views/setup-view.tsx` | Wizard UI | Step components |
### Task 0.5: Read Types Package
**Status:** `complete`
Understand type definitions:
| File | Purpose | Key Patterns |
| --------------------------------- | -------------- | ---------------------------- |
| `libs/types/src/index.ts` | Re-exports | Export patterns |
| `libs/types/src/settings.ts` | Settings types | `AIProfile`, `ModelProvider` |
| `libs/types/src/model.ts` | Model aliases | `CLAUDE_MODEL_MAP` |
| `libs/types/src/model-display.ts` | UI metadata | Display info pattern |
### Task 0.6: Document Cursor CLI Behavior
**Status:** `complete`
Test and document Cursor CLI behavior:
```bash
# Check installation
cursor-agent --version
# Check auth status (if available)
cursor-agent status 2>&1 || echo "No status command"
# Test stream-json output (dry run)
echo "Test prompt" | cursor-agent -p --output-format stream-json --model auto 2>&1 | head -20
```
Document:
- [x] Exact event sequence for simple prompt
- [x] Error message formats
- [x] Exit codes for different failure modes
- [x] How tool calls appear in stream
---
## Deliverable: Analysis Document
Create `docs/cursor-integration-analysis.md` with findings:
```markdown
# Cursor CLI Integration Analysis
## Provider Pattern Summary
### BaseProvider Interface
- `executeQuery()` returns `AsyncGenerator<ProviderMessage>`
- Messages must match format: { type, message?, result?, error? }
- Session IDs propagated through all messages
### ClaudeProvider Patterns
- Uses Claude Agent SDK `query()` function
- Streaming handled natively by SDK
- Yields messages directly from SDK stream
### Key Interfaces
[Document: ProviderMessage, ExecuteOptions, InstallationStatus]
## Cursor CLI Behavior
### Stream Event Sequence
1. system/init - session start
2. user - input prompt
3. assistant - response text
4. tool_call/started - tool invocation
5. tool_call/completed - tool result
6. result/success - final output
### Event Format Differences
[Document any transformations needed]
### Error Scenarios
- Not authenticated: [error message/code]
- Rate limited: [error message/code]
- Network error: [error message/code]
## Integration Points
### Files to Create
[List with descriptions]
### Files to Modify
[List with specific changes needed]
## Open Questions
[Any unresolved issues]
```
---
## Verification Checklist
Before marking this phase complete:
- [x] All provider files read and understood
- [x] Service integration patterns documented
- [x] Log parser patterns understood
- [x] Setup wizard flow mapped
- [x] Types package structure documented
- [x] Cursor CLI behavior tested (if installed)
- [x] Analysis document created in `docs/`
---
## Notes
- This phase is **read-only** - no code changes
- Document anything unclear for later clarification
- Note any differences from the high-level plan provided
---
## References
- [Cursor CLI Output Format](https://cursor.com/docs/cli/reference/output-format)
- [Cursor CLI Usage](https://cursor.com/docs/cli/using)
- [Cursor CLI GitHub Actions](https://cursor.com/docs/cli/github-actions)

View File

@@ -1,443 +0,0 @@
# Phase 1: Core Types & Configuration
**Status:** `completed`
**Dependencies:** Phase 0 (Analysis)
**Estimated Effort:** Small (type definitions only)
---
## Objective
Define all Cursor-specific types and extend existing types to support the new provider.
---
## Tasks
### Task 1.1: Create Cursor Model Definitions
**Status:** `completed`
**File:** `libs/types/src/cursor-models.ts`
```typescript
/**
* Cursor CLI Model IDs
* Reference: https://cursor.com/docs
*/
export type CursorModelId =
| 'auto' // Auto-select best model
| 'claude-sonnet-4' // Claude Sonnet 4
| 'claude-sonnet-4-thinking' // Claude Sonnet 4 with extended thinking
| 'gpt-4o' // GPT-4o
| 'gpt-4o-mini' // GPT-4o Mini
| 'gemini-2.5-pro' // Gemini 2.5 Pro
| 'o3-mini'; // O3 Mini
/**
* Cursor model metadata
*/
export interface CursorModelConfig {
id: CursorModelId;
label: string;
description: string;
hasThinking: boolean;
tier: 'free' | 'pro';
}
/**
* Complete model map for Cursor CLI
*/
export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
auto: {
id: 'auto',
label: 'Auto (Recommended)',
description: 'Automatically selects the best model for each task',
hasThinking: false,
tier: 'free',
},
'claude-sonnet-4': {
id: 'claude-sonnet-4',
label: 'Claude Sonnet 4',
description: 'Anthropic Claude Sonnet 4 via Cursor',
hasThinking: false,
tier: 'pro',
},
'claude-sonnet-4-thinking': {
id: 'claude-sonnet-4-thinking',
label: 'Claude Sonnet 4 (Thinking)',
description: 'Claude Sonnet 4 with extended thinking enabled',
hasThinking: true,
tier: 'pro',
},
'gpt-4o': {
id: 'gpt-4o',
label: 'GPT-4o',
description: 'OpenAI GPT-4o via Cursor',
hasThinking: false,
tier: 'pro',
},
'gpt-4o-mini': {
id: 'gpt-4o-mini',
label: 'GPT-4o Mini',
description: 'OpenAI GPT-4o Mini (faster, cheaper)',
hasThinking: false,
tier: 'free',
},
'gemini-2.5-pro': {
id: 'gemini-2.5-pro',
label: 'Gemini 2.5 Pro',
description: 'Google Gemini 2.5 Pro via Cursor',
hasThinking: false,
tier: 'pro',
},
'o3-mini': {
id: 'o3-mini',
label: 'O3 Mini',
description: 'OpenAI O3 Mini reasoning model',
hasThinking: true,
tier: 'pro',
},
};
/**
* Helper: Check if model has thinking capability
*/
export function cursorModelHasThinking(modelId: CursorModelId): boolean {
return CURSOR_MODEL_MAP[modelId]?.hasThinking ?? false;
}
/**
* Helper: Get display name for model
*/
export function getCursorModelLabel(modelId: CursorModelId): string {
return CURSOR_MODEL_MAP[modelId]?.label ?? modelId;
}
/**
* Helper: Get all cursor model IDs
*/
export function getAllCursorModelIds(): CursorModelId[] {
return Object.keys(CURSOR_MODEL_MAP) as CursorModelId[];
}
```
### Task 1.2: Create Cursor CLI Types
**Status:** `completed`
**File:** `libs/types/src/cursor-cli.ts`
```typescript
import { CursorModelId } from './cursor-models';
/**
* Cursor CLI configuration file schema
* Stored in: .automaker/cursor-config.json
*/
export interface CursorCliConfig {
defaultModel?: CursorModelId;
models?: CursorModelId[]; // Enabled models
mcpServers?: string[]; // MCP server configs to load
rules?: string[]; // .cursor/rules paths
}
/**
* Cursor authentication status
*/
export interface CursorAuthStatus {
authenticated: boolean;
method: 'login' | 'api_key' | 'none';
hasCredentialsFile?: boolean;
}
/**
* NOTE: Reuse existing InstallationStatus from provider.ts
* The existing type already has: installed, path, version, method, hasApiKey, authenticated
*
* Add 'login' to the method union if needed:
* method?: 'cli' | 'npm' | 'brew' | 'sdk' | 'login';
*/
/**
* Cursor stream-json event types (from CLI output)
*/
export interface CursorSystemEvent {
type: 'system';
subtype: 'init';
apiKeySource: 'env' | 'flag' | 'login';
cwd: string;
session_id: string;
model: string;
permissionMode: string;
}
export interface CursorUserEvent {
type: 'user';
message: {
role: 'user';
content: Array<{ type: 'text'; text: string }>;
};
session_id: string;
}
export interface CursorAssistantEvent {
type: 'assistant';
message: {
role: 'assistant';
content: Array<{ type: 'text'; text: string }>;
};
session_id: string;
}
export interface CursorToolCallEvent {
type: 'tool_call';
subtype: 'started' | 'completed';
call_id: string;
tool_call: {
readToolCall?: {
args: { path: string };
result?: {
success?: {
content: string;
isEmpty: boolean;
exceededLimit: boolean;
totalLines: number;
totalChars: number;
};
};
};
writeToolCall?: {
args: { path: string; fileText: string; toolCallId?: string };
result?: {
success?: {
path: string;
linesCreated: number;
fileSize: number;
};
};
};
function?: {
name: string;
arguments: string;
};
};
session_id: string;
}
export interface CursorResultEvent {
type: 'result';
subtype: 'success' | 'error';
duration_ms: number;
duration_api_ms: number;
is_error: boolean;
result: string;
session_id: string;
request_id?: string;
error?: string;
}
export type CursorStreamEvent =
| CursorSystemEvent
| CursorUserEvent
| CursorAssistantEvent
| CursorToolCallEvent
| CursorResultEvent;
```
### Task 1.3: Extend ModelProvider Type
**Status:** `completed`
**File:** `libs/types/src/settings.ts`
Find and update:
```typescript
// BEFORE:
export type ModelProvider = 'claude';
// AFTER:
export type ModelProvider = 'claude' | 'cursor';
```
### Task 1.4: Add Cursor Profile Config Type
**Status:** `skipped` (not needed - thinking is embedded in model ID)
**File:** `libs/types/src/settings.ts`
Add after existing AIProfile interface:
```typescript
/**
* Cursor-specific profile configuration
* Note: For Cursor, thinking is embedded in model ID (e.g., 'claude-sonnet-4-thinking')
*/
export interface CursorProfileConfig {
model: CursorModelId;
// No separate thinkingLevel needed - embedded in model ID
}
```
### Task 1.5: Update ModelOption Interface
**Status:** `completed`
**File:** `libs/types/src/model-display.ts`
Update the hardcoded provider type to use ModelProvider:
```typescript
// BEFORE (line 24):
export interface ModelOption {
id: AgentModel;
label: string;
description: string;
badge?: string;
provider: 'claude'; // ❌ Hardcoded
}
// AFTER:
import { ModelProvider } from './settings.js';
export interface ModelOption {
id: AgentModel | CursorModelId; // Union for both providers
label: string;
description: string;
badge?: string;
provider: ModelProvider; // ✅ Supports both 'claude' and 'cursor'
}
```
### Task 1.6: Extend DEFAULT_MODELS
**Status:** `completed`
**File:** `libs/types/src/model.ts`
Add cursor default model:
```typescript
// BEFORE:
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
} as const;
// AFTER:
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
cursor: 'auto', // Cursor's recommended default
} as const;
```
### Task 1.7: Update Type Exports
**Status:** `completed`
**File:** `libs/types/src/index.ts`
Add exports:
```typescript
// Cursor types
export * from './cursor-models.js';
export * from './cursor-cli.js';
```
---
## Verification
### Test 1: Type Compilation
```bash
cd libs/types
pnpm build
```
**Expected:** No compilation errors
### Test 2: Import Check
Create a temporary test file:
```typescript
// test-cursor-types.ts
import {
CursorModelId,
CursorModelConfig,
CURSOR_MODEL_MAP,
cursorModelHasThinking,
CursorStreamEvent,
CursorCliConfig,
ModelProvider,
} from '@automaker/types';
// Should compile without errors
const model: CursorModelId = 'claude-sonnet-4';
const provider: ModelProvider = 'cursor';
const hasThinking = cursorModelHasThinking('claude-sonnet-4-thinking');
console.log(model, provider, hasThinking);
```
```bash
npx tsc test-cursor-types.ts --noEmit
rm test-cursor-types.ts
```
**Expected:** No errors
### Test 3: Model Map Validity
```typescript
// In Node REPL or test file
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
const modelIds = Object.keys(CURSOR_MODEL_MAP) as CursorModelId[];
console.log('Models:', modelIds.length);
// All models should have required fields
for (const [id, config] of Object.entries(CURSOR_MODEL_MAP)) {
console.assert(config.id === id, `ID mismatch: ${id}`);
console.assert(typeof config.label === 'string', `Missing label: ${id}`);
console.assert(typeof config.hasThinking === 'boolean', `Missing hasThinking: ${id}`);
console.assert(['free', 'pro'].includes(config.tier), `Invalid tier: ${id}`);
}
console.log('All models valid');
```
**Expected:** All assertions pass
---
## Verification Checklist
Before marking this phase complete:
- [ ] `libs/types/src/cursor-models.ts` created with all model definitions
- [ ] `libs/types/src/cursor-cli.ts` created with CLI types
- [ ] `libs/types/src/settings.ts` extended with `cursor` provider
- [ ] `libs/types/src/index.ts` exports new types
- [ ] `pnpm build` succeeds in libs/types
- [ ] No TypeScript errors in dependent packages
- [ ] Model map contains all expected models
---
## Files Changed
| File | Action | Description |
| --------------------------------- | ------ | ----------------------------- |
| `libs/types/src/cursor-models.ts` | Create | Model definitions and helpers |
| `libs/types/src/cursor-cli.ts` | Create | CLI and stream event types |
| `libs/types/src/settings.ts` | Modify | Add `cursor` to ModelProvider |
| `libs/types/src/index.ts` | Modify | Export new types |
---
## Notes
- Model IDs may need updating as Cursor adds/removes models
- The `hasThinking` property is critical for UI display
- Stream event types must match actual CLI output exactly

View File

@@ -1,649 +0,0 @@
# Phase 10: Testing & Validation
**Status:** `pending`
**Dependencies:** All previous phases
**Estimated Effort:** Medium (comprehensive testing)
---
## Objective
Create comprehensive tests and perform validation to ensure the Cursor CLI integration works correctly across all scenarios.
---
## Tasks
### Task 10.1: Unit Tests - Cursor Provider
**Status:** `pending`
**File:** `apps/server/tests/unit/providers/cursor-provider.test.ts`
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { CursorProvider, CursorErrorCode } from '../../../src/providers/cursor-provider';
import { execSync, spawn } from 'child_process';
import * as fs from 'fs';
// Mock child_process
vi.mock('child_process', () => ({
execSync: vi.fn(),
spawn: vi.fn(),
}));
// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
}));
describe('CursorProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getName', () => {
it('should return "cursor"', () => {
const provider = new CursorProvider();
expect(provider.getName()).toBe('cursor');
});
});
describe('isInstalled', () => {
it('should return true when CLI is found in PATH', async () => {
vi.mocked(execSync).mockReturnValue('/usr/local/bin/cursor-agent\n');
vi.mocked(fs.existsSync).mockReturnValue(true);
const provider = new CursorProvider();
const result = await provider.isInstalled();
expect(result).toBe(true);
});
it('should return false when CLI is not found', async () => {
vi.mocked(execSync).mockImplementation(() => {
throw new Error('not found');
});
vi.mocked(fs.existsSync).mockReturnValue(false);
const provider = new CursorProvider();
const result = await provider.isInstalled();
expect(result).toBe(false);
});
});
describe('checkAuth', () => {
it('should detect API key authentication', async () => {
process.env.CURSOR_API_KEY = 'test-key';
const provider = new CursorProvider();
const result = await provider.checkAuth();
expect(result.authenticated).toBe(true);
expect(result.method).toBe('api_key');
delete process.env.CURSOR_API_KEY;
});
it('should detect login authentication from credentials file', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ accessToken: 'token' }));
const provider = new CursorProvider();
const result = await provider.checkAuth();
expect(result.authenticated).toBe(true);
expect(result.method).toBe('login');
});
it('should return not authenticated when no credentials', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const provider = new CursorProvider();
const result = await provider.checkAuth();
expect(result.authenticated).toBe(false);
expect(result.method).toBe('none');
});
});
describe('parseStreamLine', () => {
it('should parse valid JSON event', () => {
const provider = new CursorProvider();
const line = '{"type":"system","subtype":"init","session_id":"abc"}';
const result = (provider as any).parseStreamLine(line);
expect(result).toEqual({
type: 'system',
subtype: 'init',
session_id: 'abc',
});
});
it('should return null for invalid JSON', () => {
const provider = new CursorProvider();
const result = (provider as any).parseStreamLine('not json');
expect(result).toBeNull();
});
it('should return null for empty lines', () => {
const provider = new CursorProvider();
expect((provider as any).parseStreamLine('')).toBeNull();
expect((provider as any).parseStreamLine(' ')).toBeNull();
});
});
describe('mapError', () => {
it('should map authentication errors', () => {
const provider = new CursorProvider();
const error = (provider as any).mapError('Error: not authenticated', 1);
expect(error.code).toBe(CursorErrorCode.NOT_AUTHENTICATED);
expect(error.recoverable).toBe(true);
expect(error.suggestion).toBeDefined();
});
it('should map rate limit errors', () => {
const provider = new CursorProvider();
const error = (provider as any).mapError('Rate limit exceeded', 1);
expect(error.code).toBe(CursorErrorCode.RATE_LIMITED);
expect(error.recoverable).toBe(true);
});
it('should map network errors', () => {
const provider = new CursorProvider();
const error = (provider as any).mapError('ECONNREFUSED', 1);
expect(error.code).toBe(CursorErrorCode.NETWORK_ERROR);
expect(error.recoverable).toBe(true);
});
it('should return unknown error for unrecognized messages', () => {
const provider = new CursorProvider();
const error = (provider as any).mapError('Something weird happened', 1);
expect(error.code).toBe(CursorErrorCode.UNKNOWN);
});
});
describe('getAvailableModels', () => {
it('should return all Cursor models', () => {
const provider = new CursorProvider();
const models = provider.getAvailableModels();
expect(models.length).toBeGreaterThan(0);
expect(models.every((m) => m.provider === 'cursor')).toBe(true);
expect(models.some((m) => m.id.includes('auto'))).toBe(true);
});
});
});
```
### Task 10.2: Unit Tests - Provider Factory
**Status:** `pending`
**File:** `apps/server/tests/unit/providers/provider-factory.test.ts`
```typescript
import { describe, it, expect } from 'vitest';
import { ProviderFactory } from '../../../src/providers/provider-factory';
import { ClaudeProvider } from '../../../src/providers/claude-provider';
import { CursorProvider } from '../../../src/providers/cursor-provider';
describe('ProviderFactory', () => {
describe('getProviderNameForModel', () => {
it('should route cursor-prefixed models to cursor', () => {
expect(ProviderFactory.getProviderNameForModel('cursor-auto')).toBe('cursor');
expect(ProviderFactory.getProviderNameForModel('cursor-gpt-4o')).toBe('cursor');
expect(ProviderFactory.getProviderNameForModel('cursor-claude-sonnet-4')).toBe('cursor');
});
it('should route claude models to claude', () => {
expect(ProviderFactory.getProviderNameForModel('claude-sonnet-4')).toBe('claude');
expect(ProviderFactory.getProviderNameForModel('opus')).toBe('claude');
expect(ProviderFactory.getProviderNameForModel('sonnet')).toBe('claude');
expect(ProviderFactory.getProviderNameForModel('haiku')).toBe('claude');
});
it('should default unknown models to claude', () => {
expect(ProviderFactory.getProviderNameForModel('unknown-model')).toBe('claude');
expect(ProviderFactory.getProviderNameForModel('random')).toBe('claude');
});
});
describe('getProviderForModel', () => {
it('should return CursorProvider for cursor models', () => {
const provider = ProviderFactory.getProviderForModel('cursor-auto');
expect(provider).toBeInstanceOf(CursorProvider);
expect(provider.getName()).toBe('cursor');
});
it('should return ClaudeProvider for claude models', () => {
const provider = ProviderFactory.getProviderForModel('sonnet');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(provider.getName()).toBe('claude');
});
});
describe('getAllProviders', () => {
it('should return both providers', () => {
const providers = ProviderFactory.getAllProviders();
const names = providers.map((p) => p.getName());
expect(names).toContain('claude');
expect(names).toContain('cursor');
});
});
describe('getProviderByName', () => {
it('should return correct provider by name', () => {
expect(ProviderFactory.getProviderByName('cursor')?.getName()).toBe('cursor');
expect(ProviderFactory.getProviderByName('claude')?.getName()).toBe('claude');
expect(ProviderFactory.getProviderByName('unknown')).toBeNull();
});
});
describe('getAllAvailableModels', () => {
it('should include models from all providers', () => {
const models = ProviderFactory.getAllAvailableModels();
const cursorModels = models.filter((m) => m.provider === 'cursor');
const claudeModels = models.filter((m) => m.provider === 'claude');
expect(cursorModels.length).toBeGreaterThan(0);
expect(claudeModels.length).toBeGreaterThan(0);
});
});
});
```
### Task 10.3: Unit Tests - Types
**Status:** `pending`
**File:** `libs/types/tests/cursor-types.test.ts`
```typescript
import { describe, it, expect } from 'vitest';
import {
CURSOR_MODEL_MAP,
cursorModelHasThinking,
getCursorModelLabel,
getAllCursorModelIds,
CursorModelId,
} from '../src/cursor-models';
import { profileHasThinking, getProfileModelString, AIProfile } from '../src/settings';
describe('Cursor Model Types', () => {
describe('CURSOR_MODEL_MAP', () => {
it('should have all required models', () => {
const requiredModels: CursorModelId[] = [
'auto',
'claude-sonnet-4',
'claude-sonnet-4-thinking',
'gpt-4o',
'gpt-4o-mini',
];
for (const model of requiredModels) {
expect(CURSOR_MODEL_MAP[model]).toBeDefined();
expect(CURSOR_MODEL_MAP[model].id).toBe(model);
}
});
it('should have valid tier values', () => {
for (const config of Object.values(CURSOR_MODEL_MAP)) {
expect(['free', 'pro']).toContain(config.tier);
}
});
});
describe('cursorModelHasThinking', () => {
it('should return true for thinking models', () => {
expect(cursorModelHasThinking('claude-sonnet-4-thinking')).toBe(true);
expect(cursorModelHasThinking('o3-mini')).toBe(true);
});
it('should return false for non-thinking models', () => {
expect(cursorModelHasThinking('auto')).toBe(false);
expect(cursorModelHasThinking('gpt-4o')).toBe(false);
expect(cursorModelHasThinking('claude-sonnet-4')).toBe(false);
});
});
describe('getCursorModelLabel', () => {
it('should return correct labels', () => {
expect(getCursorModelLabel('auto')).toBe('Auto (Recommended)');
expect(getCursorModelLabel('gpt-4o')).toBe('GPT-4o');
});
it('should return model ID for unknown models', () => {
expect(getCursorModelLabel('unknown' as CursorModelId)).toBe('unknown');
});
});
});
describe('Profile Helpers', () => {
describe('profileHasThinking', () => {
it('should detect Claude thinking levels', () => {
const profile: AIProfile = {
id: '1',
name: 'Test',
description: '',
provider: 'claude',
model: 'sonnet',
thinkingLevel: 'high',
isBuiltIn: false,
};
expect(profileHasThinking(profile)).toBe(true);
profile.thinkingLevel = 'none';
expect(profileHasThinking(profile)).toBe(false);
});
it('should detect Cursor thinking models', () => {
const profile: AIProfile = {
id: '1',
name: 'Test',
description: '',
provider: 'cursor',
cursorModel: 'claude-sonnet-4-thinking',
isBuiltIn: false,
};
expect(profileHasThinking(profile)).toBe(true);
profile.cursorModel = 'gpt-4o';
expect(profileHasThinking(profile)).toBe(false);
});
});
describe('getProfileModelString', () => {
it('should format Cursor models correctly', () => {
const profile: AIProfile = {
id: '1',
name: 'Test',
description: '',
provider: 'cursor',
cursorModel: 'gpt-4o',
isBuiltIn: false,
};
expect(getProfileModelString(profile)).toBe('cursor-gpt-4o');
});
it('should format Claude models correctly', () => {
const profile: AIProfile = {
id: '1',
name: 'Test',
description: '',
provider: 'claude',
model: 'sonnet',
isBuiltIn: false,
};
expect(getProfileModelString(profile)).toBe('sonnet');
});
});
});
```
### Task 10.4: Integration Tests
**Status:** `pending`
**File:** `apps/server/tests/integration/cursor-integration.test.ts`
```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { CursorProvider } from '../../src/providers/cursor-provider';
import { ProviderFactory } from '../../src/providers/provider-factory';
describe('Cursor Integration (requires cursor-agent)', () => {
let provider: CursorProvider;
let isInstalled: boolean;
beforeAll(async () => {
provider = new CursorProvider();
isInstalled = await provider.isInstalled();
});
describe('when cursor-agent is installed', () => {
it.skipIf(!isInstalled)('should get version', async () => {
const version = await provider.getVersion();
expect(version).toBeTruthy();
expect(typeof version).toBe('string');
});
it.skipIf(!isInstalled)('should check auth status', async () => {
const auth = await provider.checkAuth();
expect(auth).toHaveProperty('authenticated');
expect(auth).toHaveProperty('method');
});
it.skipIf(!isInstalled)('should detect installation', async () => {
const status = await provider.detectInstallation();
expect(status.installed).toBe(true);
expect(status.path).toBeTruthy();
});
});
describe('when cursor-agent is not installed', () => {
it.skipIf(isInstalled)('should report not installed', async () => {
const status = await provider.detectInstallation();
expect(status.installed).toBe(false);
});
});
});
```
### Task 10.5: E2E Tests
**Status:** `pending`
**File:** `apps/ui/tests/e2e/cursor-setup.spec.ts`
```typescript
import { test, expect } from '@playwright/test';
test.describe('Cursor Setup Wizard', () => {
test('should show Cursor setup step', async ({ page }) => {
// Navigate to setup (fresh install)
await page.goto('/setup');
// Wait for Cursor step to appear
await expect(page.getByText('Cursor CLI Setup')).toBeVisible();
await expect(page.getByText('Optional')).toBeVisible();
});
test('should allow skipping Cursor setup', async ({ page }) => {
await page.goto('/setup');
// Find and click skip button
await page.getByRole('button', { name: 'Skip for now' }).click();
// Should proceed to next step
await expect(page.getByText('Cursor CLI Setup')).not.toBeVisible();
});
test('should show installation instructions when not installed', async ({ page }) => {
await page.goto('/setup');
// Check for install command
await expect(page.getByText('curl https://cursor.com/install')).toBeVisible();
});
});
test.describe('Cursor Settings', () => {
test('should show Cursor tab in settings', async ({ page }) => {
await page.goto('/settings/providers');
// Should have tabs for both providers
await expect(page.getByRole('tab', { name: 'Claude' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Cursor' })).toBeVisible();
});
test('should switch between provider tabs', async ({ page }) => {
await page.goto('/settings/providers');
// Click Cursor tab
await page.getByRole('tab', { name: 'Cursor' }).click();
// Should show Cursor settings
await expect(page.getByText('Cursor CLI Status')).toBeVisible();
});
});
```
### Task 10.6: Manual Testing Checklist
**Status:** `pending`
Create a manual testing checklist:
```markdown
## Manual Testing Checklist
### Setup Flow
- [ ] Fresh install shows Cursor step
- [ ] Can skip Cursor setup
- [ ] Installation status is accurate
- [ ] Login flow works (copy command, poll for auth)
- [ ] Refresh button updates status
### Settings
- [ ] Provider tabs work
- [ ] Cursor status shows correctly
- [ ] Model selection works
- [ ] Default model saves
- [ ] Enabled models save
### Profiles
- [ ] Can create Cursor profile
- [ ] Provider switch resets options
- [ ] Cursor models show thinking badge
- [ ] Built-in Cursor profiles appear
- [ ] Profile cards show provider info
### Execution
- [ ] Tasks with Cursor models execute
- [ ] Streaming works correctly
- [ ] Tool calls are displayed
- [ ] Errors show suggestions
- [ ] Can abort Cursor tasks
### Log Viewer
- [ ] Cursor events parsed correctly
- [ ] Tool calls categorized
- [ ] File paths highlighted
- [ ] Provider badge shown
### Edge Cases
- [ ] Switch provider mid-session
- [ ] Cursor not installed handling
- [ ] Network errors handled
- [ ] Rate limiting handled
- [ ] Auth expired handling
```
---
## Verification
### Test 1: Run All Unit Tests
```bash
pnpm test:unit
```
All tests should pass.
### Test 2: Run Integration Tests
```bash
pnpm test:integration
```
Tests requiring cursor-agent will be skipped if not installed.
### Test 3: Run E2E Tests
```bash
pnpm test:e2e
```
Browser tests should pass.
### Test 4: Type Check
```bash
pnpm typecheck
```
No TypeScript errors.
### Test 5: Lint Check
```bash
pnpm lint
```
No linting errors.
### Test 6: Build
```bash
pnpm build
```
Build should succeed without errors.
---
## Verification Checklist
Before marking this phase complete:
- [ ] Unit tests pass (cursor-provider)
- [ ] Unit tests pass (provider-factory)
- [ ] Unit tests pass (types)
- [ ] Integration tests pass (or skip if not installed)
- [ ] E2E tests pass
- [ ] Manual testing checklist completed
- [ ] No TypeScript errors
- [ ] No linting errors
- [ ] Build succeeds
- [ ] Documentation updated
---
## Files Changed
| File | Action | Description |
| ----------------------------------------------------------- | ------ | ----------------- |
| `apps/server/tests/unit/providers/cursor-provider.test.ts` | Create | Provider tests |
| `apps/server/tests/unit/providers/provider-factory.test.ts` | Create | Factory tests |
| `libs/types/tests/cursor-types.test.ts` | Create | Type tests |
| `apps/server/tests/integration/cursor-integration.test.ts` | Create | Integration tests |
| `apps/ui/tests/e2e/cursor-setup.spec.ts` | Create | E2E tests |
---
## Notes
- Integration tests may be skipped if cursor-agent is not installed
- E2E tests should work regardless of cursor-agent installation
- Manual testing should cover both installed and not-installed scenarios

View File

@@ -1,850 +0,0 @@
# Phase 2: Cursor Provider Implementation
**Status:** `completed`
**Dependencies:** Phase 1 (Types)
**Estimated Effort:** Medium-Large (core implementation)
---
## Objective
Implement the main `CursorProvider` class that spawns the cursor-agent CLI and streams responses in the AutoMaker provider format.
---
## Tasks
### Task 2.1: Create Cursor Provider
**Status:** `completed`
**File:** `apps/server/src/providers/cursor-provider.ts`
```typescript
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { BaseProvider } from './base-provider';
import {
ProviderConfig,
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
} from './types';
import {
CursorModelId,
CursorStreamEvent,
CursorSystemEvent,
CursorAssistantEvent,
CursorToolCallEvent,
CursorResultEvent,
CURSOR_MODEL_MAP,
CursorAuthStatus,
} from '@automaker/types';
import { createLogger, isAbortError } from '@automaker/utils';
import { spawnJSONLProcess, type SubprocessOptions } from '@automaker/platform';
// Create logger for this module
const logger = createLogger('CursorProvider');
/**
* Cursor-specific error codes for detailed error handling
*/
export enum CursorErrorCode {
NOT_INSTALLED = 'CURSOR_NOT_INSTALLED',
NOT_AUTHENTICATED = 'CURSOR_NOT_AUTHENTICATED',
RATE_LIMITED = 'CURSOR_RATE_LIMITED',
MODEL_UNAVAILABLE = 'CURSOR_MODEL_UNAVAILABLE',
NETWORK_ERROR = 'CURSOR_NETWORK_ERROR',
PROCESS_CRASHED = 'CURSOR_PROCESS_CRASHED',
TIMEOUT = 'CURSOR_TIMEOUT',
UNKNOWN = 'CURSOR_UNKNOWN_ERROR',
}
export interface CursorError extends Error {
code: CursorErrorCode;
recoverable: boolean;
suggestion?: string;
}
/**
* CursorProvider - Integrates cursor-agent CLI as an AI provider
*
* Uses the cursor-agent CLI with --output-format stream-json for streaming responses.
* Normalizes Cursor events to the AutoMaker ProviderMessage format.
*/
export class CursorProvider extends BaseProvider {
private static CLI_NAME = 'cursor-agent';
/**
* Installation paths based on official cursor-agent install script:
*
* Linux/macOS:
* - Binary: ~/.local/share/cursor-agent/versions/<version>/cursor-agent
* - Symlink: ~/.local/bin/cursor-agent -> versions/<version>/cursor-agent
*
* The install script creates versioned folders like:
* ~/.local/share/cursor-agent/versions/2025.12.17-996666f/cursor-agent
* And symlinks to ~/.local/bin/cursor-agent
*/
private static COMMON_PATHS: Record<string, string[]> = {
linux: [
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
'/usr/local/bin/cursor-agent',
],
darwin: [
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
'/usr/local/bin/cursor-agent',
],
win32: [
path.join(os.homedir(), 'AppData/Local/Programs/cursor-agent/cursor-agent.exe'),
path.join(os.homedir(), '.local/bin/cursor-agent.exe'),
'C:\\Program Files\\cursor-agent\\cursor-agent.exe',
],
};
// Version data directory where cursor-agent stores versions
private static VERSIONS_DIR = path.join(os.homedir(), '.local/share/cursor-agent/versions');
private cliPath: string | null = null;
private currentProcess: ChildProcess | null = null;
constructor(config: ProviderConfig = {}) {
super(config);
this.cliPath = config.cliPath || this.findCliPath();
}
getName(): string {
return 'cursor';
}
/**
* Find cursor-agent CLI in PATH or common installation locations
*/
private findCliPath(): string | null {
// Try 'which' / 'where' first
try {
const cmd = process.platform === 'win32' ? 'where cursor-agent' : 'which cursor-agent';
const result = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
if (result && fs.existsSync(result)) {
return result;
}
} catch {
// Not in PATH
}
// Check common installation paths for current platform
const platform = process.platform as 'linux' | 'darwin' | 'win32';
const platformPaths = CursorProvider.COMMON_PATHS[platform] || [];
for (const p of platformPaths) {
if (fs.existsSync(p)) {
return p;
}
}
// Also check versions directory for any installed version
if (fs.existsSync(CursorProvider.VERSIONS_DIR)) {
try {
const versions = fs
.readdirSync(CursorProvider.VERSIONS_DIR)
.filter((v) => !v.startsWith('.'))
.sort()
.reverse(); // Most recent first
for (const version of versions) {
const binaryName = platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent';
const versionPath = path.join(CursorProvider.VERSIONS_DIR, version, binaryName);
if (fs.existsSync(versionPath)) {
return versionPath;
}
}
} catch {
// Ignore directory read errors
}
}
return null;
}
/**
* Check if Cursor CLI is installed
*/
async isInstalled(): Promise<boolean> {
return this.cliPath !== null;
}
/**
* Get Cursor CLI version
*/
async getVersion(): Promise<string | null> {
if (!this.cliPath) return null;
try {
const result = execSync(`"${this.cliPath}" --version`, {
encoding: 'utf8',
timeout: 5000,
}).trim();
return result;
} catch {
return null;
}
}
/**
* Check authentication status
*/
async checkAuth(): Promise<CursorAuthStatus> {
if (!this.cliPath) {
return { authenticated: false, method: 'none' };
}
// Check for API key in environment
if (process.env.CURSOR_API_KEY) {
return { authenticated: true, method: 'api_key' };
}
// Check for credentials file (location may vary)
const credentialPaths = [
path.join(os.homedir(), '.cursor', 'credentials.json'),
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
];
for (const credPath of credentialPaths) {
if (fs.existsSync(credPath)) {
try {
const content = fs.readFileSync(credPath, 'utf8');
const creds = JSON.parse(content);
if (creds.accessToken || creds.token) {
return { authenticated: true, method: 'login', hasCredentialsFile: true };
}
} catch {
// Invalid credentials file
}
}
}
// Try running a simple command to check auth
try {
execSync(`"${this.cliPath}" --version`, {
encoding: 'utf8',
timeout: 10000,
env: { ...process.env },
});
// If we get here without error, assume authenticated
// (actual auth check would need a real API call)
return { authenticated: true, method: 'login' };
} catch (error: any) {
if (error.stderr?.includes('not authenticated') || error.stderr?.includes('log in')) {
return { authenticated: false, method: 'none' };
}
}
return { authenticated: false, method: 'none' };
}
/**
* Detect installation status (required by BaseProvider)
*/
async detectInstallation(): Promise<InstallationStatus> {
const installed = await this.isInstalled();
const version = installed ? await this.getVersion() : undefined;
const auth = await this.checkAuth();
return {
installed,
version: version || undefined,
path: this.cliPath || undefined,
method: 'cli',
hasApiKey: !!process.env.CURSOR_API_KEY,
authenticated: auth.authenticated,
};
}
/**
* Get available Cursor models
*/
getAvailableModels(): ModelDefinition[] {
return Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ({
id: `cursor-${id}`,
name: config.label,
modelString: id,
provider: 'cursor',
description: config.description,
tier: config.tier === 'pro' ? 'premium' : 'basic',
supportsTools: true,
supportsVision: false, // Cursor CLI may not support vision
}));
}
/**
* Create a CursorError with details
*/
private createError(
code: CursorErrorCode,
message: string,
recoverable: boolean = false,
suggestion?: string
): CursorError {
const error = new Error(message) as CursorError;
error.code = code;
error.recoverable = recoverable;
error.suggestion = suggestion;
error.name = 'CursorError';
return error;
}
/**
* Map stderr/exit codes to detailed CursorError
*/
private mapError(stderr: string, exitCode: number | null): CursorError {
const lower = stderr.toLowerCase();
if (
lower.includes('not authenticated') ||
lower.includes('please log in') ||
lower.includes('unauthorized')
) {
return this.createError(
CursorErrorCode.NOT_AUTHENTICATED,
'Cursor CLI is not authenticated',
true,
'Run "cursor-agent login" to authenticate with your browser'
);
}
if (
lower.includes('rate limit') ||
lower.includes('too many requests') ||
lower.includes('429')
) {
return this.createError(
CursorErrorCode.RATE_LIMITED,
'Cursor API rate limit exceeded',
true,
'Wait a few minutes and try again, or upgrade to Cursor Pro'
);
}
if (
lower.includes('model not available') ||
lower.includes('invalid model') ||
lower.includes('unknown model')
) {
return this.createError(
CursorErrorCode.MODEL_UNAVAILABLE,
'Requested model is not available',
true,
'Try using "auto" mode or select a different model'
);
}
if (
lower.includes('network') ||
lower.includes('connection') ||
lower.includes('econnrefused') ||
lower.includes('timeout')
) {
return this.createError(
CursorErrorCode.NETWORK_ERROR,
'Network connection error',
true,
'Check your internet connection and try again'
);
}
if (exitCode === 137 || lower.includes('killed') || lower.includes('sigterm')) {
return this.createError(
CursorErrorCode.PROCESS_CRASHED,
'Cursor agent process was terminated',
true,
'The process may have run out of memory. Try a simpler task.'
);
}
return this.createError(
CursorErrorCode.UNKNOWN,
stderr || `Cursor agent exited with code ${exitCode}`,
false
);
}
/**
* Parse a line of stream-json output
*/
private parseStreamLine(line: string): CursorStreamEvent | null {
if (!line.trim()) return null;
try {
return JSON.parse(line) as CursorStreamEvent;
} catch {
logger.debug('[CursorProvider] Failed to parse stream line:', line);
return null;
}
}
/**
* Convert Cursor event to AutoMaker ProviderMessage format
*/
private normalizeEvent(event: CursorStreamEvent): ProviderMessage | null {
switch (event.type) {
case 'system':
// System init - we capture session_id but don't yield a message
return null;
case 'user':
// User message - already handled by caller
return null;
case 'assistant': {
const assistantEvent = event as CursorAssistantEvent;
return {
type: 'assistant',
session_id: assistantEvent.session_id,
message: {
role: 'assistant',
content: assistantEvent.message.content.map((c) => ({
type: 'text' as const,
text: c.text,
})),
},
};
}
case 'tool_call': {
const toolEvent = event as CursorToolCallEvent;
const toolCall = toolEvent.tool_call;
// Determine tool name and input
let toolName: string;
let toolInput: unknown;
if (toolCall.readToolCall) {
toolName = 'Read';
toolInput = { file_path: toolCall.readToolCall.args.path };
} else if (toolCall.writeToolCall) {
toolName = 'Write';
toolInput = {
file_path: toolCall.writeToolCall.args.path,
content: toolCall.writeToolCall.args.fileText,
};
} else if (toolCall.function) {
toolName = toolCall.function.name;
try {
toolInput = JSON.parse(toolCall.function.arguments || '{}');
} catch {
toolInput = { raw: toolCall.function.arguments };
}
} else {
return null;
}
// For started events, emit tool_use
if (toolEvent.subtype === 'started') {
return {
type: 'assistant',
session_id: toolEvent.session_id,
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
name: toolName,
tool_use_id: toolEvent.call_id,
input: toolInput,
},
],
},
};
}
// For completed events, emit tool_result
if (toolEvent.subtype === 'completed') {
let resultContent = '';
if (toolCall.readToolCall?.result?.success) {
resultContent = toolCall.readToolCall.result.success.content;
} else if (toolCall.writeToolCall?.result?.success) {
resultContent = `Wrote ${toolCall.writeToolCall.result.success.linesCreated} lines to ${toolCall.writeToolCall.result.success.path}`;
}
return {
type: 'assistant',
session_id: toolEvent.session_id,
message: {
role: 'assistant',
content: [
{
type: 'tool_result',
tool_use_id: toolEvent.call_id,
content: resultContent,
},
],
},
};
}
return null;
}
case 'result': {
const resultEvent = event as CursorResultEvent;
if (resultEvent.is_error) {
return {
type: 'error',
session_id: resultEvent.session_id,
error: resultEvent.error || resultEvent.result || 'Unknown error',
};
}
return {
type: 'result',
subtype: 'success',
session_id: resultEvent.session_id,
result: resultEvent.result,
};
}
default:
return null;
}
}
/**
* Execute a prompt using Cursor CLI with streaming
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
if (!this.cliPath) {
throw this.createError(
CursorErrorCode.NOT_INSTALLED,
'Cursor CLI is not installed',
true,
'Install with: curl https://cursor.com/install -fsS | bash'
);
}
// Extract model from options (strip 'cursor-' prefix if present)
let model = options.model || 'auto';
if (model.startsWith('cursor-')) {
model = model.substring(7);
}
const cwd = options.cwd || process.cwd();
// Build prompt content
let promptText: string;
if (typeof options.prompt === 'string') {
promptText = options.prompt;
} else if (Array.isArray(options.prompt)) {
promptText = options.prompt
.filter((p) => p.type === 'text' && p.text)
.map((p) => p.text)
.join('\n');
} else {
throw new Error('Invalid prompt format');
}
// Build CLI arguments
const args: string[] = [
'-p', // Print mode (non-interactive)
'--force', // Allow file modifications
'--output-format',
'stream-json',
'--stream-partial-output', // Real-time streaming
];
// Add model if not auto
if (model !== 'auto') {
args.push('--model', model);
}
// Add the prompt
args.push(promptText);
logger.debug(`[CursorProvider] Executing: ${this.cliPath} ${args.slice(0, 6).join(' ')}...`);
// Use spawnJSONLProcess from @automaker/platform for JSONL streaming
// This handles line buffering, timeouts, and abort signals automatically
const subprocessOptions: SubprocessOptions = {
command: this.cliPath,
args,
cwd,
env: { ...process.env },
abortController: options.abortController,
timeout: 120000, // 2 min timeout for CLI operations (may take longer than default 30s)
};
let sessionId: string | undefined;
try {
// spawnJSONLProcess yields parsed JSON objects, handles errors
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
const event = rawEvent as CursorStreamEvent;
// Capture session ID from system init
if (event.type === 'system' && (event as CursorSystemEvent).subtype === 'init') {
sessionId = event.session_id;
}
// Normalize and yield the event
const normalized = this.normalizeEvent(event);
if (normalized) {
// Ensure session_id is always set
if (!normalized.session_id && sessionId) {
normalized.session_id = sessionId;
}
yield normalized;
}
}
} catch (error) {
// Use isAbortError from @automaker/utils for abort detection
if (isAbortError(error)) {
return; // Clean abort, don't throw
}
// Map CLI errors to CursorError
if (error instanceof Error && 'stderr' in error) {
throw this.mapError((error as any).stderr || error.message, (error as any).exitCode);
}
throw error;
}
}
/**
* Abort the current execution
*/
abort(): void {
if (this.currentProcess) {
this.currentProcess.kill('SIGTERM');
this.currentProcess = null;
}
}
/**
* Check if a feature is supported
*/
supportsFeature(feature: string): boolean {
const supported = ['tools', 'text', 'streaming'];
return supported.includes(feature);
}
}
```
### Task 2.2: Create Cursor Config Manager
**Status:** `completed`
**File:** `apps/server/src/providers/cursor-config-manager.ts`
```typescript
import * as path from 'path';
import { CursorCliConfig, CursorModelId } from '@automaker/types';
import { createLogger, mkdirSafe, existsSafe } from '@automaker/utils';
import { getAutomakerDir } from '@automaker/platform';
import { secureFs } from '@automaker/platform';
// Create logger for this module
const logger = createLogger('CursorConfigManager');
/**
* Manages Cursor CLI configuration
* Config location: .automaker/cursor-config.json
*/
export class CursorConfigManager {
private configPath: string;
private config: CursorCliConfig;
constructor(projectPath: string) {
// Use getAutomakerDir for consistent path resolution
this.configPath = path.join(getAutomakerDir(projectPath), 'cursor-config.json');
this.config = this.loadConfig();
}
private loadConfig(): CursorCliConfig {
try {
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, 'utf8');
return JSON.parse(content);
}
} catch (error) {
logger.warn('[CursorConfigManager] Failed to load config:', error);
}
// Return default config
return {
defaultModel: 'auto',
models: ['auto', 'claude-sonnet-4', 'gpt-4o-mini'],
};
}
private saveConfig(): void {
try {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
logger.debug('[CursorConfigManager] Config saved');
} catch (error) {
logger.error('[CursorConfigManager] Failed to save config:', error);
throw error;
}
}
getConfig(): CursorCliConfig {
return { ...this.config };
}
getDefaultModel(): CursorModelId {
return this.config.defaultModel || 'auto';
}
setDefaultModel(model: CursorModelId): void {
this.config.defaultModel = model;
this.saveConfig();
}
getEnabledModels(): CursorModelId[] {
return this.config.models || ['auto'];
}
setEnabledModels(models: CursorModelId[]): void {
this.config.models = models;
this.saveConfig();
}
addModel(model: CursorModelId): void {
if (!this.config.models) {
this.config.models = [];
}
if (!this.config.models.includes(model)) {
this.config.models.push(model);
this.saveConfig();
}
}
removeModel(model: CursorModelId): void {
if (this.config.models) {
this.config.models = this.config.models.filter((m) => m !== model);
this.saveConfig();
}
}
}
```
---
## Verification
### Test 1: Provider Instantiation
```typescript
// test-cursor-provider.ts
import { CursorProvider } from './apps/server/src/providers/cursor-provider';
const provider = new CursorProvider();
console.log('Provider name:', provider.getName()); // Should be 'cursor'
const status = await provider.detectInstallation();
console.log('Installation status:', status);
const models = provider.getAvailableModels();
console.log('Available models:', models.length);
```
### Test 2: CLI Detection (requires cursor-agent installed)
```bash
# Check if cursor-agent is found
node -e "
const { CursorProvider } = require('./apps/server/dist/providers/cursor-provider');
const p = new CursorProvider();
p.isInstalled().then(installed => {
console.log('Installed:', installed);
if (installed) {
p.getVersion().then(v => console.log('Version:', v));
p.checkAuth().then(a => console.log('Auth:', a));
}
});
"
```
### Test 3: Simple Query (requires cursor-agent authenticated)
```typescript
// test-cursor-query.ts
import { CursorProvider } from './apps/server/src/providers/cursor-provider';
const provider = new CursorProvider();
const stream = provider.executeQuery({
prompt: 'What is 2 + 2? Reply with just the number.',
model: 'auto',
cwd: process.cwd(),
});
for await (const msg of stream) {
console.log('Message:', JSON.stringify(msg, null, 2));
}
```
### Test 4: Error Handling
```typescript
// Test with invalid model
try {
const stream = provider.executeQuery({
prompt: 'test',
model: 'invalid-model-xyz',
cwd: process.cwd(),
});
for await (const msg of stream) {
// Should not reach here
}
} catch (error) {
console.log('Error code:', error.code);
console.log('Suggestion:', error.suggestion);
}
```
---
## Verification Checklist
Before marking this phase complete:
- [ ] `cursor-provider.ts` compiles without errors
- [ ] `cursor-config-manager.ts` compiles without errors
- [ ] Provider returns correct name ('cursor')
- [ ] `detectInstallation()` correctly detects CLI
- [ ] `getAvailableModels()` returns model definitions
- [ ] `executeQuery()` streams messages (if CLI installed)
- [ ] Errors are properly mapped to CursorError
- [ ] Abort signal terminates process
---
## Files Changed
| File | Action | Description |
| ---------------------------------------------------- | ------ | ----------------- |
| `apps/server/src/providers/cursor-provider.ts` | Create | Main provider |
| `apps/server/src/providers/cursor-config-manager.ts` | Create | Config management |
---
## Known Limitations
1. **Windows Support**: CLI path detection may need adjustment
2. **Vision**: Cursor CLI may not support image inputs
3. **Resume**: Session resumption not implemented in Phase 2
---
## Notes
- The provider uses `--stream-partial-output` for real-time character streaming
- Tool call events are normalized to match Claude SDK format
- Session IDs are captured from system init event

View File

@@ -1,229 +0,0 @@
# Phase 3: Provider Factory Integration
**Status:** `completed`
**Dependencies:** Phase 2 (Provider)
**Estimated Effort:** Small (routing logic only)
---
## Objective
Integrate CursorProvider into the ProviderFactory so models are automatically routed to the correct provider.
---
## Tasks
### Task 3.1: Update Provider Factory
**Status:** `completed`
**File:** `apps/server/src/providers/provider-factory.ts`
Add Cursor provider import and routing:
```typescript
import { CursorProvider } from './cursor-provider';
import { CURSOR_MODEL_MAP } from '@automaker/types';
export class ProviderFactory {
/**
* Determine which provider to use for a given model
*/
static getProviderNameForModel(model: string): 'claude' | 'cursor' {
const lowerModel = model.toLowerCase();
// Check for explicit cursor prefix
if (lowerModel.startsWith('cursor-')) {
return 'cursor';
}
// Check if it's a known Cursor model ID
const cursorModelId = lowerModel.replace('cursor-', '');
if (cursorModelId in CURSOR_MODEL_MAP) {
return 'cursor';
}
// Check for Cursor-specific patterns
if (
lowerModel === 'auto' ||
lowerModel.includes('gpt-') ||
lowerModel.includes('gemini-') ||
lowerModel === 'o3-mini'
) {
// These could be Cursor models, but we default to Claude
// unless explicitly prefixed with cursor-
}
// Check for Claude model patterns
if (
lowerModel.startsWith('claude-') ||
['opus', 'sonnet', 'haiku'].some((n) => lowerModel.includes(n))
) {
return 'claude';
}
// Default to Claude
return 'claude';
}
/**
* Get a provider instance for the given model
*/
static getProviderForModel(model: string, config?: ProviderConfig): BaseProvider {
const providerName = this.getProviderNameForModel(model);
if (providerName === 'cursor') {
return new CursorProvider(config);
}
return new ClaudeProvider(config);
}
/**
* Get all registered providers
*/
static getAllProviders(): BaseProvider[] {
return [new ClaudeProvider(), new CursorProvider()];
}
/**
* Get a provider by name
*/
static getProviderByName(name: string): BaseProvider | null {
const lowerName = name.toLowerCase();
switch (lowerName) {
case 'claude':
return new ClaudeProvider();
case 'cursor':
return new CursorProvider();
default:
return null;
}
}
/**
* Check installation status of all providers
*/
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
const providers = this.getAllProviders();
const statuses: Record<string, InstallationStatus> = {};
await Promise.all(
providers.map(async (provider) => {
const status = await provider.detectInstallation();
statuses[provider.getName()] = status;
})
);
return statuses;
}
/**
* Get all available models from all providers
*/
static getAllAvailableModels(): ModelDefinition[] {
const providers = this.getAllProviders();
return providers.flatMap((p) => p.getAvailableModels());
}
}
```
### Task 3.2: Export CursorProvider
**Status:** `completed`
**File:** `apps/server/src/providers/index.ts`
Add export:
```typescript
export { CursorProvider, CursorErrorCode, CursorError } from './cursor-provider';
export { CursorConfigManager } from './cursor-config-manager';
```
---
## Verification
### Test 1: Model Routing
```typescript
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
// Cursor models
console.assert(ProviderFactory.getProviderNameForModel('cursor-auto') === 'cursor');
console.assert(ProviderFactory.getProviderNameForModel('cursor-gpt-4o') === 'cursor');
console.assert(ProviderFactory.getProviderNameForModel('cursor-claude-sonnet-4') === 'cursor');
// Claude models (default)
console.assert(ProviderFactory.getProviderNameForModel('claude-sonnet-4') === 'claude');
console.assert(ProviderFactory.getProviderNameForModel('opus') === 'claude');
console.assert(ProviderFactory.getProviderNameForModel('sonnet') === 'claude');
console.assert(ProviderFactory.getProviderNameForModel('haiku') === 'claude');
// Unknown models default to Claude
console.assert(ProviderFactory.getProviderNameForModel('unknown-model') === 'claude');
console.log('All routing tests passed!');
```
### Test 2: Provider Instantiation
```typescript
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
const cursorProvider = ProviderFactory.getProviderForModel('cursor-auto');
console.assert(cursorProvider.getName() === 'cursor');
const claudeProvider = ProviderFactory.getProviderForModel('sonnet');
console.assert(claudeProvider.getName() === 'claude');
console.log('Provider instantiation tests passed!');
```
### Test 3: All Providers Check
```typescript
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
const statuses = await ProviderFactory.checkAllProviders();
console.log('Provider statuses:', statuses);
// Should have both 'claude' and 'cursor' keys
const allModels = ProviderFactory.getAllAvailableModels();
console.log('Total models:', allModels.length);
// Should include models from both providers
```
---
## Verification Checklist
Before marking this phase complete:
- [x] ProviderFactory routes `cursor-*` models to CursorProvider
- [x] ProviderFactory routes Claude models to ClaudeProvider
- [x] `getAllProviders()` returns both providers
- [x] `getProviderByName('cursor')` returns CursorProvider
- [x] `checkAllProviders()` returns status for both providers
- [x] `getAllAvailableModels()` includes Cursor models
- [x] Existing Claude routing not broken
---
## Files Changed
| File | Action | Description |
| ----------------------------------------------- | ------ | --------------------- |
| `apps/server/src/providers/provider-factory.ts` | Modify | Add Cursor routing |
| `apps/server/src/providers/index.ts` | Modify | Export CursorProvider |
---
## Notes
- Model routing uses prefix matching for explicit `cursor-` models
- Unknown models default to Claude for backward compatibility
- The factory is stateless - new provider instances created per call

View File

@@ -1,348 +0,0 @@
# Phase 4: Setup Routes & Status Endpoints
**Status:** `completed`
**Dependencies:** Phase 3 (Factory)
**Estimated Effort:** Medium (API endpoints)
---
## Objective
Create API endpoints for checking Cursor CLI status and managing configuration.
---
## Tasks
### Task 4.1: Create Cursor Status Route
**Status:** `completed`
**File:** `apps/server/src/routes/setup/routes/cursor-status.ts`
```typescript
import { Router, Request, Response } from 'express';
import { CursorProvider } from '../../../providers/cursor-provider';
import { createLogger } from '@automaker/utils';
// Create logger for this module
const logger = createLogger('CursorStatusRoute');
/**
* GET /api/setup/cursor-status
* Returns Cursor CLI installation and authentication status
*/
export function createCursorStatusHandler() {
return async (req: Request, res: Response) => {
try {
const provider = new CursorProvider();
const [installed, version, auth] = await Promise.all([
provider.isInstalled(),
provider.getVersion(),
provider.checkAuth(),
]);
res.json({
success: true,
installed,
version: version || null,
path: installed ? (provider as any).cliPath : null,
auth: {
authenticated: auth.authenticated,
method: auth.method,
},
installCommand: 'curl https://cursor.com/install -fsS | bash',
loginCommand: 'cursor-agent login',
});
} catch (error) {
logger.error('[cursor-status] Error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};
}
export function createCursorStatusRoute(): Router {
const router = Router();
router.get('/cursor-status', createCursorStatusHandler());
return router;
}
```
### Task 4.2: Create Cursor Config Routes
**Status:** `completed`
**File:** `apps/server/src/routes/setup/routes/cursor-config.ts`
```typescript
import { Router, Request, Response } from 'express';
import { CursorConfigManager } from '../../../providers/cursor-config-manager';
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
import { createLogger } from '@automaker/utils';
// Create logger for this module
const logger = createLogger('CursorConfigRoute');
export function createCursorConfigRoutes(dataDir: string): Router {
const router = Router();
const configManager = new CursorConfigManager(dataDir);
/**
* GET /api/setup/cursor-config
* Get current Cursor configuration
*/
router.get('/cursor-config', (req: Request, res: Response) => {
try {
res.json({
success: true,
config: configManager.getConfig(),
availableModels: Object.values(CURSOR_MODEL_MAP),
});
} catch (error) {
logger.error('[cursor-config] GET error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
/**
* POST /api/setup/cursor-config/default-model
* Set the default Cursor model
*/
router.post('/cursor-config/default-model', (req: Request, res: Response) => {
try {
const { model } = req.body;
if (!model || !(model in CURSOR_MODEL_MAP)) {
res.status(400).json({
success: false,
error: `Invalid model ID. Valid models: ${Object.keys(CURSOR_MODEL_MAP).join(', ')}`,
});
return;
}
configManager.setDefaultModel(model as CursorModelId);
res.json({ success: true, model });
} catch (error) {
logger.error('[cursor-config] POST default-model error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
/**
* POST /api/setup/cursor-config/models
* Set enabled Cursor models
*/
router.post('/cursor-config/models', (req: Request, res: Response) => {
try {
const { models } = req.body;
if (!Array.isArray(models)) {
res.status(400).json({
success: false,
error: 'Models must be an array',
});
return;
}
// Filter to valid models only
const validModels = models.filter((m): m is CursorModelId => m in CURSOR_MODEL_MAP);
if (validModels.length === 0) {
res.status(400).json({
success: false,
error: 'No valid models provided',
});
return;
}
configManager.setEnabledModels(validModels);
res.json({ success: true, models: validModels });
} catch (error) {
logger.error('[cursor-config] POST models error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
return router;
}
```
### Task 4.3: Register Routes in Setup Index
**Status:** `completed`
**File:** `apps/server/src/routes/setup/index.ts`
Add to existing router:
```typescript
import { createCursorStatusRoute } from './routes/cursor-status';
import { createCursorConfigRoutes } from './routes/cursor-config';
// In the router setup function:
export function createSetupRouter(dataDir: string): Router {
const router = Router();
// Existing routes...
router.get('/claude-status', createClaudeStatusHandler());
// ...
// Add Cursor routes
router.use(createCursorStatusRoute());
router.use(createCursorConfigRoutes(dataDir));
return router;
}
```
### Task 4.4: Update HttpApiClient
**Status:** `completed`
**File:** `apps/ui/src/lib/http-api-client.ts`
Add Cursor methods to the HttpApiClient setup object:
```typescript
// In HttpApiClient class, extend the setup object:
setup = {
// Existing methods...
getClaudeStatus: () => this.get('/api/setup/claude-status'),
// Add Cursor methods
getCursorStatus: () =>
this.get<{
success: boolean;
installed?: boolean;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
};
installCommand?: string;
loginCommand?: string;
error?: string;
}>('/api/setup/cursor-status'),
getCursorConfig: () =>
this.get<{
success: boolean;
config?: CursorCliConfig;
availableModels?: CursorModelConfig[];
error?: string;
}>('/api/setup/cursor-config'),
setCursorDefaultModel: (model: CursorModelId) =>
this.post<{ success: boolean; error?: string }>('/api/setup/cursor-config/default-model', {
model,
}),
setCursorModels: (models: CursorModelId[]) =>
this.post<{ success: boolean; error?: string }>('/api/setup/cursor-config/models', { models }),
};
```
This integrates with the existing HttpApiClient pattern used throughout the UI.
---
## Verification
### Test 1: Status Endpoint
```bash
# Start the server, then:
curl http://localhost:3001/api/setup/cursor-status
# Expected response (if installed):
# {
# "success": true,
# "installed": true,
# "version": "0.1.0",
# "path": "/home/user/.local/bin/cursor-agent",
# "auth": { "authenticated": true, "method": "login" }
# }
# Expected response (if not installed):
# {
# "success": true,
# "installed": false,
# "installCommand": "curl https://cursor.com/install -fsS | bash"
# }
```
### Test 2: Config Endpoints
```bash
# Get config
curl http://localhost:3001/api/setup/cursor-config
# Set default model
curl -X POST http://localhost:3001/api/setup/cursor-config/default-model \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o"}'
# Set enabled models
curl -X POST http://localhost:3001/api/setup/cursor-config/models \
-H "Content-Type: application/json" \
-d '{"models": ["auto", "gpt-4o", "claude-sonnet-4"]}'
```
### Test 3: Error Handling
```bash
# Invalid model should return 400
curl -X POST http://localhost:3001/api/setup/cursor-config/default-model \
-H "Content-Type: application/json" \
-d '{"model": "invalid-model"}'
# Expected: {"success": false, "error": "Invalid model ID..."}
```
---
## Verification Checklist
Before marking this phase complete:
- [x] `/api/setup/cursor-status` returns installation status
- [x] `/api/setup/cursor-config` returns current config
- [x] `/api/setup/cursor-config/default-model` updates default
- [x] `/api/setup/cursor-config/models` updates enabled models
- [x] Error responses have correct status codes (400, 500)
- [x] Config persists to file after changes
- [x] HttpApiClient updated with Cursor methods (using web mode, not Electron IPC)
---
## Files Changed
| File | Action | Description |
| ------------------------------------------------------ | ------ | ----------------------------- |
| `apps/server/src/routes/setup/routes/cursor-status.ts` | Create | Status endpoint |
| `apps/server/src/routes/setup/routes/cursor-config.ts` | Create | Config endpoints |
| `apps/server/src/routes/setup/index.ts` | Modify | Register routes |
| `apps/ui/src/lib/http-api-client.ts` | Modify | Add Cursor API client methods |
---
## Notes
- Config is stored in `.automaker/cursor-config.json`
- The status endpoint is optimized for quick checks (parallel calls)
- Install/login commands are included in response for UI display

View File

@@ -1,374 +0,0 @@
# Phase 5: Log Parser Integration
**Status:** `completed`
**Dependencies:** Phase 2 (Provider), Phase 3 (Factory)
**Estimated Effort:** Small (parser extension)
---
## Objective
Update the log parser to recognize and normalize Cursor CLI stream events for display in the log viewer.
---
## Tasks
### Task 5.1: Add Cursor Event Type Detection
**Status:** `completed`
**File:** `apps/ui/src/lib/log-parser.ts`
Add Cursor event detection and normalization:
```typescript
import {
CursorStreamEvent,
CursorSystemEvent,
CursorAssistantEvent,
CursorToolCallEvent,
CursorResultEvent,
} from '@automaker/types';
/**
* Detect if a parsed JSON object is a Cursor stream event
*/
function isCursorEvent(obj: any): obj is CursorStreamEvent {
return (
obj &&
typeof obj === 'object' &&
'type' in obj &&
'session_id' in obj &&
['system', 'user', 'assistant', 'tool_call', 'result'].includes(obj.type)
);
}
/**
* Normalize Cursor stream event to log entry
*/
export function normalizeCursorEvent(event: CursorStreamEvent): LogEntry | null {
const timestamp = new Date().toISOString();
const baseEntry = {
id: `cursor-${event.session_id}-${Date.now()}`,
timestamp,
};
switch (event.type) {
case 'system': {
const sysEvent = event as CursorSystemEvent;
return {
...baseEntry,
type: 'info' as LogEntryType,
title: 'Session Started',
content: `Model: ${sysEvent.model}\nAuth: ${sysEvent.apiKeySource}\nCWD: ${sysEvent.cwd}`,
collapsed: true,
metadata: {
phase: 'init',
},
};
}
case 'assistant': {
const assistEvent = event as CursorAssistantEvent;
const text = assistEvent.message.content
.filter((c) => c.type === 'text')
.map((c) => c.text)
.join('');
if (!text.trim()) return null;
return {
...baseEntry,
type: 'info' as LogEntryType,
title: 'Assistant',
content: text,
collapsed: false,
};
}
case 'tool_call': {
const toolEvent = event as CursorToolCallEvent;
return normalizeCursorToolCall(toolEvent, baseEntry);
}
case 'result': {
const resultEvent = event as CursorResultEvent;
if (resultEvent.is_error) {
return {
...baseEntry,
type: 'error' as LogEntryType,
title: 'Error',
content: resultEvent.error || resultEvent.result || 'Unknown error',
collapsed: false,
};
}
return {
...baseEntry,
type: 'success' as LogEntryType,
title: 'Completed',
content: `Duration: ${resultEvent.duration_ms}ms`,
collapsed: true,
};
}
default:
return null;
}
}
/**
* Normalize Cursor tool call event
*/
function normalizeCursorToolCall(
event: CursorToolCallEvent,
baseEntry: { id: string; timestamp: string }
): LogEntry | null {
const toolCall = event.tool_call;
const isStarted = event.subtype === 'started';
const isCompleted = event.subtype === 'completed';
// Read tool
if (toolCall.readToolCall) {
const path = toolCall.readToolCall.args.path;
const result = toolCall.readToolCall.result?.success;
return {
...baseEntry,
id: `${baseEntry.id}-${event.call_id}`,
type: 'tool_call' as LogEntryType,
title: isStarted ? `Reading ${path}` : `Read ${path}`,
content:
isCompleted && result
? `${result.totalLines} lines, ${result.totalChars} chars`
: `Path: ${path}`,
collapsed: true,
metadata: {
toolName: 'Read',
toolCategory: 'read' as ToolCategory,
filePath: path,
summary: isCompleted ? `Read ${result?.totalLines || 0} lines` : `Reading file...`,
},
};
}
// Write tool
if (toolCall.writeToolCall) {
const path =
toolCall.writeToolCall.args?.path ||
toolCall.writeToolCall.result?.success?.path ||
'unknown';
const result = toolCall.writeToolCall.result?.success;
return {
...baseEntry,
id: `${baseEntry.id}-${event.call_id}`,
type: 'tool_call' as LogEntryType,
title: isStarted ? `Writing ${path}` : `Wrote ${path}`,
content:
isCompleted && result
? `${result.linesCreated} lines, ${result.fileSize} bytes`
: `Path: ${path}`,
collapsed: true,
metadata: {
toolName: 'Write',
toolCategory: 'write' as ToolCategory,
filePath: path,
summary: isCompleted ? `Wrote ${result?.linesCreated || 0} lines` : `Writing file...`,
},
};
}
// Generic function tool
if (toolCall.function) {
const name = toolCall.function.name;
const args = toolCall.function.arguments;
// Determine category based on tool name
let category: ToolCategory = 'other';
if (['Read', 'Glob'].includes(name)) category = 'read';
if (['Write', 'Edit'].includes(name)) category = 'edit';
if (['Bash'].includes(name)) category = 'bash';
if (['Grep'].includes(name)) category = 'search';
if (['TodoWrite'].includes(name)) category = 'todo';
if (['Task'].includes(name)) category = 'task';
return {
...baseEntry,
id: `${baseEntry.id}-${event.call_id}`,
type: 'tool_call' as LogEntryType,
title: `${name} ${isStarted ? 'started' : 'completed'}`,
content: args || '',
collapsed: true,
metadata: {
toolName: name,
toolCategory: category,
summary: `${name} ${event.subtype}`,
},
};
}
return null;
}
```
### Task 5.2: Update parseLogLine Function
**Status:** `completed`
**File:** `apps/ui/src/lib/log-parser.ts`
Update the main parsing function to detect Cursor events:
```typescript
/**
* Parse a single log line into a structured entry
*/
export function parseLogLine(line: string): LogEntry | null {
if (!line.trim()) return null;
try {
const parsed = JSON.parse(line);
// Check if it's a Cursor stream event
if (isCursorEvent(parsed)) {
return normalizeCursorEvent(parsed);
}
// Existing AutoMaker/Claude event parsing...
return parseAutoMakerEvent(parsed);
} catch {
// Non-JSON line - treat as plain text
return {
id: `text-${Date.now()}-${Math.random().toString(36).slice(2)}`,
type: 'info',
title: 'Output',
content: line,
timestamp: new Date().toISOString(),
collapsed: false,
};
}
}
```
### Task 5.3: Add Cursor-Specific Styling (Optional)
**Status:** `completed`
**File:** `apps/ui/src/lib/log-parser.ts`
Add provider-aware styling:
```typescript
/**
* Get provider-specific styling for log entries
*/
export function getProviderStyle(entry: LogEntry): { badge?: string; icon?: string } {
// Check if entry has Cursor session ID pattern
if (entry.id.startsWith('cursor-')) {
return {
badge: 'Cursor',
icon: 'terminal', // Or a Cursor-specific icon
};
}
// Default (Claude)
return {
badge: 'Claude',
icon: 'bot',
};
}
```
---
## Verification
### Test 1: Cursor Event Parsing
```typescript
import { parseLogLine, normalizeCursorEvent } from './apps/ui/src/lib/log-parser';
// Test system init
const systemEvent =
'{"type":"system","subtype":"init","apiKeySource":"login","cwd":"/project","session_id":"abc-123","model":"Claude 4 Sonnet","permissionMode":"default"}';
const systemEntry = parseLogLine(systemEvent);
console.assert(systemEntry?.type === 'info', 'System event should be info type');
console.assert(systemEntry?.title === 'Session Started', 'System should have correct title');
// Test assistant message
const assistantEvent =
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello world"}]},"session_id":"abc-123"}';
const assistantEntry = parseLogLine(assistantEvent);
console.assert(assistantEntry?.content === 'Hello world', 'Assistant content should match');
// Test tool call
const toolEvent =
'{"type":"tool_call","subtype":"started","call_id":"call-1","tool_call":{"readToolCall":{"args":{"path":"test.ts"}}},"session_id":"abc-123"}';
const toolEntry = parseLogLine(toolEvent);
console.assert(toolEntry?.metadata?.toolName === 'Read', 'Tool name should be Read');
console.assert(toolEntry?.metadata?.toolCategory === 'read', 'Category should be read');
console.log('All Cursor parsing tests passed!');
```
### Test 2: Mixed Event Stream
```typescript
// Simulate a stream with both Claude and Cursor events
const events = [
// Cursor events
'{"type":"system","subtype":"init","session_id":"cur-1","model":"GPT-4o","apiKeySource":"login","cwd":"/project","permissionMode":"default"}',
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Reading file..."}]},"session_id":"cur-1"}',
'{"type":"tool_call","subtype":"started","call_id":"t1","tool_call":{"readToolCall":{"args":{"path":"README.md"}}},"session_id":"cur-1"}',
// Claude-style event (existing format)
'{"type":"assistant","content":[{"type":"text","text":"From Claude"}]}',
];
const entries = events.map(parseLogLine).filter(Boolean);
console.log('Parsed entries:', entries.length);
// Should parse all events correctly
```
### Test 3: Log Viewer Integration
1. Start the app with a Cursor provider task
2. Observe log viewer updates in real-time
3. Verify:
- Tool calls show correct icons
- File paths are highlighted
- Collapsed by default where appropriate
- Timestamps are displayed
---
## Verification Checklist
Before marking this phase complete:
- [x] `isCursorEvent()` correctly identifies Cursor events
- [x] `normalizeCursorEvent()` handles all event types
- [x] Tool calls are categorized correctly
- [x] File paths extracted for Read/Write tools
- [x] Existing Claude event parsing not broken
- [x] Log viewer displays Cursor events correctly
- [x] No runtime errors with malformed events
---
## Files Changed
| File | Action | Description |
| ------------------------------- | ------ | ------------------------------ |
| `apps/ui/src/lib/log-parser.ts` | Modify | Add Cursor event normalization |
---
## Notes
- Cursor events have `session_id` on all events (unlike Claude SDK)
- Tool call events come in pairs: started + completed
- The `call_id` is used to correlate started/completed events
- Entry IDs include session_id for uniqueness

View File

@@ -1,457 +0,0 @@
# Phase 6: UI Setup Wizard
**Status:** `completed`
**Dependencies:** Phase 4 (Routes)
**Estimated Effort:** Medium (React component)
---
## Objective
Add an optional Cursor CLI setup step to the welcome wizard, allowing users to configure Cursor as an AI provider during initial setup.
---
## Tasks
### Task 6.1: Create Cursor Setup Step Component
**Status:** `completed`
**File:** `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx`
```tsx
import React, { useState, useEffect, useCallback } from 'react';
import { CheckCircle2, XCircle, Loader2, ExternalLink, Terminal, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { api } from '@/lib/http-api-client';
interface CursorSetupStepProps {
onComplete: () => void;
onSkip: () => void;
}
interface CliStatus {
installed: boolean;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
};
installCommand?: string;
loginCommand?: string;
}
export function CursorSetupStep({ onComplete, onSkip }: CursorSetupStepProps) {
const [status, setStatus] = useState<CliStatus | null>(null);
const [isChecking, setIsChecking] = useState(true);
const [isLoggingIn, setIsLoggingIn] = useState(false);
const checkStatus = useCallback(async () => {
setIsChecking(true);
try {
const result = await api.setup.getCursorStatus();
if (result.success) {
setStatus({
installed: result.installed ?? false,
version: result.version,
path: result.path,
auth: result.auth,
installCommand: result.installCommand,
loginCommand: result.loginCommand,
});
if (result.auth?.authenticated) {
toast.success('Cursor CLI is ready!');
}
} else {
toast.error('Failed to check Cursor status');
}
} catch (error) {
console.error('Failed to check Cursor status:', error);
toast.error('Failed to check Cursor CLI status');
} finally {
setIsChecking(false);
}
}, []);
useEffect(() => {
checkStatus();
}, [checkStatus]);
const handleLogin = async () => {
setIsLoggingIn(true);
try {
// Copy login command to clipboard and show instructions
if (status?.loginCommand) {
await navigator.clipboard.writeText(status.loginCommand);
toast.info('Login command copied! Paste in terminal to authenticate.');
}
// Poll for auth status
let attempts = 0;
const maxAttempts = 60; // 2 minutes with 2s interval
const pollInterval = setInterval(async () => {
attempts++;
try {
const result = await api.setup.getCursorStatus();
if (result.auth?.authenticated) {
clearInterval(pollInterval);
setStatus((prev) => (prev ? { ...prev, auth: result.auth } : null));
setIsLoggingIn(false);
toast.success('Successfully logged in to Cursor!');
}
} catch {
// Ignore polling errors
}
if (attempts >= maxAttempts) {
clearInterval(pollInterval);
setIsLoggingIn(false);
toast.error('Login timed out. Please try again.');
}
}, 2000);
} catch (error) {
console.error('Login failed:', error);
toast.error('Failed to start login process');
setIsLoggingIn(false);
}
};
const handleCopyInstallCommand = async () => {
if (status?.installCommand) {
await navigator.clipboard.writeText(status.installCommand);
toast.success('Install command copied to clipboard!');
}
};
const isComplete = status?.installed && status?.auth?.authenticated;
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="w-5 h-5" />
Cursor CLI Setup
<Badge variant="outline" className="ml-2">
Optional
</Badge>
</CardTitle>
<CardDescription>
Configure Cursor CLI as an alternative AI provider. You can skip this and use Claude
instead, or configure it later in Settings.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Installation Status */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">CLI Installation</span>
{isChecking ? (
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
) : status?.installed ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs">v{status.version}</span>
</div>
) : (
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<XCircle className="w-4 h-4" />
<span className="text-xs">Not installed</span>
</div>
)}
</div>
{!status?.installed && !isChecking && (
<Alert>
<AlertDescription className="text-sm space-y-3">
<p>Install Cursor CLI to use Cursor models:</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
{status?.installCommand || 'curl https://cursor.com/install -fsS | bash'}
</code>
<Button variant="outline" size="sm" onClick={handleCopyInstallCommand}>
Copy
</Button>
</div>
<Button
variant="link"
size="sm"
className="p-0 h-auto"
onClick={() => window.open('https://cursor.com/docs/cli', '_blank')}
>
View installation docs
<ExternalLink className="w-3 h-3 ml-1" />
</Button>
</AlertDescription>
</Alert>
)}
</div>
{/* Authentication Status */}
{status?.installed && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Authentication</span>
{status.auth?.authenticated ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs capitalize">
{status.auth.method === 'api_key' ? 'API Key' : 'Browser Login'}
</span>
</div>
) : (
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<XCircle className="w-4 h-4" />
<span className="text-xs">Not authenticated</span>
</div>
)}
</div>
{!status.auth?.authenticated && (
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
Run the login command in your terminal, then complete authentication in your
browser:
</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted p-2 rounded text-xs font-mono">
{status.loginCommand || 'cursor-agent login'}
</code>
</div>
<Button onClick={handleLogin} disabled={isLoggingIn} className="w-full">
{isLoggingIn ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Waiting for login...
</>
) : (
'Copy Command & Wait for Login'
)}
</Button>
</div>
)}
</div>
)}
{/* Action Buttons */}
<div className="flex gap-3 pt-4 border-t">
<Button variant="outline" onClick={onSkip} className="flex-1">
Skip for now
</Button>
<Button
onClick={onComplete}
disabled={!isComplete && status?.installed}
className="flex-1"
>
{isComplete ? 'Continue' : 'Complete setup to continue'}
</Button>
<Button
variant="ghost"
size="icon"
onClick={checkStatus}
disabled={isChecking}
title="Refresh status"
>
<RefreshCw className={`w-4 h-4 ${isChecking ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* Info note */}
<p className="text-xs text-muted-foreground text-center">
You can always configure Cursor later in Settings Providers
</p>
</CardContent>
</Card>
);
}
export default CursorSetupStep;
```
### Task 6.2: Update Setup View Steps
**Status:** `completed`
**File:** `apps/ui/src/components/views/setup-view.tsx`
Add the Cursor step to the wizard:
```tsx
import { CursorSetupStep } from './setup-view/steps/cursor-setup-step';
// Add to steps configuration
const SETUP_STEPS = [
// Existing steps...
{
id: 'claude',
title: 'Claude CLI',
optional: false,
component: ClaudeSetupStep,
},
// Add Cursor step
{
id: 'cursor',
title: 'Cursor CLI',
optional: true,
component: CursorSetupStep,
},
// Remaining steps...
{
id: 'project',
title: 'Project',
optional: false,
component: ProjectSetupStep,
},
];
// In the render function, handle optional steps:
function SetupView() {
const [currentStep, setCurrentStep] = useState(0);
const [skippedSteps, setSkippedSteps] = useState<Set<string>>(new Set());
const handleSkip = (stepId: string) => {
setSkippedSteps((prev) => new Set([...prev, stepId]));
setCurrentStep((prev) => prev + 1);
};
const handleComplete = () => {
setCurrentStep((prev) => prev + 1);
};
const step = SETUP_STEPS[currentStep];
const StepComponent = step.component;
return (
<div className="setup-view">
{/* Progress indicator */}
<div className="flex gap-2 mb-6">
{SETUP_STEPS.map((s, i) => (
<div
key={s.id}
className={cn(
'flex-1 h-2 rounded',
i < currentStep
? 'bg-green-500'
: i === currentStep
? 'bg-blue-500'
: skippedSteps.has(s.id)
? 'bg-gray-300'
: 'bg-gray-200'
)}
/>
))}
</div>
{/* Step title */}
<h2 className="text-xl font-semibold mb-4">
{step.title}
{step.optional && <span className="text-sm text-muted-foreground ml-2">(Optional)</span>}
</h2>
{/* Step component */}
<StepComponent onComplete={handleComplete} onSkip={() => handleSkip(step.id)} />
</div>
);
}
```
### Task 6.3: Add Step Indicator for Optional Steps
**Status:** `completed`
Add visual indicator for optional vs required steps in the progress bar.
---
## Verification
### Test 1: Component Rendering
1. Start the app with a fresh setup (or clear setup state)
2. Navigate through setup steps
3. Verify Cursor step appears after Claude step
4. Verify "Optional" badge is displayed
### Test 2: Skip Functionality
1. Click "Skip for now" on Cursor step
2. Verify step is skipped and progress continues
3. Verify skipped state is persisted (if applicable)
### Test 3: Installation Detection
1. With cursor-agent NOT installed:
- Should show "Not installed" status
- Should show install command
- Continue button should be disabled
2. With cursor-agent installed but not authenticated:
- Should show version number
- Should show "Not authenticated" status
- Should show login instructions
3. With cursor-agent installed and authenticated:
- Should show green checkmarks
- Continue button should be enabled
### Test 4: Login Flow
1. Click "Copy Command & Wait for Login"
2. Verify command is copied to clipboard
3. Run login command in terminal
4. Verify status updates after authentication
5. Verify success toast appears
### Test 5: Refresh Status
1. Click refresh button
2. Verify loading state is shown
3. Verify status is re-fetched
---
## Verification Checklist
Before marking this phase complete:
- [ ] CursorSetupStep component renders correctly
- [ ] Step appears in setup wizard flow
- [ ] Skip button works and progresses to next step
- [ ] Installation status is correctly detected
- [ ] Authentication status is correctly detected
- [ ] Login command copy works
- [ ] Polling for auth status works
- [ ] Refresh button updates status
- [ ] Error states handled gracefully
- [ ] Progress indicator shows optional step differently
---
## Files Changed
| File | Action | Description |
| --------------------------------------------------------------------- | ------ | -------------------- |
| `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx` | Create | Setup step component |
| `apps/ui/src/components/views/setup-view.tsx` | Modify | Add step to wizard |
---
## Design Notes
- The step is marked as optional with a badge
- Skip button is always available for optional steps
- The login flow is asynchronous with polling
- Status can be manually refreshed
- Error states show clear recovery instructions

View File

@@ -1,556 +0,0 @@
# Phase 7: Settings View Provider Tabs
**Status:** `completed`
**Dependencies:** Phase 4 (Routes)
**Estimated Effort:** Medium (React components)
---
## Objective
Create a tabbed interface in Settings for managing different AI providers (Claude and Cursor), with provider-specific configuration options.
---
## Tasks
### Task 7.1: Create Cursor Settings Tab Component
**Status:** `completed`
**File:** `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx`
```tsx
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Terminal, CheckCircle2, XCircle, Loader2, RefreshCw, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/http-api-client';
import {
CursorModelId,
CursorModelConfig,
CursorCliConfig,
CURSOR_MODEL_MAP,
} from '@automaker/types';
interface CursorStatus {
installed: boolean;
version?: string;
authenticated: boolean;
method?: string;
}
export function CursorSettingsTab() {
const [status, setStatus] = useState<CursorStatus | null>(null);
const [config, setConfig] = useState<CursorCliConfig | null>(null);
const [availableModels, setAvailableModels] = useState<CursorModelConfig[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const loadData = async () => {
setIsLoading(true);
try {
const [statusData, configData] = await Promise.all([
api.setup.getCursorStatus(),
api.setup.getCursorConfig(),
]);
if (statusData.success) {
setStatus({
installed: statusData.installed ?? false,
version: statusData.version,
authenticated: statusData.auth?.authenticated ?? false,
method: statusData.auth?.method,
});
}
if (configData.success) {
setConfig(configData.config);
setAvailableModels(configData.availableModels || Object.values(CURSOR_MODEL_MAP));
}
} catch (error) {
console.error('Failed to load Cursor settings:', error);
toast.error('Failed to load Cursor settings');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const handleDefaultModelChange = async (model: CursorModelId) => {
if (!config) return;
setIsSaving(true);
try {
const result = await api.setup.setCursorDefaultModel(model);
if (result.success) {
setConfig({ ...config, defaultModel: model });
toast.success('Default model updated');
} else {
toast.error(result.error || 'Failed to update default model');
}
} catch (error) {
toast.error('Failed to update default model');
} finally {
setIsSaving(false);
}
};
const handleModelToggle = async (model: CursorModelId, enabled: boolean) => {
if (!config) return;
const newModels = enabled
? [...(config.models || []), model]
: (config.models || []).filter((m) => m !== model);
setIsSaving(true);
try {
const result = await api.setup.setCursorModels(newModels);
if (result.success) {
setConfig({ ...config, models: newModels });
} else {
toast.error(result.error || 'Failed to update models');
}
} catch (error) {
toast.error('Failed to update models');
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
{/* Status Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Terminal className="w-5 h-5" />
Cursor CLI Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Installation */}
<div className="flex items-center justify-between">
<span className="text-sm">Installation</span>
{status?.installed ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs font-mono">v{status.version}</span>
</div>
) : (
<div className="flex items-center gap-2 text-destructive">
<XCircle className="w-4 h-4" />
<span className="text-xs">Not installed</span>
</div>
)}
</div>
{/* Authentication */}
<div className="flex items-center justify-between">
<span className="text-sm">Authentication</span>
{status?.authenticated ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs capitalize">
{status.method === 'api_key' ? 'API Key' : 'Browser Login'}
</span>
</div>
) : (
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<XCircle className="w-4 h-4" />
<span className="text-xs">Not authenticated</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 pt-2">
<Button variant="outline" size="sm" onClick={loadData}>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh Status
</Button>
{!status?.installed && (
<Button
variant="outline"
size="sm"
onClick={() => window.open('https://cursor.com/docs/cli', '_blank')}
>
Installation Guide
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</CardContent>
</Card>
{/* Model Configuration */}
{status?.installed && config && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Model Configuration</CardTitle>
<CardDescription>
Configure which Cursor models are available and set the default
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Default Model */}
<div className="space-y-2">
<Label>Default Model</Label>
<Select
value={config.defaultModel || 'auto'}
onValueChange={(v) => handleDefaultModelChange(v as CursorModelId)}
disabled={isSaving}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{(config.models || ['auto']).map((modelId) => {
const model = CURSOR_MODEL_MAP[modelId];
if (!model) return null;
return (
<SelectItem key={modelId} value={modelId}>
<div className="flex items-center gap-2">
<span>{model.label}</span>
{model.hasThinking && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{/* Enabled Models */}
<div className="space-y-3">
<Label>Available Models</Label>
<div className="grid gap-3">
{availableModels.map((model) => {
const isEnabled = config.models?.includes(model.id) ?? false;
const isAuto = model.id === 'auto';
return (
<div
key={model.id}
className="flex items-center justify-between p-3 rounded-lg border bg-card"
>
<div className="flex items-center gap-3">
<Checkbox
checked={isEnabled}
onCheckedChange={(checked) => handleModelToggle(model.id, !!checked)}
disabled={isSaving || isAuto}
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{model.label}</span>
{model.hasThinking && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">{model.description}</p>
</div>
</div>
<Badge variant={model.tier === 'free' ? 'default' : 'secondary'}>
{model.tier}
</Badge>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
)}
{/* Not Installed State */}
{!status?.installed && (
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
<Terminal className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>Cursor CLI is not installed.</p>
<p className="text-sm mt-2">Install it to use Cursor models in AutoMaker.</p>
</CardContent>
</Card>
)}
</div>
);
}
export default CursorSettingsTab;
```
### Task 7.2: Create Provider Tabs Container
**Status:** `completed`
**File:** `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx`
```tsx
import React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Bot, Terminal } from 'lucide-react';
import { CursorSettingsTab } from './cursor-settings-tab';
import { ClaudeSettingsTab } from './claude-settings-tab';
interface ProviderTabsProps {
defaultTab?: 'claude' | 'cursor';
}
export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
return (
<Tabs defaultValue={defaultTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="claude" className="flex items-center gap-2">
<Bot className="w-4 h-4" />
Claude
</TabsTrigger>
<TabsTrigger value="cursor" className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Cursor
</TabsTrigger>
</TabsList>
<TabsContent value="claude">
<ClaudeSettingsTab />
</TabsContent>
<TabsContent value="cursor">
<CursorSettingsTab />
</TabsContent>
</Tabs>
);
}
export default ProviderTabs;
```
### Task 7.3: Create Claude Settings Tab (if not exists)
**Status:** `completed`
**File:** `apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx`
```tsx
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Bot, CheckCircle2, XCircle, Loader2, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/http-api-client';
interface ClaudeStatus {
installed: boolean;
version?: string;
authenticated: boolean;
method?: string;
}
export function ClaudeSettingsTab() {
const [status, setStatus] = useState<ClaudeStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
const loadStatus = async () => {
setIsLoading(true);
try {
const result = await api.setup.getClaudeStatus();
if (result.success) {
setStatus({
installed: result.installed ?? true,
version: result.version,
authenticated: result.authenticated ?? false,
method: result.method,
});
}
} catch (error) {
console.error('Failed to load Claude status:', error);
toast.error('Failed to load Claude status');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadStatus();
}, []);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Bot className="w-5 h-5" />
Claude Status
</CardTitle>
<CardDescription>Claude is the primary AI provider for AutoMaker</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm">SDK Status</span>
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs">Active</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Authentication</span>
{status?.authenticated ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs capitalize">{status.method}</span>
</div>
) : (
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<XCircle className="w-4 h-4" />
<span className="text-xs">Not authenticated</span>
</div>
)}
</div>
<Button variant="outline" size="sm" onClick={loadStatus}>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh Status
</Button>
</CardContent>
</Card>
</div>
);
}
export default ClaudeSettingsTab;
```
### Task 7.4: Update Settings View Navigation
**Status:** `completed`
**File:** `apps/ui/src/components/views/settings-view/config/navigation.ts`
Add or update providers section:
```typescript
export const SETTINGS_NAVIGATION = [
// Existing sections...
{
id: 'providers',
label: 'AI Providers',
icon: 'bot',
description: 'Configure Claude and Cursor AI providers',
},
// ... other sections
];
```
### Task 7.5: Integrate Provider Tabs in Settings
**Status:** `completed`
Update the settings view to render ProviderTabs for the providers section.
---
## Verification
### Test 1: Tab Switching
1. Navigate to Settings → Providers
2. Click on "Claude" tab
3. Verify Claude settings are displayed
4. Click on "Cursor" tab
5. Verify Cursor settings are displayed
### Test 2: Cursor Status Display
1. With Cursor CLI installed: verify version is shown
2. With Cursor authenticated: verify green checkmark
3. Without Cursor installed: verify "Not installed" state
### Test 3: Model Selection
1. Enable/disable models via checkboxes
2. Verify changes persist after refresh
3. Change default model
4. Verify default is highlighted in selector
### Test 4: Responsive Design
1. Test on different screen sizes
2. Verify tabs are usable on mobile
3. Verify model list scrolls properly
---
## Verification Checklist
Before marking this phase complete:
- [ ] ProviderTabs component renders correctly
- [ ] Tab switching works smoothly
- [ ] CursorSettingsTab shows correct status
- [ ] ClaudeSettingsTab shows correct status
- [ ] Model checkboxes toggle state
- [ ] Default model selector works
- [ ] Settings persist after page refresh
- [ ] Loading states displayed
- [ ] Error states handled gracefully
- [ ] Settings navigation includes providers
---
## Files Changed
| File | Action | Description |
| ------------------------------------------------------------------------------ | ------ | ------------- |
| `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx` | Create | Cursor config |
| `apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx` | Create | Claude config |
| `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx` | Create | Tab container |
| `apps/ui/src/components/views/settings-view/config/navigation.ts` | Modify | Add section |
---
## Design Notes
- Tabs use consistent icons (Bot for Claude, Terminal for Cursor)
- Model cards show tier badges (free/pro)
- Thinking models have a "Thinking" badge
- The "auto" model cannot be disabled
- Settings auto-save on change (no explicit save button)

View File

@@ -1,590 +0,0 @@
# Phase 8: AI Profiles Integration
**Status:** `completed`
**Dependencies:** Phase 1 (Types), Phase 7 (Settings)
**Estimated Effort:** Medium (UI + types)
---
## Objective
Extend the AI Profiles system to support Cursor as a provider, with proper handling of Cursor's embedded thinking mode (via model ID) vs Claude's separate thinking level.
---
## Key Concept: Thinking Mode Handling
### Claude Approach
- Separate `thinkingLevel` property: `'none' | 'low' | 'medium' | 'high' | 'ultrathink'`
- Applied to any Claude model
### Cursor Approach
- Thinking is **embedded in the model ID**
- Examples: `claude-sonnet-4` (no thinking) vs `claude-sonnet-4-thinking` (with thinking)
- No separate thinking level selector needed for Cursor profiles
---
## Tasks
### Task 8.1: Update AIProfile Type
**Status:** `completed`
**File:** `libs/types/src/settings.ts`
Update the AIProfile interface:
```typescript
import { CursorModelId } from './cursor-models';
/**
* AI Profile - saved configuration for different use cases
*/
export interface AIProfile {
id: string;
name: string;
description: string;
isBuiltIn: boolean;
icon?: string;
// Provider selection
provider: ModelProvider; // 'claude' | 'cursor'
// Claude-specific
model?: AgentModel; // 'opus' | 'sonnet' | 'haiku'
thinkingLevel?: ThinkingLevel; // 'none' | 'low' | 'medium' | 'high' | 'ultrathink'
// Cursor-specific
cursorModel?: CursorModelId; // 'auto' | 'claude-sonnet-4' | 'gpt-4o' | etc.
// Note: For Cursor, thinking is in the model ID (e.g., 'claude-sonnet-4-thinking')
}
/**
* Helper to determine if a profile uses thinking mode
*/
export function profileHasThinking(profile: AIProfile): boolean {
if (profile.provider === 'claude') {
return profile.thinkingLevel !== undefined && profile.thinkingLevel !== 'none';
}
if (profile.provider === 'cursor') {
const model = profile.cursorModel || 'auto';
return model.includes('thinking') || model === 'o3-mini';
}
return false;
}
/**
* Get effective model string for execution
*/
export function getProfileModelString(profile: AIProfile): string {
if (profile.provider === 'cursor') {
return `cursor-${profile.cursorModel || 'auto'}`;
}
// Claude
return profile.model || 'sonnet';
}
```
### Task 8.2: Update Profile Form Component
**Status:** `completed`
**File:** `apps/ui/src/components/views/profiles-view/components/profile-form.tsx`
Add Cursor-specific fields:
```tsx
import React, { useState } from 'react';
import { Bot, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
AIProfile,
AgentModel,
ModelProvider,
ThinkingLevel,
CursorModelId,
CURSOR_MODEL_MAP,
cursorModelHasThinking,
} from '@automaker/types';
interface ProfileFormProps {
profile: AIProfile;
onSave: (profile: AIProfile) => void;
onCancel: () => void;
}
export function ProfileForm({ profile, onSave, onCancel }: ProfileFormProps) {
const [formData, setFormData] = useState<AIProfile>(profile);
const handleProviderChange = (provider: ModelProvider) => {
setFormData((prev) => ({
...prev,
provider,
// Reset provider-specific fields
model: provider === 'claude' ? 'sonnet' : undefined,
thinkingLevel: provider === 'claude' ? 'none' : undefined,
cursorModel: provider === 'cursor' ? 'auto' : undefined,
}));
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name & Description */}
<div className="space-y-4">
<div>
<Label>Profile Name</Label>
<Input
value={formData.name}
onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))}
placeholder="My Profile"
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
placeholder="Describe when to use this profile..."
/>
</div>
</div>
{/* Provider Selection */}
<div className="space-y-2">
<Label>AI Provider</Label>
<Select value={formData.provider} onValueChange={handleProviderChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="claude">
<div className="flex items-center gap-2">
<Bot className="w-4 h-4" />
Claude (Anthropic)
</div>
</SelectItem>
<SelectItem value="cursor">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Cursor CLI
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Claude-specific settings */}
{formData.provider === 'claude' && (
<>
<div className="space-y-2">
<Label>Model</Label>
<Select
value={formData.model || 'sonnet'}
onValueChange={(v) => setFormData((p) => ({ ...p, model: v as AgentModel }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="haiku">Haiku (Fast)</SelectItem>
<SelectItem value="sonnet">Sonnet (Balanced)</SelectItem>
<SelectItem value="opus">Opus (Powerful)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Thinking Level</Label>
<Select
value={formData.thinkingLevel || 'none'}
onValueChange={(v) =>
setFormData((p) => ({ ...p, thinkingLevel: v as ThinkingLevel }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="ultrathink">Ultra</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
{/* Cursor-specific settings */}
{formData.provider === 'cursor' && (
<div className="space-y-2">
<Label>Cursor Model</Label>
<Select
value={formData.cursorModel || 'auto'}
onValueChange={(v) => setFormData((p) => ({ ...p, cursorModel: v as CursorModelId }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => (
<SelectItem key={id} value={id}>
<div className="flex items-center gap-2">
<span>{config.label}</span>
{config.hasThinking && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
<Badge
variant={config.tier === 'free' ? 'default' : 'secondary'}
className="text-xs"
>
{config.tier}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{/* Info about thinking models */}
{formData.cursorModel && cursorModelHasThinking(formData.cursorModel) && (
<p className="text-xs text-muted-foreground mt-2">
This model has built-in extended thinking capabilities.
</p>
)}
</div>
)}
{/* Form Actions */}
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Save Profile</Button>
</div>
</form>
);
}
```
### Task 8.3: Update Profile Card Display
**Status:** `completed`
**File:** `apps/ui/src/components/views/profiles-view/components/profile-card.tsx`
Show provider-specific info:
```tsx
import React from 'react';
import { Bot, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { AIProfile, CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
interface ProfileCardProps {
profile: AIProfile;
onEdit: (profile: AIProfile) => void;
onDelete: (profile: AIProfile) => void;
}
export function ProfileCard({ profile, onEdit, onDelete }: ProfileCardProps) {
const hasThinking = profileHasThinking(profile);
return (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
{profile.provider === 'cursor' ? (
<Terminal className="w-4 h-4" />
) : (
<Bot className="w-4 h-4" />
)}
{profile.name}
</CardTitle>
{profile.isBuiltIn && <Badge variant="secondary">Built-in</Badge>}
</div>
<CardDescription>{profile.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{/* Provider badge */}
<Badge variant="outline" className="capitalize">
{profile.provider}
</Badge>
{/* Model badge */}
<Badge variant="outline">
{profile.provider === 'cursor'
? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label || profile.cursorModel
: profile.model}
</Badge>
{/* Thinking badge */}
{hasThinking && <Badge variant="default">Thinking</Badge>}
</div>
</CardContent>
{!profile.isBuiltIn && (
<CardFooter className="pt-0">
<div className="flex gap-2 ml-auto">
<Button variant="ghost" size="sm" onClick={() => onEdit(profile)}>
Edit
</Button>
<Button variant="ghost" size="sm" onClick={() => onDelete(profile)}>
Delete
</Button>
</div>
</CardFooter>
)}
</Card>
);
}
```
### Task 8.4: Add Default Cursor Profiles
**Status:** `completed`
**File:** `apps/ui/src/components/views/profiles-view/constants.ts`
Add built-in Cursor profiles:
```typescript
import { AIProfile } from '@automaker/types';
export const DEFAULT_PROFILES: AIProfile[] = [
// Existing Claude profiles...
{
id: 'claude-default',
name: 'Claude Default',
description: 'Balanced Claude Sonnet model',
provider: 'claude',
model: 'sonnet',
thinkingLevel: 'none',
isBuiltIn: true,
icon: 'bot',
},
// ... other Claude profiles
// Cursor profiles
{
id: 'cursor-auto',
name: 'Cursor Auto',
description: 'Let Cursor choose the best model automatically',
provider: 'cursor',
cursorModel: 'auto',
isBuiltIn: true,
icon: 'terminal',
},
{
id: 'cursor-fast',
name: 'Cursor Fast',
description: 'Quick responses with GPT-4o Mini',
provider: 'cursor',
cursorModel: 'gpt-4o-mini',
isBuiltIn: true,
icon: 'zap',
},
{
id: 'cursor-thinking',
name: 'Cursor Thinking',
description: 'Claude Sonnet 4 with extended thinking for complex tasks',
provider: 'cursor',
cursorModel: 'claude-sonnet-4-thinking',
isBuiltIn: true,
icon: 'brain',
},
];
```
### Task 8.5: Update Profile Validation
**Status:** `completed`
Add validation for profile data:
```typescript
import { AIProfile, CURSOR_MODEL_MAP } from '@automaker/types';
export function validateProfile(profile: AIProfile): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!profile.name?.trim()) {
errors.push('Profile name is required');
}
if (!['claude', 'cursor'].includes(profile.provider)) {
errors.push('Invalid provider');
}
if (profile.provider === 'claude') {
if (!profile.model) {
errors.push('Claude model is required');
}
}
if (profile.provider === 'cursor') {
if (profile.cursorModel && !(profile.cursorModel in CURSOR_MODEL_MAP)) {
errors.push('Invalid Cursor model');
}
}
return {
valid: errors.length === 0,
errors,
};
}
```
---
## Verification
### Test 1: Profile Creation with Cursor
1. Navigate to Profiles view
2. Click "Create Profile"
3. Select "Cursor CLI" as provider
4. Select a Cursor model
5. Save the profile
6. Verify it appears in the list with correct badges
### Test 2: Thinking Mode Detection
```typescript
import { profileHasThinking } from '@automaker/types';
// Claude with thinking
const claudeThinking: AIProfile = {
id: '1',
name: 'Test',
description: '',
provider: 'claude',
model: 'sonnet',
thinkingLevel: 'high',
isBuiltIn: false,
};
console.assert(profileHasThinking(claudeThinking) === true);
// Claude without thinking
const claudeNoThinking: AIProfile = {
id: '2',
name: 'Test',
description: '',
provider: 'claude',
model: 'sonnet',
thinkingLevel: 'none',
isBuiltIn: false,
};
console.assert(profileHasThinking(claudeNoThinking) === false);
// Cursor with thinking model
const cursorThinking: AIProfile = {
id: '3',
name: 'Test',
description: '',
provider: 'cursor',
cursorModel: 'claude-sonnet-4-thinking',
isBuiltIn: false,
};
console.assert(profileHasThinking(cursorThinking) === true);
// Cursor without thinking
const cursorNoThinking: AIProfile = {
id: '4',
name: 'Test',
description: '',
provider: 'cursor',
cursorModel: 'gpt-4o',
isBuiltIn: false,
};
console.assert(profileHasThinking(cursorNoThinking) === false);
console.log('All thinking detection tests passed!');
```
### Test 3: Provider Switching
1. Create a new profile
2. Select Claude as provider
3. Configure Claude options
4. Switch to Cursor
5. Verify Claude options are hidden
6. Verify Cursor options are shown
7. Previous selections should be cleared
### Test 4: Built-in Profiles
1. Navigate to Profiles view
2. Verify Cursor built-in profiles appear
3. Verify they cannot be edited/deleted
4. Verify they show correct badges
---
## Verification Checklist
Before marking this phase complete:
- [ ] AIProfile type extended with Cursor fields
- [ ] `profileHasThinking()` works for both providers
- [ ] Profile form shows provider selector
- [ ] Claude options shown only for Claude provider
- [ ] Cursor options shown only for Cursor provider
- [ ] Cursor models show thinking badge where applicable
- [ ] Built-in Cursor profiles added
- [ ] Profile cards display provider info
- [ ] Profile validation works
- [ ] Profiles persist correctly
---
## Files Changed
| File | Action | Description |
| ------------------------------------------------------------------------ | ------ | ------------------------------ |
| `libs/types/src/settings.ts` | Modify | Add Cursor fields to AIProfile |
| `apps/ui/src/components/views/profiles-view/components/profile-form.tsx` | Modify | Add Cursor UI |
| `apps/ui/src/components/views/profiles-view/components/profile-card.tsx` | Modify | Show provider info |
| `apps/ui/src/components/views/profiles-view/constants.ts` | Modify | Add Cursor profiles |
---
## Design Notes
- Provider selection is the first choice in profile form
- Switching providers resets model-specific options
- Cursor thinking is determined by model ID, not separate field
- Built-in profiles provide good starting points
- Profile cards show provider icon and model badges

View File

@@ -1,451 +0,0 @@
# Phase 9: Task Execution Integration
**Status:** `completed`
**Dependencies:** Phase 3 (Factory), Phase 8 (Profiles)
**Estimated Effort:** Medium (service updates)
---
## Objective
Update the task execution flow (agent-service, auto-mode-service) to use the ProviderFactory for model routing, ensuring Cursor models are executed via CursorProvider.
---
## Tasks
### Task 9.1: Update Agent Service
**Status:** `pending`
**File:** `apps/server/src/services/agent-service.ts`
Update to use ProviderFactory:
```typescript
import { ProviderFactory } from '../providers/provider-factory';
import { getProfileModelString, profileHasThinking } from '@automaker/types';
export class AgentService {
// ...existing code...
/**
* Execute a chat message using the appropriate provider
*/
async executeChat(sessionId: string, message: string, options: ChatOptions = {}): Promise<void> {
const session = this.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
// Determine effective model
const profile = options.profile;
let effectiveModel: string;
if (profile) {
effectiveModel = getProfileModelString(profile);
} else {
effectiveModel = options.model || session.model || 'sonnet';
}
// Get provider for this model
const provider = ProviderFactory.getProviderForModel(effectiveModel, {
cwd: session.workDir,
});
const providerName = provider.getName();
this.logger.debug(`[AgentService] Using ${providerName} provider for model ${effectiveModel}`);
// Build execution options
const executeOptions: ExecuteOptions = {
prompt: message,
model: effectiveModel,
cwd: session.workDir,
systemPrompt: this.buildSystemPrompt(session, options),
maxTurns: options.maxTurns || 100,
allowedTools: options.allowedTools || TOOL_PRESETS.chat,
abortController: session.abortController,
conversationHistory: session.conversationHistory,
sdkSessionId: session.sdkSessionId,
};
// Add thinking level for Claude
if (providerName === 'claude' && profile?.thinkingLevel) {
executeOptions.thinkingLevel = profile.thinkingLevel;
}
try {
// Stream from provider
const stream = provider.executeQuery(executeOptions);
for await (const msg of stream) {
// Capture session ID
if (msg.session_id && !session.sdkSessionId) {
session.sdkSessionId = msg.session_id;
}
// Process message and emit events
this.processProviderMessage(sessionId, msg);
}
} catch (error) {
this.handleProviderError(sessionId, error, providerName);
}
}
/**
* Process a provider message and emit appropriate events
*/
private processProviderMessage(sessionId: string, msg: ProviderMessage): void {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
this.emitAgentEvent(sessionId, {
type: 'stream',
content: block.text,
});
} else if (block.type === 'tool_use') {
this.emitAgentEvent(sessionId, {
type: 'tool_use',
tool: {
name: block.name,
input: block.input,
id: block.tool_use_id,
},
});
} else if (block.type === 'tool_result') {
this.emitAgentEvent(sessionId, {
type: 'tool_result',
toolId: block.tool_use_id,
content: block.content,
});
} else if (block.type === 'thinking' && block.thinking) {
this.emitAgentEvent(sessionId, {
type: 'thinking',
content: block.thinking,
});
}
}
} else if (msg.type === 'result') {
this.emitAgentEvent(sessionId, {
type: 'complete',
content: msg.result || '',
});
} else if (msg.type === 'error') {
this.emitAgentEvent(sessionId, {
type: 'error',
error: msg.error || 'Unknown error',
});
}
}
/**
* Handle provider-specific errors
*/
private handleProviderError(sessionId: string, error: any, providerName: string): void {
let errorMessage = error.message || 'Unknown error';
let suggestion = error.suggestion;
// Add provider context
if (providerName === 'cursor' && error.code) {
switch (error.code) {
case 'CURSOR_NOT_AUTHENTICATED':
suggestion = 'Run "cursor-agent login" in your terminal';
break;
case 'CURSOR_RATE_LIMITED':
suggestion = 'Wait a few minutes or upgrade to Cursor Pro';
break;
case 'CURSOR_NOT_INSTALLED':
suggestion = 'Install Cursor CLI: curl https://cursor.com/install -fsS | bash';
break;
}
}
this.emitAgentEvent(sessionId, {
type: 'error',
error: errorMessage,
suggestion,
provider: providerName,
});
this.logger.error(`[AgentService] ${providerName} error:`, error);
}
}
```
### Task 9.2: Update Auto Mode Service
**Status:** `pending`
**File:** `apps/server/src/services/auto-mode-service.ts`
Update the `runAgent` method:
```typescript
import { ProviderFactory } from '../providers/provider-factory';
import { getProfileModelString } from '@automaker/types';
export class AutoModeService {
// ...existing code...
/**
* Run the agent for a task
*/
private async runAgent(task: Task, options: AutoModeOptions): Promise<AgentResult> {
const { workDir, profile, maxTurns } = options;
// Determine model from profile or task
let model: string;
if (profile) {
model = getProfileModelString(profile);
} else {
model = task.model || 'sonnet';
}
// Get provider
const provider = ProviderFactory.getProviderForModel(model, { cwd: workDir });
const providerName = provider.getName();
this.logger.info(`[AutoMode] Running with ${providerName} provider, model: ${model}`);
// Build execution options
const executeOptions: ExecuteOptions = {
prompt: this.buildPrompt(task),
model,
cwd: workDir,
systemPrompt: options.systemPrompt,
maxTurns: maxTurns || MAX_TURNS.extended,
allowedTools: options.allowedTools || TOOL_PRESETS.fullAccess,
abortController: options.abortController,
};
let responseText = '';
const toolCalls: ToolCall[] = [];
try {
const stream = provider.executeQuery(executeOptions);
for await (const msg of stream) {
// Emit progress events
this.emitProgress(task.id, msg, providerName);
// Collect response
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
responseText += block.text || '';
} else if (block.type === 'tool_use') {
toolCalls.push({
id: block.tool_use_id,
name: block.name,
input: block.input,
});
}
}
}
}
return {
success: true,
response: responseText,
toolCalls,
provider: providerName,
};
} catch (error) {
return {
success: false,
error: error.message,
suggestion: error.suggestion,
provider: providerName,
};
}
}
/**
* Emit progress event for UI updates
*/
private emitProgress(taskId: string, msg: ProviderMessage, provider: string): void {
// Emit event for log viewer and progress tracking
this.events.emit('auto-mode:event', {
taskId,
provider,
message: msg,
timestamp: Date.now(),
});
}
}
```
### Task 9.3: Update Model Selector in Board View
**Status:** `pending`
**File:** `apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx`
Add Cursor models to selection:
```tsx
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
interface ModelOption {
id: string;
label: string;
provider: 'claude' | 'cursor';
hasThinking?: boolean;
}
const MODEL_OPTIONS: ModelOption[] = [
// Claude models
{ id: 'haiku', label: 'Claude Haiku', provider: 'claude' },
{ id: 'sonnet', label: 'Claude Sonnet', provider: 'claude' },
{ id: 'opus', label: 'Claude Opus', provider: 'claude' },
// Cursor models
...Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ({
id: `cursor-${id}`,
label: `Cursor: ${config.label}`,
provider: 'cursor' as const,
hasThinking: config.hasThinking,
})),
];
// In the dialog form:
<div className="space-y-2">
<Label>Model</Label>
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Claude</SelectLabel>
{MODEL_OPTIONS.filter((m) => m.provider === 'claude').map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.label}
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Cursor</SelectLabel>
{MODEL_OPTIONS.filter((m) => m.provider === 'cursor').map((model) => (
<SelectItem key={model.id} value={model.id}>
<div className="flex items-center gap-2">
{model.label}
{model.hasThinking && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>;
```
### Task 9.4: Update Feature Execution with Provider Tracking
**Status:** `pending`
Track which provider executed each feature for UI display:
```typescript
interface FeatureExecution {
id: string;
featureId: string;
model: string;
provider: 'claude' | 'cursor';
startTime: number;
endTime?: number;
status: 'running' | 'completed' | 'failed';
error?: string;
}
// Store provider info in execution results
const execution: FeatureExecution = {
id: generateId(),
featureId: feature.id,
model: effectiveModel,
provider: ProviderFactory.getProviderNameForModel(effectiveModel),
startTime: Date.now(),
status: 'running',
};
```
---
## Verification
### Test 1: Claude Model Execution
1. Create a task with a Claude model (e.g., `sonnet`)
2. Execute the task
3. Verify ClaudeProvider is used
4. Verify output streams correctly
5. Verify tool calls work
### Test 2: Cursor Model Execution
1. Create a task with a Cursor model (e.g., `cursor-auto`)
2. Execute the task
3. Verify CursorProvider is used
4. Verify output streams correctly
5. Verify tool calls work
### Test 3: Profile-Based Execution
1. Create a Cursor profile
2. Use that profile for a task
3. Verify correct provider is selected
4. Verify profile settings are applied
### Test 4: Error Handling
1. Use Cursor model without CLI installed
2. Verify appropriate error message
3. Verify suggestion is shown
4. Verify execution can be retried
### Test 5: Mixed Provider Session
1. Run a task with Claude
2. Run another task with Cursor
3. Verify both execute correctly
4. Verify logs show correct provider info
---
## Verification Checklist
Before marking this phase complete:
- [ ] AgentService uses ProviderFactory
- [ ] AutoModeService uses ProviderFactory
- [ ] Claude models route to ClaudeProvider
- [ ] Cursor models route to CursorProvider
- [ ] Profile model string conversion works
- [ ] Provider errors include suggestions
- [ ] Progress events include provider info
- [ ] Model selector includes Cursor models
- [ ] Execution results track provider
- [ ] Log viewer shows provider context
---
## Files Changed
| File | Action | Description |
| ------------------------------------------------------------------------ | ------ | ------------------- |
| `apps/server/src/services/agent-service.ts` | Modify | Use ProviderFactory |
| `apps/server/src/services/auto-mode-service.ts` | Modify | Use ProviderFactory |
| `apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx` | Modify | Add Cursor models |
---
## Notes
- Provider selection happens at execution time, not configuration time
- Session state may span provider switches
- Error handling is provider-aware
- Progress events include provider for UI grouping

View File

@@ -1,787 +0,0 @@
# Per-Phase AI Provider Configuration - Implementation Plan
> **Created**: 2024-12-30
> **Approach**: UI-First with incremental wiring
> **Estimated Total Effort**: 20-25 hours
## Overview
Allow users to configure which AI provider/model to use for each distinct phase of the application. This gives users fine-grained control over cost, speed, and quality tradeoffs.
---
## Current State Analysis
### Existing Settings Fields (UNUSED)
These fields already exist in `GlobalSettings` but are not wired up:
```typescript
// libs/types/src/settenhancementModel: AgentModel; // Currently ignored, hardcoded to 'sonnet'
validationModel: AgentModel; // Currently ignored, hardcoded to 'opus'
```
### All AI Usage Phases
| Phase | Location | Current Model | Priority |
| ------------------- | -------------------------------- | ------------------ | -------- |
| Feature Execution | `auto-mode-service.ts` | Per-feature | ✅ Done |
| Enhancement | `enhance.ts` | Hardcoded `sonnet` | ✅ Done |
| GitHub Validation | `validate-issue.ts` | Hardcoded `opus` | ✅ Done |
| File Description | `describe-file.ts` | Hardcoded `haiku` | P2 |
| Image Description | `describe-image.ts` | Hardcoded `haiku` | P2 |
| App Spec Generation | `generate-spec.ts` | SDK default | P2 |
| Feature from Spec | `generate-features-from-spec.ts` | SDK default | P3 |
| Backlog Planning | `generate-plan.ts` | SDK default | P3 |
| Project Analysis | `analyze-project.ts` | Hardcoded default | P3 |
---
## Phase 1: Type Definitions & Settings Structure
**Effort**: 2-3 hours
**Files**: `libs/types/src/settings.ts`
### 1.1 Add PhaseModelConfig Type
```typescript
/**
* Configuration for AI models used in different application phases
*/
export interface PhaseModelConfig {
// Quick tasks - recommend fast/cheap models (Haiku, Cursor auto)
enhancementModel: AgentModel | CursorModelId;
fileDescriptionModel: AgentModel | CursorModelId;
imageDescriptionModel: AgentModel | CursorModelId;
// Validation tasks - recommend smart models (Sonnet, Opus)
validationModel: AgentModel | CursorModelId;
// Generation tasks - recommend powerful models (Opus, Sonnet)
specGenerationModel: AgentModel | CursorModelId;
featureGenerationModel: AgentModel | CursorModelId;
backlogPlanningModel: AgentModel | CursorModelId;
projectAnalysisModel: AgentModel | CursorModelId;
}
```
### 1.2 Update GlobalSettings
```typescript
export interface GlobalSettings {
// ... existing fields ...
// Phase-specific model configuration
phaseModels: PhaseModelConfig;
// Legacy fields (keep for backwards compatibility)
enhancementModel?: AgentModel; // Deprecated, use phaseModels
validationModel?: AgentModel; // Deprecated, use phaseModels
}
```
### 1.3 Default Values
```typescript
export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
// Quick tasks - use fast models
enhancementModel: 'sonnet',
fileDescriptionModel: 'haiku',
imageDescriptionModel: 'haiku',
// Validation - use smart models
validationModel: 'sonnet',
// Generation - use powerful models
specGenerationModel: 'opus',
featureGenerationModel: 'sonnet',
backlogPlanningModel: 'sonnet',
projectAnalysisModel: 'sonnet',
};
```
### 1.4 Migration Helper
```typescript
// In settings-service.ts
function migrateSettings(settings: GlobalSettings): GlobalSettings {
// Migrate legacy fields to new structure
if (!settings.phaseModels) {
settings.phaseModels = {
...DEFAULT_PHASE_MODELS,
enhancementModel: settings.enhancementModel || 'sonnet',
validationModel: settings.validationModel || 'opus',
};
}
return settings;
}
```
---
## Phase 2: Settings UI
**Effort**: 6-8 hours
**Files**:
- `apps/ui/src/components/views/settings-view.tsx`
- `apps/ui/src/components/views/settings-view/phase-models-tab.tsx` (new)
### 2.1 Create PhaseModelsTab Component
```
┌─────────────────────────────────────────────────────────────┐
│ AI Phase Configuration │
├─────────────────────────────────────────────────────────────┤
│ │
│ Configure which AI model to use for each application task. │
│ Cursor models require cursor-agent CLI installed. │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ QUICK TASKS │ │
│ │ Fast models recommended for speed and cost savings │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Feature Enhancement │ │
│ │ Improves feature names and descriptions │ │
│ │ [Claude Sonnet ▼] [Haiku] [Cursor Auto] │ │
│ │ │ │
│ │ File Descriptions │ │
│ │ Generates descriptions for context files │ │
│ │ [Claude Haiku ▼] [Sonnet] [Cursor Auto] │ │
│ │ │ │
│ │ Image Descriptions │ │
│ │ Analyzes and describes context images │ │
│ │ [Claude Haiku ▼] [Sonnet] [Cursor Auto] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ VALIDATION TASKS │ │
│ │ Smart models recommended for accuracy │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ GitHub Issue Validation │ │
│ │ Validates and improves GitHub issues │ │
│ │ [Claude Sonnet ▼] [Opus] [Cursor Sonnet] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GENERATION TASKS │ │
│ │ Powerful models recommended for quality │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ App Specification │ │
│ │ Generates full application specifications │ │
│ │ [Claude Opus ▼] [Sonnet] [Cursor Opus] │ │
│ │ │ │
│ │ Feature Generation │ │
│ │ Creates features from specifications │ │
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
│ │ │ │
│ │ Backlog Planning │ │
│ │ Reorganizes and prioritizes backlog │ │
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
│ │ │ │
│ │ Project Analysis │ │
│ │ Analyzes project structure for suggestions │ │
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [Reset to Defaults] [Save Changes] │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 PhaseModelSelector Component
Reusable component for selecting model per phase:
```typescript
interface PhaseModelSelectorProps {
phase: keyof PhaseModelConfig;
label: string;
description: string;
value: AgentModel | CursorModelId;
onChange: (value: AgentModel | CursorModelId) => void;
recommendedModels?: string[];
}
```
Features:
- Shows both Claude and Cursor models
- Indicates which provider each model uses
- Shows "Recommended" badge on suggested models
- Disables Cursor models if CLI not installed
- Shows thinking level indicator for supported models
### 2.3 Add Tab to Settings View
```typescript
// In settings-view.tsx, add new tab
const SETTINGS_TABS = [
{ id: 'general', label: 'General', icon: Settings },
{ id: 'ai-profiles', label: 'AI Profiles', icon: Bot },
{ id: 'phase-models', label: 'Phase Models', icon: Workflow }, // NEW
{ id: 'providers', label: 'Providers', icon: Key },
// ...
];
```
---
## Phase 3: Wire Enhancement Route (P1)
**Effort**: 2-3 hours
**Files**: `apps/server/src/routes/enhance-prompt/routes/enhance.ts`
### 3.1 Current Code
```typescript
// BEFORE - Hardcoded
const model = CLAUDE_MODEL_MAP.sonnet;
```
### 3.2 Updated Code
```typescript
// AFTER - Uses settings
import { SettingsService } from '@/services/settings-service.js';
import { ProviderFactory } from '@/providers/provider-factory.js';
const settingsService = new SettingsService(dataDir);
const settings = await settingsService.getSettings();
const modelId = settings.phaseModels?.enhancementModel || 'sonnet';
// Resolve to full model string
const provider = ProviderFactory.getProviderForModel(modelId);
const model = resolveModelString(modelId);
```
### 3.3 Test Cases
- [ ] Default behavior (uses sonnet) still works
- [ ] Changing to haiku in settings uses haiku
- [ ] Changing to cursor-auto routes to Cursor provider
- [ ] Invalid model falls back to default
---
## Phase 4: Wire Validation Route (P1)
**Effort**: 2-3 hours
**Files**: `apps/server/src/routes/github/routes/validate-issue.ts`
### 4.1 Current Code
```typescript
// BEFORE - Has model param but defaults to hardcoded
const model = request.body.model || 'opus';
```
### 4.2 Updated Code
```typescript
// AFTER - Uses settings as default
const settings = await settingsService.getSettings();
const defaultModel = settings.phaseModels?.validationModel || 'opus';
const model = request.body.model || defaultModel;
```
### 4.3 Test Cases
- [ ] Default uses configured model from settings
- [ ] Explicit model in request overrides settings
- [ ] Cursor models work for validation
---
## Phase 5: Wire Context Description Routes (P2)
**Effort**: 3-4 hours
**Files**:
- `apps/server/src/routes/context/routes/describe-file.ts`
- `apps/server/src/routes/context/routes/describe-image.ts`
### 5.1 Pattern
Same pattern as enhancement - replace hardcoded `haiku` with settings lookup.
### 5.2 Test Cases
- [ ] File description uses configured model
- [ ] Image description uses configured model (with vision support check)
- [ ] Fallback to haiku if model doesn't support vision
---
## Phase 6: Wire Generation Routes (P2)
**Effort**: 4-5 hours
**Files**:
- `apps/server/src/routes/app-spec/generate-spec.ts`
- `apps/server/src/routes/app-spec/generate-features-from-spec.ts`
### 6.1 Pattern
These routes use the Claude SDK directly. Need to:
1. Load settings
2. Resolve model string
3. Pass to SDK configuration
### 6.2 Test Cases
- [ ] App spec generation uses configured model
- [ ] Feature generation uses configured model
- [ ] Works with both Claude and Cursor providers
---
## Phase 7: Wire Remaining Routes (P3)
**Effort**: 4-5 hours
**Files**:
- `apps/server/src/routes/backlog-plan/generate-plan.ts`
- `apps/server/src/routes/auto-mode/routes/analyze-project.ts`
### 7.1 Pattern
Same settings injection pattern.
### 7.2 Test Cases
- [ ] Backlog planning uses configured model
- [ ] Project analysis uses configured model
---
## Implementation Order
```
Phase 1: Types & Settings Structure
Phase 2: Settings UI (Phase Models tab)
Phase 8: Quick Model Override Component <- Right after UI for easier testing
Phase 9: Integration Points for Override <- Wire override to each feature
Then wire routes (testing both global + override together):
Phase 3: Enhancement Route - Done
Phase 4: Validation Route - Broken validation parsing ! need urgent fix to properly validate cursor cli output
Phase 5: Context Routes (file/image description)
Phase 6: Generation Routes (spec, features)
Phase 7: Remaining Routes (backlog, analysis)
```
---
## File Changes Summary
### New Files
- `apps/ui/src/components/views/settings-view/phase-models-tab.tsx`
- `apps/ui/src/components/views/settings-view/phase-model-selector.tsx`
### Modified Files
| File | Changes |
| ---------------------------------------------------------------- | ------------------------- |
| `libs/types/src/settings.ts` | Add PhaseModelConfig type |
| `apps/server/src/services/settings-service.ts` | Add migration logic |
| `apps/ui/src/components/views/settings-view.tsx` | Add Phase Models tab |
| `apps/server/src/routes/enhance-prompt/routes/enhance.ts` | Use settings |
| `apps/server/src/routes/github/routes/validate-issue.ts` | Use settings |
| `apps/server/src/routes/context/routes/describe-file.ts` | Use settings |
| `apps/server/src/routes/context/routes/describe-image.ts` | Use settings |
| `apps/server/src/routes/app-spec/generate-spec.ts` | Use settings |
| `apps/server/src/routes/app-spec/generate-features-from-spec.ts` | Use settings |
| `apps/server/src/routes/backlog-plan/generate-plan.ts` | Use settings |
| `apps/server/src/routes/auto-mode/routes/analyze-project.ts` | Use settings |
---
## Testing Strategy
### Unit Tests
- Settings migration preserves existing values
- Default values applied correctly
- Model resolution works for both providers
### Integration Tests
- Each phase uses configured model
- Provider factory routes correctly
- Cursor fallback when CLI not available
### E2E Tests
- Settings UI saves correctly
- Changes persist across restarts
- Each feature works with non-default model
---
## Rollback Plan
If issues arise:
1. All routes have fallback to hardcoded defaults
2. Settings migration is additive (doesn't remove old fields)
3. Can revert individual routes independently
---
## Success Criteria
- [ ] Users can configure model for each phase via Settings UI
- [ ] All 8+ phases respect configured model
- [ ] Cursor models work for all applicable phases
- [ ] Graceful fallback when Cursor CLI not available
- [ ] Settings persist across app restarts
- [ ] No regression in existing functionality
---
---
## Phase 8: Quick Model Override Component (P1)
**Effort**: 4-6 hours
**Files**:
- `apps/ui/src/components/shared/model-override-popover.tsx` (new)
- `apps/ui/src/components/shared/model-override-trigger.tsx` (new)
### 8.1 Concept
Global defaults are great, but users often want to override for a specific run:
- "Use Opus for this complex feature"
- "Use Cursor for this quick fix"
- "Use Haiku to save costs on this simple task"
### 8.2 Component: ModelOverrideTrigger
A small gear/settings icon that opens the override popover:
```typescript
interface ModelOverrideTriggerProps {
// Current effective model (from global settings or explicit override)
currentModel: string;
// Callback when user selects override
onModelChange: (model: string | null) => void;
// Optional: which phase this is for (shows recommended models)
phase?: keyof PhaseModelConfig;
// Size variants for different contexts
size?: 'sm' | 'md' | 'lg';
// Show as icon-only or with label
variant?: 'icon' | 'button' | 'inline';
}
```
### 8.3 Component: ModelOverridePopover
```
┌──────────────────────────────────────────────┐
│ Model Override [x] │
├──────────────────────────────────────────────┤
│ │
│ Current: Claude Sonnet (from settings) │
│ │
│ ○ Use Global Setting │
│ └─ Claude Sonnet │
│ │
│ ● Override for this run: │
│ │
│ CLAUDE │
│ ┌──────┐ ┌──────┐ ┌───────┐ │
│ │ Opus │ │Sonnet│ │ Haiku │ │
│ └──────┘ └──────┘ └───────┘ │
│ │
│ CURSOR │
│ ┌──────┐ ┌────────┐ ┌───────┐ │
│ │ Auto │ │Sonnet45│ │GPT-5.2│ │
│ └──────┘ └────────┘ └───────┘ │
│ │
│ [Clear Override] [Apply] │
│ │
└──────────────────────────────────────────────┘
```
### 8.4 Usage Examples
**In Feature Modal (existing model selector enhancement):**
```tsx
<div className="flex items-center gap-2">
<Label>Model</Label>
<ModelOverrideTrigger
currentModel={feature.model || globalDefault}
onModelChange={(model) => setFeature({ ...feature, model })}
phase="featureExecution"
size="md"
variant="button"
/>
</div>
```
**In Kanban Card Actions:**
```tsx
<CardActions>
<Button onClick={handleImplement}>Implement</Button>
<ModelOverrideTrigger
currentModel={feature.model}
onModelChange={handleQuickModelChange}
size="sm"
variant="icon"
/>
</CardActions>
```
**In Enhancement Dialog:**
```tsx
<DialogHeader>
<DialogTitle>Enhance Feature</DialogTitle>
<ModelOverrideTrigger
currentModel={settings.phaseModels.enhancementModel}
onModelChange={setEnhanceModel}
phase="enhancement"
size="sm"
variant="icon"
/>
</DialogHeader>
```
**In GitHub Issue Import:**
```tsx
<div className="flex justify-between">
<span>Validating issue...</span>
<ModelOverrideTrigger
currentModel={validationModel}
onModelChange={setValidationModel}
phase="validation"
size="sm"
variant="inline"
/>
</div>
```
### 8.5 Visual Variants
```
Icon Only (size=sm):
┌───┐
│ ⚙ │ <- Just gear icon, hover shows current model
└───┘
Button (size=md):
┌─────────────────┐
│ ⚙ Claude Sonnet │ <- Gear + model name
└─────────────────┘
Inline (size=sm):
Using Claude Sonnet ⚙ <- Text with gear at end
```
### 8.6 State Management
```typescript
// Hook for managing model overrides
function useModelOverride(phase: keyof PhaseModelConfig) {
const { settings } = useSettings();
const [override, setOverride] = useState<string | null>(null);
const effectiveModel = override || settings.phaseModels[phase];
const isOverridden = override !== null;
const clearOverride = () => setOverride(null);
return {
effectiveModel,
isOverridden,
setOverride,
clearOverride,
globalDefault: settings.phaseModels[phase],
};
}
```
### 8.7 Visual Feedback for Overrides
When a model is overridden from global:
- Show small indicator dot on the gear icon
- Different color tint on the trigger
- Tooltip shows "Overridden from global setting"
```tsx
// Indicator when overridden
<div className="relative">
<GearIcon />
{isOverridden && <div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full" />}
</div>
```
---
## Phase 9: Integration Points for Quick Override
**Effort**: 3-4 hours
### 9.1 Feature Modal
**File**: `apps/ui/src/components/views/board-view/components/feature-modal.tsx`
Replace current model selector with ModelOverrideTrigger:
- Shows inherited model from AI Profile
- Allows quick override for this feature
- Clear override returns to profile default
### 9.2 Kanban Card
**File**: `apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`
Add small gear icon next to "Implement" button:
- Quick model change before running
- Doesn't persist to feature (one-time override)
### 9.3 Enhancement Dialog
**File**: `apps/ui/src/components/views/board-view/components/enhance-dialog.tsx`
Add override trigger in header:
- Default from global settings
- Override for this enhancement only
### 9.4 GitHub Import
**File**: `apps/ui/src/components/views/github-view/`
Add override for validation model:
for this feature
- Clear override returns to profile default
### 9.2 Kanban Card
**File**: `apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`
Add small gear icon next to "Implement" button:
- Quick model change before running
- Doesn't persist to feature (one-time override)
### 9.3 Enhancement Dialog
**File**: `apps/ui/src/components/views/board-view/components/enhance-dialog.tsx`
Add override trigger in header:
- Default from global settings
- Override for this enhancement only
### 9.4 GitHub Import
**File**: `apps/ui/src/components/views/github-view/`
Add override for validation model:
- Default from global settings
- Override for this import session
---
## Updated Implementation Order
```
FOUNDATION:
├── Phase 1: Types & Settings Structure
├── Phase 2: Settings UI (Phase Models tab)
├── Phase 8: Quick Model Override Component
└── Phase 9: Integration Points (wire override to feature modal, kanban, etc.)
ROUTE WIRING (test both global settings + quick override for each):
├── Phase 3: Enhancement Route + Test global + override
├── Phase 4: Validation Route + Test global + override
├── Phase 5: Context Routes + Test global + override
├── Phase 6: Generation Routes + Test global + override
└── Phase 7: Remaining Routes + Test global + override
FINALIZATION:
├── Full Integration Testing
└── Documentation
```
---
## Architecture: Global vs Override
```
┌─────────────────────────────────────────────────────────────┐
│ Settings Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 1: Global Defaults (DEFAULT_PHASE_MODELS) │
│ │ │
│ ▼ │
│ Level 2: User Global Settings (settings.phaseModels) │
│ │ │
│ ▼ │
│ Level 3: Feature-Level Override (feature.model) │
│ │ │
│ ▼ │
│ Level 4: Run-Time Override (via ModelOverridePopover) │
│ │
│ Resolution: First non-null value wins (bottom-up) │
│ │
└─────────────────────────────────────────────────────────────┘
```
```typescript
function resolveModel(
phase: keyof PhaseModelConfig,
feature?: Feature,
runtimeOverride?: string
): string {
// Runtime override takes precedence
if (runtimeOverride) return runtimeOverride;
// Feature-level override
if (feature?.model) return feature.model;
// User global settings
const settings = getSettings();
if (settings.phaseModels?.[phase]) return settings.phaseModels[phase];
// Default
return DEFAULT_PHASE_MODELS[phase];
}
```
---
## Future Enhancements
1. **Per-Project Overrides**: Allow project-level phase model config
2. **Quick Presets**: "Cost Optimized", "Quality First", "Balanced" presets
3. **Usage Stats**: Show which models used for which phases
4. **Auto-Selection**: ML-based model selection based on task complexity
5. **Model History**: Remember last-used model per phase for quick access
6. **Keyboard Shortcuts**: Cmd+Shift+M to quickly change model anywhere