mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,10 +24,10 @@ This document serves as a comprehensive guide for writing clean, maintainable, a
|
||||
|
||||
```typescript
|
||||
// Repeated validation logic
|
||||
if (email.includes("@") && email.length > 5) {
|
||||
if (email.includes('@') && email.length > 5) {
|
||||
// ...
|
||||
}
|
||||
if (email.includes("@") && email.length > 5) {
|
||||
if (email.includes('@') && email.length > 5) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -36,7 +36,7 @@ if (email.includes("@") && email.length > 5) {
|
||||
|
||||
```typescript
|
||||
function isValidEmail(email: string): boolean {
|
||||
return email.includes("@") && email.length > 5;
|
||||
return email.includes('@') && email.length > 5;
|
||||
}
|
||||
|
||||
if (isValidEmail(email)) {
|
||||
@@ -101,8 +101,8 @@ function calculateUserTotal(userId: string) {
|
||||
```typescript
|
||||
function processPayment(amount: number, cardNumber: string, cvv: string) {
|
||||
// Direct implementation tied to specific payment processor
|
||||
fetch("https://stripe.com/api/charge", {
|
||||
method: "POST",
|
||||
fetch('https://stripe.com/api/charge', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amount, cardNumber, cvv }),
|
||||
});
|
||||
}
|
||||
@@ -112,26 +112,16 @@ function processPayment(amount: number, cardNumber: string, cvv: string) {
|
||||
|
||||
```typescript
|
||||
interface PaymentProcessor {
|
||||
processPayment(
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
): Promise<PaymentResult>;
|
||||
processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult>;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
async processPayment(
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
): Promise<PaymentResult> {
|
||||
async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
function processPayment(
|
||||
processor: PaymentProcessor,
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
) {
|
||||
function processPayment(processor: PaymentProcessor, amount: number, details: PaymentDetails) {
|
||||
return processor.processPayment(amount, details);
|
||||
}
|
||||
```
|
||||
@@ -156,9 +146,9 @@ function processPayment(
|
||||
|
||||
```typescript
|
||||
function sendNotification(user: User, type: string) {
|
||||
if (type === "email") {
|
||||
if (type === 'email') {
|
||||
sendEmail(user.email);
|
||||
} else if (type === "sms") {
|
||||
} else if (type === 'sms') {
|
||||
sendSMS(user.phone);
|
||||
}
|
||||
// Adding new notification types requires modifying this function
|
||||
@@ -219,7 +209,7 @@ setTimeout(() => {
|
||||
// What does 3000 mean?
|
||||
}, 3000);
|
||||
|
||||
if (status === "active") {
|
||||
if (status === 'active') {
|
||||
// What are the valid statuses?
|
||||
}
|
||||
```
|
||||
@@ -231,9 +221,9 @@ const MINIMUM_AGE_FOR_ADULTS = 18;
|
||||
const SESSION_TIMEOUT_MS = 3000;
|
||||
|
||||
enum UserStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
SUSPENDED = "suspended",
|
||||
ACTIVE = 'active',
|
||||
INACTIVE = 'inactive',
|
||||
SUSPENDED = 'suspended',
|
||||
}
|
||||
|
||||
if (user.age >= MINIMUM_AGE_FOR_ADULTS) {
|
||||
@@ -340,7 +330,7 @@ function divide(a: number, b: number) {
|
||||
// Good
|
||||
function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero is not allowed");
|
||||
throw new Error('Division by zero is not allowed');
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
@@ -418,7 +408,7 @@ function processUser(user: User): string {
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
const apiUrl = "https://api.example.com";
|
||||
const apiUrl = 'https://api.example.com';
|
||||
const timeout = 5000;
|
||||
|
||||
// Good
|
||||
@@ -429,9 +419,9 @@ interface Config {
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
apiUrl: process.env.API_URL || "https://api.example.com",
|
||||
timeout: parseInt(process.env.TIMEOUT || "5000"),
|
||||
maxRetries: parseInt(process.env.MAX_RETRIES || "3"),
|
||||
apiUrl: process.env.API_URL || 'https://api.example.com',
|
||||
timeout: parseInt(process.env.TIMEOUT || '5000'),
|
||||
maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@@ -90,13 +90,17 @@ components/views/
|
||||
## Global vs View-Specific Code
|
||||
|
||||
### Global (`src/hooks/`, `src/lib/`, etc.)
|
||||
|
||||
Code that is used across **multiple views**:
|
||||
|
||||
- `src/hooks/use-auto-mode.ts` - Used by board-view, agent-view, etc.
|
||||
- `src/hooks/use-keyboard-shortcuts.ts` - Used across the app
|
||||
- `src/lib/utils.ts` - Global utilities
|
||||
|
||||
### View-Specific (`[view-name]/hooks/`, `[view-name]/components/`)
|
||||
|
||||
Code that is **only used within a single view**:
|
||||
|
||||
- `board-view/hooks/use-board-actions.ts` - Only used by board-view
|
||||
- `board-view/components/kanban-card.tsx` - Only used by board-view
|
||||
|
||||
@@ -106,16 +110,17 @@ Use `index.ts` files to create clean import paths:
|
||||
|
||||
```tsx
|
||||
// board-view/hooks/index.ts
|
||||
export { useBoardActions } from "./use-board-actions";
|
||||
export { useBoardFeatures } from "./use-board-features";
|
||||
export { useBoardActions } from './use-board-actions';
|
||||
export { useBoardFeatures } from './use-board-features';
|
||||
|
||||
// Usage in board-view.tsx
|
||||
import { useBoardActions, useBoardFeatures } from "./board-view/hooks";
|
||||
import { useBoardActions, useBoardFeatures } from './board-view/hooks';
|
||||
```
|
||||
|
||||
## When to Create a Subfolder
|
||||
|
||||
Create a subfolder for a view when:
|
||||
|
||||
1. The view file exceeds ~500 lines
|
||||
2. The view has 3+ related components
|
||||
3. The view has 2+ custom hooks
|
||||
@@ -126,38 +131,37 @@ Create a subfolder for a view when:
|
||||
The `dialogs/` folder contains all dialog and modal components specific to a view:
|
||||
|
||||
### What goes in `dialogs/`:
|
||||
|
||||
- Confirmation dialogs (e.g., `delete-all-verified-dialog.tsx`)
|
||||
- Form dialogs (e.g., `add-feature-dialog.tsx`, `edit-feature-dialog.tsx`)
|
||||
- Modal overlays (e.g., `agent-output-modal.tsx`, `completed-features-modal.tsx`)
|
||||
- Any component that renders as an overlay/popup
|
||||
|
||||
### Naming convention:
|
||||
|
||||
- Use `-dialog.tsx` suffix for confirmation/form dialogs
|
||||
- Use `-modal.tsx` suffix for content-heavy modals
|
||||
|
||||
### Barrel export pattern:
|
||||
|
||||
```tsx
|
||||
// dialogs/index.ts
|
||||
export { AddFeatureDialog } from "./add-feature-dialog";
|
||||
export { EditFeatureDialog } from "./edit-feature-dialog";
|
||||
export { AgentOutputModal } from "./agent-output-modal";
|
||||
export { AddFeatureDialog } from './add-feature-dialog';
|
||||
export { EditFeatureDialog } from './edit-feature-dialog';
|
||||
export { AgentOutputModal } from './agent-output-modal';
|
||||
// ... etc
|
||||
|
||||
// Usage in view entry point
|
||||
import {
|
||||
AddFeatureDialog,
|
||||
EditFeatureDialog,
|
||||
AgentOutputModal,
|
||||
} from "./board-view/dialogs";
|
||||
import { AddFeatureDialog, EditFeatureDialog, AgentOutputModal } from './board-view/dialogs';
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Location | File Naming | Export Naming |
|
||||
|----------|-------------|---------------|
|
||||
| Components | `kebab-case.tsx` | `PascalCase` |
|
||||
| Dialogs | `*-dialog.tsx` or `*-modal.tsx` | `PascalCase` |
|
||||
| Hooks | `use-kebab-case.ts` | `camelCase` |
|
||||
| Utils/Lib | `kebab-case.ts` | `camelCase` |
|
||||
| Types | `kebab-case.ts` | `PascalCase` |
|
||||
| Constants | `constants.ts` | `SCREAMING_SNAKE_CASE` |
|
||||
| Location | File Naming | Export Naming |
|
||||
| ---------- | ------------------------------- | ---------------------- |
|
||||
| Components | `kebab-case.tsx` | `PascalCase` |
|
||||
| Dialogs | `*-dialog.tsx` or `*-modal.tsx` | `PascalCase` |
|
||||
| Hooks | `use-kebab-case.ts` | `camelCase` |
|
||||
| Utils/Lib | `kebab-case.ts` | `camelCase` |
|
||||
| Types | `kebab-case.ts` | `PascalCase` |
|
||||
| Constants | `constants.ts` | `SCREAMING_SNAKE_CASE` |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -125,10 +125,10 @@ Output messages streamed from providers:
|
||||
|
||||
```typescript
|
||||
export interface ProviderMessage {
|
||||
type: "assistant" | "user" | "error" | "result";
|
||||
subtype?: "success" | "error";
|
||||
type: 'assistant' | 'user' | 'error' | 'result';
|
||||
subtype?: 'success' | 'error';
|
||||
message?: {
|
||||
role: "user" | "assistant";
|
||||
role: 'user' | 'assistant';
|
||||
content: ContentBlock[];
|
||||
};
|
||||
result?: string;
|
||||
@@ -142,7 +142,7 @@ Individual content blocks in messages:
|
||||
|
||||
```typescript
|
||||
export interface ContentBlock {
|
||||
type: "text" | "tool_use" | "thinking" | "tool_result";
|
||||
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
name?: string;
|
||||
@@ -174,12 +174,14 @@ Uses `@anthropic-ai/claude-agent-sdk` for direct SDK integration.
|
||||
#### Model Detection
|
||||
|
||||
Routes models that:
|
||||
|
||||
- Start with `"claude-"` (e.g., `"claude-opus-4-5-20251101"`)
|
||||
- Are Claude aliases: `"opus"`, `"sonnet"`, `"haiku"`
|
||||
|
||||
#### Authentication
|
||||
|
||||
Requires:
|
||||
|
||||
- `ANTHROPIC_API_KEY` environment variable
|
||||
|
||||
#### Example Usage
|
||||
@@ -188,21 +190,21 @@ Requires:
|
||||
const provider = new ClaudeProvider();
|
||||
|
||||
const stream = provider.executeQuery({
|
||||
prompt: "What is 2+2?",
|
||||
model: "claude-opus-4-5-20251101",
|
||||
cwd: "/project/path",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
prompt: 'What is 2+2?',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
cwd: '/project/path',
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
maxTurns: 20,
|
||||
allowedTools: ["Read", "Write", "Bash"],
|
||||
allowedTools: ['Read', 'Write', 'Bash'],
|
||||
abortController: new AbortController(),
|
||||
conversationHistory: [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi! How can I help?" }
|
||||
]
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi! How can I help?' },
|
||||
],
|
||||
});
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant") {
|
||||
if (msg.type === 'assistant') {
|
||||
console.log(msg.message?.content);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +217,7 @@ Uses `convertHistoryToMessages()` utility to convert history to SDK format:
|
||||
```typescript
|
||||
const historyMessages = convertHistoryToMessages(conversationHistory);
|
||||
for (const msg of historyMessages) {
|
||||
yield msg; // Yield to SDK
|
||||
yield msg; // Yield to SDK
|
||||
}
|
||||
```
|
||||
|
||||
@@ -240,28 +242,31 @@ Spawns OpenAI Codex CLI as a subprocess and converts JSONL output to provider fo
|
||||
#### Model Detection
|
||||
|
||||
Routes models that:
|
||||
|
||||
- Start with `"gpt-"` (e.g., `"gpt-5.2"`, `"gpt-5.1-codex-max"`)
|
||||
- Start with `"o"` (e.g., `"o1"`, `"o1-mini"`)
|
||||
|
||||
#### Available Models
|
||||
|
||||
| Model | Description | Context | Max Output | Vision |
|
||||
|-------|-------------|---------|------------|--------|
|
||||
| `gpt-5.2` | Latest Codex model | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex-max` | Maximum capability | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex` | Standard Codex | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex-mini` | Lightweight | 256K | 16K | No |
|
||||
| `gpt-5.1` | General-purpose | 256K | 32K | Yes |
|
||||
| Model | Description | Context | Max Output | Vision |
|
||||
| -------------------- | ------------------ | ------- | ---------- | ------ |
|
||||
| `gpt-5.2` | Latest Codex model | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex-max` | Maximum capability | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex` | Standard Codex | 256K | 32K | Yes |
|
||||
| `gpt-5.1-codex-mini` | Lightweight | 256K | 16K | No |
|
||||
| `gpt-5.1` | General-purpose | 256K | 32K | Yes |
|
||||
|
||||
#### Authentication
|
||||
|
||||
Supports two methods:
|
||||
|
||||
1. **CLI login**: `codex login` (OAuth tokens stored in `~/.codex/auth.json`)
|
||||
2. **API key**: `OPENAI_API_KEY` environment variable
|
||||
|
||||
#### Installation Detection
|
||||
|
||||
Uses `CodexCliDetector` to check:
|
||||
|
||||
- PATH for `codex` command
|
||||
- npm global: `npm list -g @openai/codex`
|
||||
- Homebrew (macOS): `/opt/homebrew/bin/codex`
|
||||
@@ -273,17 +278,17 @@ Uses `CodexCliDetector` to check:
|
||||
const provider = new CodexProvider();
|
||||
|
||||
const stream = provider.executeQuery({
|
||||
prompt: "Fix the bug in main.ts",
|
||||
model: "gpt-5.2",
|
||||
cwd: "/project/path",
|
||||
systemPrompt: "You are an expert TypeScript developer.",
|
||||
abortController: new AbortController()
|
||||
prompt: 'Fix the bug in main.ts',
|
||||
model: 'gpt-5.2',
|
||||
cwd: '/project/path',
|
||||
systemPrompt: 'You are an expert TypeScript developer.',
|
||||
abortController: new AbortController(),
|
||||
});
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant") {
|
||||
if (msg.type === 'assistant') {
|
||||
console.log(msg.message?.content);
|
||||
} else if (msg.type === "error") {
|
||||
} else if (msg.type === 'error') {
|
||||
console.error(msg.error);
|
||||
}
|
||||
}
|
||||
@@ -293,15 +298,15 @@ for await (const msg of stream) {
|
||||
|
||||
Codex CLI outputs JSONL events that get converted to `ProviderMessage` format:
|
||||
|
||||
| Codex Event | Provider Message |
|
||||
|-------------|------------------|
|
||||
| `item.completed` (reasoning) | `{ type: "assistant", content: [{ type: "thinking" }] }` |
|
||||
| `item.completed` (agent_message) | `{ type: "assistant", content: [{ type: "text" }] }` |
|
||||
| `item.completed` (command_execution) | `{ type: "assistant", content: [{ type: "text", text: "```bash\n...\n```" }] }` |
|
||||
| `item.started` (command_execution) | `{ type: "assistant", content: [{ type: "tool_use" }] }` |
|
||||
| `item.updated` (todo_list) | `{ type: "assistant", content: [{ type: "text", text: "**Updated Todo List:**..." }] }` |
|
||||
| `thread.completed` | `{ type: "result", subtype: "success" }` |
|
||||
| `error` | `{ type: "error", error: "..." }` |
|
||||
| Codex Event | Provider Message |
|
||||
| ------------------------------------ | --------------------------------------------------------------------------------------- |
|
||||
| `item.completed` (reasoning) | `{ type: "assistant", content: [{ type: "thinking" }] }` |
|
||||
| `item.completed` (agent_message) | `{ type: "assistant", content: [{ type: "text" }] }` |
|
||||
| `item.completed` (command_execution) | `{ type: "assistant", content: [{ type: "text", text: "```bash\n...\n```" }] }` |
|
||||
| `item.started` (command_execution) | `{ type: "assistant", content: [{ type: "tool_use" }] }` |
|
||||
| `item.updated` (todo_list) | `{ type: "assistant", content: [{ type: "text", text: "**Updated Todo List:**..." }] }` |
|
||||
| `thread.completed` | `{ type: "result", subtype: "success" }` |
|
||||
| `error` | `{ type: "error", error: "..." }` |
|
||||
|
||||
#### Conversation History Handling
|
||||
|
||||
@@ -323,6 +328,7 @@ await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath);
|
||||
```
|
||||
|
||||
Generates `.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[mcp_servers.automaker-tools]
|
||||
command = "node"
|
||||
@@ -349,13 +355,12 @@ export class ProviderFactory {
|
||||
const lowerModel = modelId.toLowerCase();
|
||||
|
||||
// OpenAI/Codex models
|
||||
if (lowerModel.startsWith("gpt-") || lowerModel.startsWith("o")) {
|
||||
if (lowerModel.startsWith('gpt-') || lowerModel.startsWith('o')) {
|
||||
return new CodexProvider();
|
||||
}
|
||||
|
||||
// Claude models
|
||||
if (lowerModel.startsWith("claude-") ||
|
||||
["haiku", "sonnet", "opus"].includes(lowerModel)) {
|
||||
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
|
||||
return new ClaudeProvider();
|
||||
}
|
||||
|
||||
@@ -381,7 +386,7 @@ export class ProviderFactory {
|
||||
### Usage in Services
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||
import { ProviderFactory } from '../providers/provider-factory.js';
|
||||
|
||||
// In AgentService or AutoModeService
|
||||
const provider = ProviderFactory.getProviderForModel(model);
|
||||
@@ -401,17 +406,17 @@ for await (const msg of stream) {
|
||||
Create `apps/server/src/providers/[name]-provider.ts`:
|
||||
|
||||
```typescript
|
||||
import { BaseProvider } from "./base-provider.js";
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import type {
|
||||
ExecuteOptions,
|
||||
ProviderMessage,
|
||||
InstallationStatus,
|
||||
ModelDefinition,
|
||||
} from "./types.js";
|
||||
} from './types.js';
|
||||
|
||||
export class CursorProvider extends BaseProvider {
|
||||
getName(): string {
|
||||
return "cursor";
|
||||
return 'cursor';
|
||||
}
|
||||
|
||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||
@@ -429,23 +434,23 @@ export class CursorProvider extends BaseProvider {
|
||||
getAvailableModels(): ModelDefinition[] {
|
||||
return [
|
||||
{
|
||||
id: "cursor-premium",
|
||||
name: "Cursor Premium",
|
||||
modelString: "cursor-premium",
|
||||
provider: "cursor",
|
||||
id: 'cursor-premium',
|
||||
name: 'Cursor Premium',
|
||||
modelString: 'cursor-premium',
|
||||
provider: 'cursor',
|
||||
description: "Cursor's premium model",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
tier: "premium",
|
||||
tier: 'premium',
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
supportsFeature(feature: string): boolean {
|
||||
const supportedFeatures = ["tools", "text", "vision"];
|
||||
const supportedFeatures = ['tools', 'text', 'vision'];
|
||||
return supportedFeatures.includes(feature);
|
||||
}
|
||||
}
|
||||
@@ -499,6 +504,7 @@ Update `apps/server/src/routes/models.ts`:
|
||||
### Step 4: Done!
|
||||
|
||||
No changes needed in:
|
||||
|
||||
- ✅ AgentService
|
||||
- ✅ AutoModeService
|
||||
- ✅ Any business logic
|
||||
@@ -512,6 +518,7 @@ The provider architecture handles everything automatically.
|
||||
### SDK-Based Providers (like Claude)
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Direct SDK/library integration
|
||||
- No subprocess spawning
|
||||
- Native multi-turn support
|
||||
@@ -520,6 +527,7 @@ The provider architecture handles everything automatically.
|
||||
**Example**: ClaudeProvider using `@anthropic-ai/claude-agent-sdk`
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Lower latency
|
||||
- More control over options
|
||||
- Easier error handling
|
||||
@@ -530,6 +538,7 @@ The provider architecture handles everything automatically.
|
||||
### CLI-Based Providers (like Codex)
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Subprocess spawning
|
||||
- JSONL stream parsing
|
||||
- Text-based conversation history
|
||||
@@ -538,11 +547,13 @@ The provider architecture handles everything automatically.
|
||||
**Example**: CodexProvider using `codex exec --json`
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Access to CLI-only features
|
||||
- No SDK dependency
|
||||
- Can use any CLI tool
|
||||
|
||||
**Implementation Pattern**:
|
||||
|
||||
1. Use `spawnJSONLProcess()` from `subprocess-manager.ts`
|
||||
2. Convert JSONL events to `ProviderMessage` format
|
||||
3. Handle authentication (CLI login or API key)
|
||||
@@ -626,6 +637,7 @@ console.error(`[${this.getName()}Provider] Error:`, error);
|
||||
### 7. Installation Detection
|
||||
|
||||
Implement thorough detection:
|
||||
|
||||
- Check multiple installation methods
|
||||
- Verify authentication
|
||||
- Return detailed status
|
||||
@@ -659,16 +671,16 @@ Provide accurate model metadata:
|
||||
Test each provider method independently:
|
||||
|
||||
```typescript
|
||||
describe("ClaudeProvider", () => {
|
||||
it("should detect installation", async () => {
|
||||
describe('ClaudeProvider', () => {
|
||||
it('should detect installation', async () => {
|
||||
const provider = new ClaudeProvider();
|
||||
const status = await provider.detectInstallation();
|
||||
|
||||
expect(status.installed).toBe(true);
|
||||
expect(status.method).toBe("sdk");
|
||||
expect(status.method).toBe('sdk');
|
||||
});
|
||||
|
||||
it("should stream messages correctly", async () => {
|
||||
it('should stream messages correctly', async () => {
|
||||
const provider = new ClaudeProvider();
|
||||
const messages = [];
|
||||
|
||||
@@ -677,7 +689,7 @@ describe("ClaudeProvider", () => {
|
||||
}
|
||||
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
expect(messages[0].type).toBe("assistant");
|
||||
expect(messages[0].type).toBe('assistant');
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -687,9 +699,9 @@ describe("ClaudeProvider", () => {
|
||||
Test provider interaction with services:
|
||||
|
||||
```typescript
|
||||
describe("Provider Integration", () => {
|
||||
it("should work with AgentService", async () => {
|
||||
const provider = ProviderFactory.getProviderForModel("claude-opus-4-5-20251101");
|
||||
describe('Provider Integration', () => {
|
||||
it('should work with AgentService', async () => {
|
||||
const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101');
|
||||
|
||||
// Test full workflow
|
||||
});
|
||||
@@ -733,6 +745,7 @@ CODEX_CLI_PATH=/custom/path/to/codex
|
||||
**Problem**: Provider fails with auth error
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check environment variables
|
||||
2. For CLI providers, verify CLI login status
|
||||
3. Check `detectInstallation()` output
|
||||
@@ -742,6 +755,7 @@ CODEX_CLI_PATH=/custom/path/to/codex
|
||||
**Problem**: Failed to parse JSONL line
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check CLI output format
|
||||
2. Verify JSON is valid
|
||||
3. Add error handling for malformed lines
|
||||
@@ -751,6 +765,7 @@ CODEX_CLI_PATH=/custom/path/to/codex
|
||||
**Problem**: Subprocess hangs
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Increase timeout in `spawnJSONLProcess` options
|
||||
2. Check CLI process for hangs
|
||||
3. Verify abort signal handling
|
||||
@@ -778,6 +793,7 @@ Potential providers to add:
|
||||
- CLI or HTTP API
|
||||
|
||||
Each would follow the same pattern:
|
||||
|
||||
1. Create `[name]-provider.ts` implementing `BaseProvider`
|
||||
2. Add routing in `provider-factory.ts`
|
||||
3. Update models list
|
||||
|
||||
@@ -153,19 +153,16 @@ export function create{Module}Routes(events: EventEmitter): Router {
|
||||
**Pattern**:
|
||||
|
||||
```typescript
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
|
||||
const logger = createLogger("{ModuleName}");
|
||||
const logger = createLogger('{ModuleName}');
|
||||
|
||||
// Shared state
|
||||
export let isRunning = false;
|
||||
export let currentAbortController: AbortController | null = null;
|
||||
|
||||
// State management
|
||||
export function setRunningState(
|
||||
running: boolean,
|
||||
controller: AbortController | null = null
|
||||
): void {
|
||||
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
|
||||
isRunning = running;
|
||||
currentAbortController = controller;
|
||||
}
|
||||
@@ -176,7 +173,7 @@ export function logError(error: unknown, context: string): void {
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Unknown error";
|
||||
return error instanceof Error ? error.message : 'Unknown error';
|
||||
}
|
||||
```
|
||||
|
||||
@@ -289,13 +286,13 @@ export function create{Action}Handler(events: EventEmitter) {
|
||||
* {Brief description of what this function does}
|
||||
*/
|
||||
|
||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import { logAuthStatus } from "./common.js";
|
||||
import { anotherBusinessFunction } from "./another-business-function.js";
|
||||
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
import { logAuthStatus } from './common.js';
|
||||
import { anotherBusinessFunction } from './another-business-function.js';
|
||||
|
||||
const logger = createLogger("{ModuleName}");
|
||||
const logger = createLogger('{ModuleName}');
|
||||
|
||||
export async function businessLogicFunction(
|
||||
param1: string,
|
||||
@@ -303,7 +300,7 @@ export async function businessLogicFunction(
|
||||
events: EventEmitter,
|
||||
abortController: AbortController
|
||||
): Promise<void> {
|
||||
logger.debug("========== businessLogicFunction() started ==========");
|
||||
logger.debug('========== businessLogicFunction() started ==========');
|
||||
|
||||
try {
|
||||
// Business logic here
|
||||
@@ -312,9 +309,9 @@ export async function businessLogicFunction(
|
||||
// Can call other business logic functions
|
||||
await anotherBusinessFunction(param1, events, abortController);
|
||||
|
||||
logger.debug("========== businessLogicFunction() completed ==========");
|
||||
logger.debug('========== businessLogicFunction() completed ==========');
|
||||
} catch (error) {
|
||||
logger.error("❌ businessLogicFunction() failed:", error);
|
||||
logger.error('❌ businessLogicFunction() failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -458,7 +455,6 @@ The `app-spec` module demonstrates this pattern:
|
||||
### Migrating an Existing Route Module
|
||||
|
||||
1. **Analyze current structure**
|
||||
|
||||
- Identify all endpoints
|
||||
- Identify shared state/utilities
|
||||
- Identify large functions (>150 lines)
|
||||
@@ -470,31 +466,26 @@ The `app-spec` module demonstrates this pattern:
|
||||
```
|
||||
|
||||
3. **Extract common utilities**
|
||||
|
||||
- Move shared state to `common.ts`
|
||||
- Move utility functions to `common.ts`
|
||||
- Update imports in existing files
|
||||
|
||||
4. **Extract business logic**
|
||||
|
||||
- Identify functions to extract
|
||||
- Create `{function-name}.ts` files
|
||||
- Move logic, update imports
|
||||
|
||||
5. **Create route handlers**
|
||||
|
||||
- Create `routes/{endpoint-name}.ts` for each endpoint
|
||||
- Move HTTP handling logic
|
||||
- Keep handlers thin
|
||||
|
||||
6. **Create index.ts**
|
||||
|
||||
- Import route handlers
|
||||
- Register routes
|
||||
- Export router creation function
|
||||
|
||||
7. **Update main routes file**
|
||||
|
||||
- Import from new `index.ts`
|
||||
- Update route registration
|
||||
|
||||
@@ -509,11 +500,11 @@ The `app-spec` module demonstrates this pattern:
|
||||
|
||||
```typescript
|
||||
// routes.ts - 500+ lines
|
||||
router.post("/create", async (req, res) => {
|
||||
router.post('/create', async (req, res) => {
|
||||
// 200 lines of logic
|
||||
});
|
||||
|
||||
router.post("/generate", async (req, res) => {
|
||||
router.post('/generate', async (req, res) => {
|
||||
// 200 lines of similar logic
|
||||
});
|
||||
```
|
||||
@@ -580,6 +571,3 @@ The route organization pattern provides:
|
||||
5. **Testability** - Functions can be tested independently
|
||||
|
||||
Apply this pattern to all route modules for consistency and improved code quality.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ Centralized utilities for processing image files, including MIME type detection,
|
||||
Get MIME type for an image file based on its extension.
|
||||
|
||||
**Supported formats**:
|
||||
|
||||
- `.jpg`, `.jpeg` → `image/jpeg`
|
||||
- `.png` → `image/png`
|
||||
- `.gif` → `image/gif`
|
||||
@@ -38,10 +39,11 @@ Get MIME type for an image file based on its extension.
|
||||
- Default: `image/png`
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { getMimeTypeForImage } from "../lib/image-handler.js";
|
||||
|
||||
const mimeType = getMimeTypeForImage("/path/to/image.jpg");
|
||||
```typescript
|
||||
import { getMimeTypeForImage } from '../lib/image-handler.js';
|
||||
|
||||
const mimeType = getMimeTypeForImage('/path/to/image.jpg');
|
||||
// Returns: "image/jpeg"
|
||||
```
|
||||
|
||||
@@ -52,21 +54,23 @@ const mimeType = getMimeTypeForImage("/path/to/image.jpg");
|
||||
Read an image file and convert to base64 with metadata.
|
||||
|
||||
**Returns**: `ImageData`
|
||||
|
||||
```typescript
|
||||
interface ImageData {
|
||||
base64: string; // Base64-encoded image data
|
||||
mimeType: string; // MIME type
|
||||
filename: string; // File basename
|
||||
originalPath: string; // Original file path
|
||||
base64: string; // Base64-encoded image data
|
||||
mimeType: string; // MIME type
|
||||
filename: string; // File basename
|
||||
originalPath: string; // Original file path
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const imageData = await readImageAsBase64("/path/to/photo.png");
|
||||
console.log(imageData.base64); // "iVBORw0KG..."
|
||||
console.log(imageData.mimeType); // "image/png"
|
||||
console.log(imageData.filename); // "photo.png"
|
||||
const imageData = await readImageAsBase64('/path/to/photo.png');
|
||||
console.log(imageData.base64); // "iVBORw0KG..."
|
||||
console.log(imageData.mimeType); // "image/png"
|
||||
console.log(imageData.filename); // "photo.png"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -76,15 +80,17 @@ console.log(imageData.filename); // "photo.png"
|
||||
Convert image paths to content blocks in Claude SDK format. Handles both relative and absolute paths.
|
||||
|
||||
**Parameters**:
|
||||
|
||||
- `imagePaths` - Array of image file paths
|
||||
- `workDir` - Optional working directory for resolving relative paths
|
||||
|
||||
**Returns**: Array of `ImageContentBlock`
|
||||
|
||||
```typescript
|
||||
interface ImageContentBlock {
|
||||
type: "image";
|
||||
type: 'image';
|
||||
source: {
|
||||
type: "base64";
|
||||
type: 'base64';
|
||||
media_type: string;
|
||||
data: string;
|
||||
};
|
||||
@@ -92,17 +98,15 @@ interface ImageContentBlock {
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const imageBlocks = await convertImagesToContentBlocks(
|
||||
["./screenshot.png", "/absolute/path/diagram.jpg"],
|
||||
"/project/root"
|
||||
['./screenshot.png', '/absolute/path/diagram.jpg'],
|
||||
'/project/root'
|
||||
);
|
||||
|
||||
// Use in prompt content
|
||||
const contentBlocks = [
|
||||
{ type: "text", text: "Analyze these images:" },
|
||||
...imageBlocks
|
||||
];
|
||||
const contentBlocks = [{ type: 'text', text: 'Analyze these images:' }, ...imageBlocks];
|
||||
```
|
||||
|
||||
---
|
||||
@@ -114,10 +118,11 @@ Format image paths as a bulleted list for inclusion in text prompts.
|
||||
**Returns**: Formatted string with image paths, or empty string if no images.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const pathsList = formatImagePathsForPrompt([
|
||||
"/screenshots/login.png",
|
||||
"/diagrams/architecture.png"
|
||||
'/screenshots/login.png',
|
||||
'/diagrams/architecture.png',
|
||||
]);
|
||||
|
||||
// Returns:
|
||||
@@ -139,39 +144,44 @@ Standardized prompt building that combines text prompts with image attachments.
|
||||
Build a prompt with optional image attachments.
|
||||
|
||||
**Parameters**:
|
||||
|
||||
- `basePrompt` - The text prompt
|
||||
- `imagePaths` - Optional array of image file paths
|
||||
- `workDir` - Optional working directory for resolving relative paths
|
||||
- `includeImagePaths` - Whether to append image paths to the text (default: false)
|
||||
|
||||
**Returns**: `PromptWithImages`
|
||||
|
||||
```typescript
|
||||
interface PromptWithImages {
|
||||
content: PromptContent; // string | Array<ContentBlock>
|
||||
content: PromptContent; // string | Array<ContentBlock>
|
||||
hasImages: boolean;
|
||||
}
|
||||
|
||||
type PromptContent = string | Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
source?: object;
|
||||
}>;
|
||||
type PromptContent =
|
||||
| string
|
||||
| Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
source?: object;
|
||||
}>;
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||
import { buildPromptWithImages } from '../lib/prompt-builder.js';
|
||||
|
||||
// Without images
|
||||
const { content } = await buildPromptWithImages("What is 2+2?");
|
||||
const { content } = await buildPromptWithImages('What is 2+2?');
|
||||
// content: "What is 2+2?" (simple string)
|
||||
|
||||
// With images
|
||||
const { content, hasImages } = await buildPromptWithImages(
|
||||
"Analyze this screenshot",
|
||||
["/path/to/screenshot.png"],
|
||||
"/project/root",
|
||||
true // include image paths in text
|
||||
'Analyze this screenshot',
|
||||
['/path/to/screenshot.png'],
|
||||
'/project/root',
|
||||
true // include image paths in text
|
||||
);
|
||||
// content: [
|
||||
// { type: "text", text: "Analyze this screenshot\n\nAttached images:\n- /path/to/screenshot.png\n" },
|
||||
@@ -181,6 +191,7 @@ const { content, hasImages } = await buildPromptWithImages(
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- **AgentService**: Set `includeImagePaths: true` to list paths for Read tool access
|
||||
- **AutoModeService**: Set `includeImagePaths: false` to avoid duplication in feature descriptions
|
||||
|
||||
@@ -200,9 +211,9 @@ Model alias mapping for Claude models.
|
||||
|
||||
```typescript
|
||||
export const CLAUDE_MODEL_MAP: Record<string, string> = {
|
||||
haiku: "claude-haiku-4-5",
|
||||
sonnet: "claude-sonnet-4-20250514",
|
||||
opus: "claude-opus-4-5-20251101",
|
||||
haiku: 'claude-haiku-4-5',
|
||||
sonnet: 'claude-sonnet-4-20250514',
|
||||
opus: 'claude-opus-4-5-20251101',
|
||||
} as const;
|
||||
```
|
||||
|
||||
@@ -212,8 +223,8 @@ Default models per provider.
|
||||
|
||||
```typescript
|
||||
export const DEFAULT_MODELS = {
|
||||
claude: "claude-opus-4-5-20251101",
|
||||
openai: "gpt-5.2",
|
||||
claude: 'claude-opus-4-5-20251101',
|
||||
openai: 'gpt-5.2',
|
||||
} as const;
|
||||
```
|
||||
|
||||
@@ -224,6 +235,7 @@ export const DEFAULT_MODELS = {
|
||||
Resolve a model key/alias to a full model string.
|
||||
|
||||
**Logic**:
|
||||
|
||||
1. If `modelKey` is undefined → return `defaultModel`
|
||||
2. If starts with `"gpt-"` or `"o"` → pass through (OpenAI/Codex model)
|
||||
3. If includes `"claude-"` → pass through (full Claude model string)
|
||||
@@ -231,22 +243,23 @@ Resolve a model key/alias to a full model string.
|
||||
5. Otherwise → return `defaultModel` with warning
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
||||
|
||||
resolveModelString("opus");
|
||||
```typescript
|
||||
import { resolveModelString, DEFAULT_MODELS } from '../lib/model-resolver.js';
|
||||
|
||||
resolveModelString('opus');
|
||||
// Returns: "claude-opus-4-5-20251101"
|
||||
// Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-5-20251101""
|
||||
|
||||
resolveModelString("gpt-5.2");
|
||||
resolveModelString('gpt-5.2');
|
||||
// Returns: "gpt-5.2"
|
||||
// Logs: "[ModelResolver] Using OpenAI/Codex model: gpt-5.2"
|
||||
|
||||
resolveModelString("claude-sonnet-4-20250514");
|
||||
resolveModelString('claude-sonnet-4-20250514');
|
||||
// Returns: "claude-sonnet-4-20250514"
|
||||
// Logs: "[ModelResolver] Using full Claude model string: claude-sonnet-4-20250514"
|
||||
|
||||
resolveModelString("invalid-model");
|
||||
resolveModelString('invalid-model');
|
||||
// Returns: "claude-opus-4-5-20251101"
|
||||
// Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-5-20251101""
|
||||
```
|
||||
@@ -260,19 +273,20 @@ Get the effective model from multiple sources with priority.
|
||||
**Priority**: explicit model > session model > default model
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { getEffectiveModel } from "../lib/model-resolver.js";
|
||||
import { getEffectiveModel } from '../lib/model-resolver.js';
|
||||
|
||||
// Explicit model takes precedence
|
||||
getEffectiveModel("sonnet", "opus");
|
||||
getEffectiveModel('sonnet', 'opus');
|
||||
// Returns: "claude-sonnet-4-20250514"
|
||||
|
||||
// Falls back to session model
|
||||
getEffectiveModel(undefined, "haiku");
|
||||
getEffectiveModel(undefined, 'haiku');
|
||||
// Returns: "claude-haiku-4-5"
|
||||
|
||||
// Falls back to default
|
||||
getEffectiveModel(undefined, undefined, "gpt-5.2");
|
||||
getEffectiveModel(undefined, undefined, 'gpt-5.2');
|
||||
// Returns: "gpt-5.2"
|
||||
```
|
||||
|
||||
@@ -287,10 +301,10 @@ Standardized conversation history processing for both SDK-based and CLI-based pr
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
import type { ConversationMessage } from "../providers/types.js";
|
||||
import type { ConversationMessage } from '../providers/types.js';
|
||||
|
||||
interface ConversationMessage {
|
||||
role: "user" | "assistant";
|
||||
role: 'user' | 'assistant';
|
||||
content: string | Array<{ type: string; text?: string; source?: object }>;
|
||||
}
|
||||
```
|
||||
@@ -302,6 +316,7 @@ interface ConversationMessage {
|
||||
Extract plain text from message content (handles both string and array formats).
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { extractTextFromContent } from "../lib/conversation-utils.js";
|
||||
|
||||
@@ -325,13 +340,14 @@ extractTextFromContent([
|
||||
Normalize message content to array format.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// String → array
|
||||
normalizeContentBlocks("Hello");
|
||||
normalizeContentBlocks('Hello');
|
||||
// Returns: [{ type: "text", text: "Hello" }]
|
||||
|
||||
// Array → pass through
|
||||
normalizeContentBlocks([{ type: "text", text: "Hello" }]);
|
||||
normalizeContentBlocks([{ type: 'text', text: 'Hello' }]);
|
||||
// Returns: [{ type: "text", text: "Hello" }]
|
||||
```
|
||||
|
||||
@@ -344,10 +360,11 @@ Format conversation history as plain text for CLI-based providers (e.g., Codex).
|
||||
**Returns**: Formatted text with role labels, or empty string if no history.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const history = [
|
||||
{ role: "user", content: "What is 2+2?" },
|
||||
{ role: "assistant", content: "2+2 equals 4." }
|
||||
{ role: 'user', content: 'What is 2+2?' },
|
||||
{ role: 'assistant', content: '2+2 equals 4.' },
|
||||
];
|
||||
|
||||
const formatted = formatHistoryAsText(history);
|
||||
@@ -372,10 +389,11 @@ Convert conversation history to Claude SDK message format.
|
||||
**Returns**: Array of SDK-formatted messages ready to yield in async generator.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const history = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" }
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there!' },
|
||||
];
|
||||
|
||||
const messages = convertHistoryToMessages(history);
|
||||
@@ -413,7 +431,7 @@ Standardized error classification and handling utilities.
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
export type ErrorType = "authentication" | "abort" | "execution" | "unknown";
|
||||
export type ErrorType = 'authentication' | 'abort' | 'execution' | 'unknown';
|
||||
|
||||
export interface ErrorInfo {
|
||||
type: ErrorType;
|
||||
@@ -431,14 +449,15 @@ export interface ErrorInfo {
|
||||
Check if an error is an abort/cancellation error.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { isAbortError } from "../lib/error-handler.js";
|
||||
import { isAbortError } from '../lib/error-handler.js';
|
||||
|
||||
try {
|
||||
// ... operation
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
console.log("Operation was cancelled");
|
||||
console.log('Operation was cancelled');
|
||||
return { success: false, aborted: true };
|
||||
}
|
||||
}
|
||||
@@ -451,15 +470,17 @@ try {
|
||||
Check if an error is an authentication/API key error.
|
||||
|
||||
**Detects**:
|
||||
|
||||
- "Authentication failed"
|
||||
- "Invalid API key"
|
||||
- "authentication_failed"
|
||||
- "Fix external API key"
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
if (isAuthenticationError(error.message)) {
|
||||
console.error("Please check your API key configuration");
|
||||
console.error('Please check your API key configuration');
|
||||
}
|
||||
```
|
||||
|
||||
@@ -470,8 +491,9 @@ if (isAuthenticationError(error.message)) {
|
||||
Classify an error into a specific type.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { classifyError } from "../lib/error-handler.js";
|
||||
import { classifyError } from '../lib/error-handler.js';
|
||||
|
||||
try {
|
||||
// ... operation
|
||||
@@ -479,13 +501,13 @@ try {
|
||||
const errorInfo = classifyError(error);
|
||||
|
||||
switch (errorInfo.type) {
|
||||
case "authentication":
|
||||
case 'authentication':
|
||||
// Handle auth errors
|
||||
break;
|
||||
case "abort":
|
||||
case 'abort':
|
||||
// Handle cancellation
|
||||
break;
|
||||
case "execution":
|
||||
case 'execution':
|
||||
// Handle other errors
|
||||
break;
|
||||
}
|
||||
@@ -499,6 +521,7 @@ try {
|
||||
Get a user-friendly error message.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
// ... operation
|
||||
@@ -527,7 +550,7 @@ export interface SubprocessOptions {
|
||||
cwd: string;
|
||||
env?: Record<string, string>;
|
||||
abortController?: AbortController;
|
||||
timeout?: number; // Milliseconds of no output before timeout
|
||||
timeout?: number; // Milliseconds of no output before timeout
|
||||
}
|
||||
|
||||
export interface SubprocessResult {
|
||||
@@ -544,6 +567,7 @@ export interface SubprocessResult {
|
||||
Spawns a subprocess and streams JSONL output line-by-line.
|
||||
|
||||
**Features**:
|
||||
|
||||
- Parses each line as JSON
|
||||
- Handles abort signals
|
||||
- 30-second timeout detection for hanging processes
|
||||
@@ -551,20 +575,21 @@ Spawns a subprocess and streams JSONL output line-by-line.
|
||||
- Continues processing other lines if one fails to parse
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { spawnJSONLProcess } from "../lib/subprocess-manager.js";
|
||||
import { spawnJSONLProcess } from '../lib/subprocess-manager.js';
|
||||
|
||||
const stream = spawnJSONLProcess({
|
||||
command: "codex",
|
||||
args: ["exec", "--model", "gpt-5.2", "--json", "--full-auto", "Fix the bug"],
|
||||
cwd: "/project/path",
|
||||
env: { OPENAI_API_KEY: "sk-..." },
|
||||
command: 'codex',
|
||||
args: ['exec', '--model', 'gpt-5.2', '--json', '--full-auto', 'Fix the bug'],
|
||||
cwd: '/project/path',
|
||||
env: { OPENAI_API_KEY: 'sk-...' },
|
||||
abortController: new AbortController(),
|
||||
timeout: 30000
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
for await (const event of stream) {
|
||||
console.log("Received event:", event);
|
||||
console.log('Received event:', event);
|
||||
// Process JSONL events
|
||||
}
|
||||
```
|
||||
@@ -576,15 +601,16 @@ for await (const event of stream) {
|
||||
Spawns a subprocess and collects all output.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
const result = await spawnProcess({
|
||||
command: "git",
|
||||
args: ["status"],
|
||||
cwd: "/project/path"
|
||||
command: 'git',
|
||||
args: ['status'],
|
||||
cwd: '/project/path',
|
||||
});
|
||||
|
||||
console.log(result.stdout); // Git status output
|
||||
console.log(result.exitCode); // 0 for success
|
||||
console.log(result.stdout); // Git status output
|
||||
console.log(result.exitCode); // 0 for success
|
||||
```
|
||||
|
||||
---
|
||||
@@ -645,10 +671,10 @@ Always use `.js` extension in imports for ESM compatibility:
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||
import { buildPromptWithImages } from '../lib/prompt-builder.js';
|
||||
|
||||
// ❌ Incorrect
|
||||
import { buildPromptWithImages } from "../lib/prompt-builder";
|
||||
import { buildPromptWithImages } from '../lib/prompt-builder';
|
||||
```
|
||||
|
||||
---
|
||||
@@ -662,11 +688,12 @@ When writing tests for utilities:
|
||||
3. **Mock external dependencies** - File system, child processes
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
describe("image-handler", () => {
|
||||
it("should detect MIME type correctly", () => {
|
||||
expect(getMimeTypeForImage("photo.jpg")).toBe("image/jpeg");
|
||||
expect(getMimeTypeForImage("diagram.png")).toBe("image/png");
|
||||
describe('image-handler', () => {
|
||||
it('should detect MIME type correctly', () => {
|
||||
expect(getMimeTypeForImage('photo.jpg')).toBe('image/jpeg');
|
||||
expect(getMimeTypeForImage('diagram.png')).toBe('image/png');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -32,12 +32,12 @@ When password protection is enabled:
|
||||
|
||||
When the terminal is focused, the following shortcuts are available:
|
||||
|
||||
| Shortcut | Action |
|
||||
| -------- | ---------------------------------------- |
|
||||
| `Alt+T` | Open new terminal tab |
|
||||
| `Alt+D` | Split terminal right (horizontal split) |
|
||||
| `Alt+S` | Split terminal down (vertical split) |
|
||||
| `Alt+W` | Close current terminal |
|
||||
| Shortcut | Action |
|
||||
| -------- | --------------------------------------- |
|
||||
| `Alt+T` | Open new terminal tab |
|
||||
| `Alt+D` | Split terminal right (horizontal split) |
|
||||
| `Alt+S` | Split terminal down (vertical split) |
|
||||
| `Alt+W` | Close current terminal |
|
||||
|
||||
These shortcuts are customizable via the keyboard shortcuts settings (Settings > Keyboard Shortcuts).
|
||||
|
||||
@@ -45,12 +45,12 @@ These shortcuts are customizable via the keyboard shortcuts settings (Settings >
|
||||
|
||||
Navigate between terminal panes using directional shortcuts:
|
||||
|
||||
| Shortcut | Action |
|
||||
| --------------------------------- | ----------------------------------- |
|
||||
| `Ctrl+Alt+ArrowUp` (or `Cmd+Alt`) | Move focus to terminal pane above |
|
||||
| `Ctrl+Alt+ArrowDown` | Move focus to terminal pane below |
|
||||
| `Ctrl+Alt+ArrowLeft` | Move focus to terminal pane on left |
|
||||
| `Ctrl+Alt+ArrowRight` | Move focus to terminal pane on right|
|
||||
| Shortcut | Action |
|
||||
| --------------------------------- | ------------------------------------ |
|
||||
| `Ctrl+Alt+ArrowUp` (or `Cmd+Alt`) | Move focus to terminal pane above |
|
||||
| `Ctrl+Alt+ArrowDown` | Move focus to terminal pane below |
|
||||
| `Ctrl+Alt+ArrowLeft` | Move focus to terminal pane on left |
|
||||
| `Ctrl+Alt+ArrowRight` | Move focus to terminal pane on right |
|
||||
|
||||
The navigation is spatially aware - pressing Down will move to the terminal below your current one, not just cycle through terminals in order.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user