mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
@@ -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.
|
|
||||||
@@ -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 |
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user