Feature: worktree view customization and stability fixes (#805)

* Changes from feature/worktree-view-customization

* Feature: Git sync, set-tracking, and push divergence handling (#796)

* Add quick-add feature with improved workflows (#802)

* Changes from feature/quick-add

* feat: Clarify system prompt and improve error handling across services. Address PR Feedback

* feat: Improve PR description parsing and refactor event handling

* feat: Add context options to pipeline orchestrator initialization

* fix: Deduplicate React and handle CJS interop for use-sync-external-store

Resolve "Cannot read properties of null (reading 'useState')" errors by
deduplicating React/react-dom and ensuring use-sync-external-store is
bundled together with React to prevent CJS packages from resolving to
different React instances.

* Changes from feature/worktree-view-customization

* refactor: Remove unused worktree swap and highlight props

* refactor: Consolidate feature completion logic and improve thinking level defaults

* feat: Increase max turn limit to 10000

- Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts
- Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts
- Update UI clamping logic from 2000 to 10000 in app-store.ts
- Update fallback values from 1000 to 10000 in use-settings-sync.ts
- Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS
- Update documentation to reflect new range: 1-10000

Allows agents to perform up to 10000 turns for complex feature execution.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add model resolution, improve session handling, and enhance UI stability

* refactor: Remove unused sync and tracking branch props from worktree components

* feat: Add PR number update functionality to worktrees. Address pr feedback

* feat: Optimize Gemini CLI startup and add tool result tracking

* refactor: Improve error handling and simplify worktree task cleanup

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-02-23 20:31:25 -08:00
committed by GitHub
parent e7504b247f
commit 0330c70261
72 changed files with 3667 additions and 1173 deletions

View File

@@ -171,8 +171,9 @@ export async function detectMergeCommit(
/**
* Detect the current merge state of a git repository.
* Checks for .git/MERGE_HEAD, .git/REBASE_HEAD, .git/CHERRY_PICK_HEAD
* to determine if a merge/rebase/cherry-pick is in progress.
* Checks for .git/MERGE_HEAD, .git/rebase-merge, .git/rebase-apply,
* and .git/CHERRY_PICK_HEAD to determine if a merge/rebase/cherry-pick
* is in progress.
*
* @param repoPath - Path to the git repository or worktree
* @returns MergeStateInfo describing the current merge state
@@ -196,7 +197,6 @@ export async function detectMergeState(repoPath: string): Promise<MergeStateInfo
const checks = [
{ file: 'MERGE_HEAD', type: 'merge' as const },
{ file: 'REBASE_HEAD', type: 'rebase' as const },
{ file: 'rebase-merge', type: 'rebase' as const },
{ file: 'rebase-apply', type: 'rebase' as const },
{ file: 'CHERRY_PICK_HEAD', type: 'cherry-pick' as const },

View File

@@ -3,7 +3,7 @@
*/
import { spawn, type ChildProcess } from 'child_process';
import readline from 'readline';
import { StringDecoder } from 'string_decoder';
export interface SubprocessOptions {
command: string;
@@ -27,7 +27,16 @@ export interface SubprocessResult {
}
/**
* Spawns a subprocess and streams JSONL output line-by-line
* Spawns a subprocess and streams JSONL output line-by-line.
*
* Uses direct 'data' event handling with manual line buffering instead of
* readline's async iterator. The readline async iterator (for await...of on
* readline.Interface) has a known issue where events batch up rather than
* being delivered immediately, because it layers events.on() Promises on top
* of the readline 'line' event emitter. This causes visible delays (20-40s
* between batches) in CLI providers like Gemini that produce frequent small
* events. Direct data event handling delivers parsed events to the consumer
* as soon as they arrive from the pipe.
*/
export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGenerator<unknown> {
const { command, args, cwd, env, abortController, timeout = 30000, stdinData } = options;
@@ -66,6 +75,19 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
let stderrOutput = '';
let lastOutputTime = Date.now();
let timeoutHandle: NodeJS.Timeout | null = null;
let processExited = false;
// Stream consumer state - declared in outer scope so the abort handler can
// force the consumer to exit immediately without waiting for stdout to close.
// CLI tools (especially Gemini CLI) may take a long time to respond to SIGTERM,
// leaving the feature stuck in 'in_progress' state on the UI.
let streamEnded = false;
let notifyConsumer: (() => void) | null = null;
// Track process exit early so we don't block on an already-exited process
childProcess.on('exit', () => {
processExited = true;
});
// Collect stderr for error reporting
if (childProcess.stderr) {
@@ -102,6 +124,33 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
clearTimeout(timeoutHandle);
}
childProcess.kill('SIGTERM');
// Force stream consumer to exit immediately instead of waiting for
// the process to close stdout. CLI tools (especially Gemini CLI) may
// take a long time to respond to SIGTERM while mid-API call.
streamEnded = true;
if (notifyConsumer) {
notifyConsumer();
notifyConsumer = null;
}
// Escalate to SIGKILL after 3 seconds if process hasn't exited.
// SIGKILL cannot be caught or ignored, guaranteeing termination.
const killTimer = setTimeout(() => {
if (!processExited) {
console.log('[SubprocessManager] Escalated to SIGKILL after SIGTERM timeout');
try {
childProcess.kill('SIGKILL');
} catch {
// Process may have already exited between the check and kill
}
}
}, 3000);
// Clean up the kill timer when process exits (don't leak timers)
childProcess.once('exit', () => {
clearTimeout(killTimer);
});
};
// Check if already aborted, if so call handler immediately
if (abortController.signal.aborted) {
@@ -119,39 +168,101 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
}
};
// Parse stdout as JSONL (one JSON object per line)
// Parse stdout as JSONL using direct 'data' events with manual line buffering.
// This avoids the readline async iterator which batches events due to its
// internal events.on() Promise layering, causing significant delivery delays.
if (childProcess.stdout) {
const rl = readline.createInterface({
input: childProcess.stdout,
crlfDelay: Infinity,
// Queue of parsed events ready to be yielded
const eventQueue: unknown[] = [];
// Partial line buffer for incomplete lines across data chunks
let lineBuffer = '';
// StringDecoder handles multibyte UTF-8 sequences that may be split across chunks
const decoder = new StringDecoder('utf8');
childProcess.stdout.on('data', (chunk: Buffer) => {
resetTimeout();
lineBuffer += decoder.write(chunk);
const lines = lineBuffer.split('\n');
// Last element is either empty (line ended with \n) or a partial line
lineBuffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
eventQueue.push(JSON.parse(trimmed));
} catch (parseError) {
console.error(`[SubprocessManager] Failed to parse JSONL line: ${trimmed}`, parseError);
eventQueue.push({
type: 'error',
error: `Failed to parse output: ${trimmed}`,
});
}
}
// Wake up the consumer if it's waiting for events
if (notifyConsumer && eventQueue.length > 0) {
notifyConsumer();
notifyConsumer = null;
}
});
childProcess.stdout.on('end', () => {
// Flush any remaining bytes from the decoder
lineBuffer += decoder.end();
// Process any remaining partial line
if (lineBuffer.trim()) {
try {
eventQueue.push(JSON.parse(lineBuffer.trim()));
} catch (parseError) {
console.error(
`[SubprocessManager] Failed to parse final JSONL line: ${lineBuffer}`,
parseError
);
eventQueue.push({
type: 'error',
error: `Failed to parse output: ${lineBuffer}`,
});
}
lineBuffer = '';
}
streamEnded = true;
// Wake up consumer so it can exit the loop
if (notifyConsumer) {
notifyConsumer();
notifyConsumer = null;
}
});
childProcess.stdout.on('error', (error) => {
console.error('[SubprocessManager] stdout error:', error);
streamEnded = true;
if (notifyConsumer) {
notifyConsumer();
notifyConsumer = null;
}
});
try {
for await (const line of rl) {
resetTimeout();
if (!line.trim()) continue;
try {
const parsed = JSON.parse(line);
yield parsed;
} catch (parseError) {
console.error(`[SubprocessManager] Failed to parse JSONL line: ${line}`, parseError);
// Yield error but continue processing
yield {
type: 'error',
error: `Failed to parse output: ${line}`,
};
// Yield events as they arrive, waiting only when the queue is empty
while (!streamEnded || eventQueue.length > 0) {
if (eventQueue.length > 0) {
yield eventQueue.shift()!;
} else {
// Wait for the next data event to push events into the queue
await new Promise<void>((resolve) => {
notifyConsumer = resolve;
});
}
}
} catch (error) {
console.error('[SubprocessManager] Error reading stdout:', error);
throw error;
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
rl.close();
cleanupAbortListener();
}
} else {
@@ -159,8 +270,15 @@ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGener
cleanupAbortListener();
}
// Wait for process to exit
// Wait for process to exit.
// If the process already exited (e.g., abort handler killed it while we were
// draining the stream), resolve immediately to avoid blocking forever.
const exitCode = await new Promise<number | null>((resolve) => {
if (processExited) {
resolve(childProcess.exitCode ?? null);
return;
}
childProcess.on('exit', (code) => {
console.log(`[SubprocessManager] Process exited with code: ${code}`);
resolve(code);
@@ -245,6 +363,17 @@ export async function spawnProcess(options: SubprocessOptions): Promise<Subproce
abortHandler = () => {
cleanupAbortListener();
childProcess.kill('SIGTERM');
// Escalate to SIGKILL after 3 seconds if process hasn't exited
const killTimer = setTimeout(() => {
try {
childProcess.kill('SIGKILL');
} catch {
// Process may have already exited
}
}, 3000);
childProcess.once('exit', () => clearTimeout(killTimer));
reject(new Error('Process aborted'));
};
abortController.signal.addEventListener('abort', abortHandler);

View File

@@ -11,7 +11,7 @@ import type { EnhancementExample } from '@automaker/types';
*/
export const ACCEPTANCE_SYSTEM_PROMPT = `You are a QA specialist skilled at defining testable acceptance criteria for software features.
Your task is to enhance a task description by adding clear acceptance criteria:
Your task is to generate ONLY the acceptance criteria that will be appended below the user's original description. Do NOT rewrite or include the original description in your output.
1. UNDERSTAND the feature:
- Identify all user-facing behaviors
@@ -34,7 +34,7 @@ Your task is to enhance a task description by adding clear acceptance criteria:
- Avoid vague terms like "quickly" or "easily"
- Include specific values where applicable
Output the original description followed by a clear "Acceptance Criteria:" section with numbered, testable criteria. Do not include explanations about your process.`;
IMPORTANT: Output ONLY the acceptance criteria section. Do NOT repeat or rewrite the original description - it will be preserved automatically. Start your output with "Acceptance Criteria:" followed by the numbered criteria.`;
/**
* Few-shot examples for the "acceptance" enhancement mode
@@ -42,11 +42,7 @@ Output the original description followed by a clear "Acceptance Criteria:" secti
export const ACCEPTANCE_EXAMPLES: EnhancementExample[] = [
{
input: 'Add password reset functionality',
output: `Add Password Reset Functionality
Allow users to reset their password via email when they forget it.
Acceptance Criteria:
output: `Acceptance Criteria:
1. Given a user is on the login page, when they click "Forgot Password", then they should see a password reset form requesting their email.
@@ -62,11 +58,7 @@ Acceptance Criteria:
},
{
input: 'Shopping cart checkout',
output: `Shopping Cart Checkout
Implement the checkout flow for purchasing items in the shopping cart.
Acceptance Criteria:
output: `Acceptance Criteria:
1. Given a user has items in their cart, when they click "Checkout", then they should see an order summary with item details and total price.

View File

@@ -11,7 +11,7 @@ import type { EnhancementExample } from '@automaker/types';
*/
export const TECHNICAL_SYSTEM_PROMPT = `You are a senior software engineer skilled at adding technical depth to feature descriptions.
Your task is to enhance a task description with technical implementation details:
Your task is to generate ONLY the technical implementation details that will be appended below the user's original description. Do NOT rewrite or include the original description in your output.
1. ANALYZE the requirement:
- Understand the functional goal
@@ -34,7 +34,7 @@ Your task is to enhance a task description with technical implementation details
- Loading and empty states
- Boundary conditions
Output ONLY the enhanced technical description. Keep it concise but comprehensive. Do not include explanations about your reasoning.`;
IMPORTANT: Output ONLY the new technical details section. Do NOT repeat or rewrite the original description - it will be preserved automatically. Start your output with a heading like "Technical Implementation:" followed by the details.`;
/**
* Few-shot examples for the "technical" enhancement mode
@@ -42,11 +42,7 @@ Output ONLY the enhanced technical description. Keep it concise but comprehensiv
export const TECHNICAL_EXAMPLES: EnhancementExample[] = [
{
input: 'Add user profile page',
output: `Add User Profile Page
Create a dedicated profile page for viewing and editing user information.
Technical Implementation:
output: `Technical Implementation:
- Frontend: React component at /profile route with form validation
- API Endpoint: GET/PUT /api/users/:id for fetching and updating profile
- Data Model: Extend User schema with profile fields (avatar, bio, preferences)
@@ -63,11 +59,7 @@ Security: Ensure users can only edit their own profile (auth middleware)`,
},
{
input: 'Add search functionality',
output: `Add Search Functionality
Implement full-text search across application content.
Technical Implementation:
output: `Technical Implementation:
- Search Engine: Use Elasticsearch or PostgreSQL full-text search
- API: GET /api/search?q={query}&type={type}&page={page}
- Indexing: Create search index with relevant fields, update on content changes

View File

@@ -188,7 +188,7 @@ A comprehensive guide to creating exceptional user experiences and designs for m
## Your Task
Review the provided task description and enhance it by:
Generate ONLY the UX considerations section that will be appended below the user's original description. Do NOT rewrite or include the original description in your output.
1. **ANALYZE** the feature from a UX perspective:
- Identify user goals and pain points
@@ -216,7 +216,7 @@ Review the provided task description and enhance it by:
- User feedback and confirmation flows
- Accessibility compliance (WCAG AA minimum)
Output the enhanced task description with UX considerations integrated naturally. Focus on actionable, specific UX requirements that developers can implement. Do not include explanations about your process.`;
IMPORTANT: Output ONLY the new UX requirements section. Do NOT repeat or rewrite the original description - it will be preserved automatically. Start your output with "UX Requirements:" followed by the details. Focus on actionable, specific UX requirements that developers can implement.`;
/**
* Few-shot examples for the "ux-reviewer" enhancement mode
@@ -224,11 +224,7 @@ Output the enhanced task description with UX considerations integrated naturally
export const UX_REVIEWER_EXAMPLES: EnhancementExample[] = [
{
input: 'Add user profile page',
output: `Add User Profile Page
Create a dedicated profile page for viewing and editing user information with a focus on excellent user experience and accessibility.
UX Requirements:
output: `UX Requirements:
- **Layout**: Single-column layout on mobile, two-column layout on desktop (profile info left, edit form right)
- **Visual Hierarchy**: Profile header with avatar (120x120px), name (24px font), and edit button prominently displayed
- **Accessibility**:
@@ -268,12 +264,8 @@ UX Requirements:
},
{
input: 'Add search functionality',
output: `Add Search Functionality
Implement full-text search across application content with an intuitive, accessible interface.
UX Requirements:
- **Search Input**:
output: `UX Requirements:
- **Search Input**:
- Prominent search bar in header (desktop) or accessible via icon (mobile)
- Clear placeholder text: "Search..." with example query
- Debounced input (300ms) to reduce API calls

View File

@@ -128,6 +128,9 @@ export function getExamples(mode: EnhancementMode): EnhancementExample[] {
return EXAMPLES[mode];
}
/** Modes that append additional content rather than rewriting the description */
const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer'];
/**
* Build a user prompt for enhancement with optional few-shot examples
*
@@ -142,9 +145,14 @@ export function buildUserPrompt(
includeExamples: boolean = true
): string {
const examples = includeExamples ? getExamples(mode) : [];
const isAdditive = ADDITIVE_MODES.includes(mode);
const instruction = isAdditive
? 'Generate ONLY the additional details section for the following task description. Do NOT rewrite or repeat the original description:'
: 'Please enhance the following task description:';
if (examples.length === 0) {
return `Please enhance the following task description:\n\n${text}`;
return `${instruction}\n\n${text}`;
}
// Build few-shot examples section
@@ -155,13 +163,17 @@ export function buildUserPrompt(
)
.join('\n\n---\n\n');
return `Here are some examples of how to enhance task descriptions:
const examplesIntro = isAdditive
? 'Here are examples of the additional details section to generate (note: these show ONLY the appended content, not the original description):'
: 'Here are some examples of how to enhance task descriptions:';
return `${examplesIntro}
${examplesSection}
---
Now, please enhance the following task description:
${instruction}
${text}`;
}

View File

@@ -10,10 +10,12 @@ import {
TECHNICAL_SYSTEM_PROMPT,
SIMPLIFY_SYSTEM_PROMPT,
ACCEPTANCE_SYSTEM_PROMPT,
UX_REVIEWER_SYSTEM_PROMPT,
IMPROVE_EXAMPLES,
TECHNICAL_EXAMPLES,
SIMPLIFY_EXAMPLES,
ACCEPTANCE_EXAMPLES,
UX_REVIEWER_EXAMPLES,
} from '../src/enhancement.js';
describe('enhancement.ts', () => {
@@ -45,6 +47,12 @@ describe('enhancement.ts', () => {
expect(ACCEPTANCE_SYSTEM_PROMPT).toContain('acceptance criteria');
expect(ACCEPTANCE_SYSTEM_PROMPT).toContain('testable');
});
it('should export UX_REVIEWER_SYSTEM_PROMPT', () => {
expect(UX_REVIEWER_SYSTEM_PROMPT).toBeDefined();
expect(typeof UX_REVIEWER_SYSTEM_PROMPT).toBe('string');
expect(UX_REVIEWER_SYSTEM_PROMPT).toContain('User Experience');
});
});
describe('Examples Constants', () => {
@@ -100,6 +108,19 @@ describe('enhancement.ts', () => {
});
});
it('should export UX_REVIEWER_EXAMPLES with valid structure', () => {
expect(UX_REVIEWER_EXAMPLES).toBeDefined();
expect(Array.isArray(UX_REVIEWER_EXAMPLES)).toBe(true);
expect(UX_REVIEWER_EXAMPLES.length).toBeGreaterThan(0);
UX_REVIEWER_EXAMPLES.forEach((example) => {
expect(example).toHaveProperty('input');
expect(example).toHaveProperty('output');
expect(typeof example.input).toBe('string');
expect(typeof example.output).toBe('string');
});
});
it('should have shorter outputs in SIMPLIFY_EXAMPLES', () => {
SIMPLIFY_EXAMPLES.forEach((example) => {
// Simplify examples should have shorter output than input
@@ -148,6 +169,15 @@ describe('enhancement.ts', () => {
expect(result.description).toContain('acceptance');
});
it("should return prompt config for 'ux-reviewer' mode", () => {
const result = getEnhancementPrompt('ux-reviewer');
expect(result).toHaveProperty('systemPrompt');
expect(result).toHaveProperty('description');
expect(result.systemPrompt).toBe(UX_REVIEWER_SYSTEM_PROMPT);
expect(result.description.toLowerCase()).toContain('user experience');
});
it('should handle uppercase mode', () => {
const result = getEnhancementPrompt('IMPROVE');
@@ -194,6 +224,11 @@ describe('enhancement.ts', () => {
const result = getSystemPrompt('acceptance');
expect(result).toBe(ACCEPTANCE_SYSTEM_PROMPT);
});
it("should return UX_REVIEWER_SYSTEM_PROMPT for 'ux-reviewer'", () => {
const result = getSystemPrompt('ux-reviewer');
expect(result).toBe(UX_REVIEWER_SYSTEM_PROMPT);
});
});
describe('getExamples', () => {
@@ -220,6 +255,12 @@ describe('enhancement.ts', () => {
expect(result).toBe(ACCEPTANCE_EXAMPLES);
expect(result.length).toBeGreaterThan(0);
});
it("should return UX_REVIEWER_EXAMPLES for 'ux-reviewer'", () => {
const result = getExamples('ux-reviewer');
expect(result).toBe(UX_REVIEWER_EXAMPLES);
expect(result.length).toBeGreaterThan(0);
});
});
describe('buildUserPrompt', () => {
@@ -239,7 +280,7 @@ describe('enhancement.ts', () => {
it("should include examples by default for 'technical' mode", () => {
const result = buildUserPrompt('technical', testText);
expect(result).toContain('Here are some examples');
expect(result).toContain('Here are examples of the additional details section');
expect(result).toContain('Example 1:');
expect(result).toContain(TECHNICAL_EXAMPLES[0].input);
expect(result).toContain(testText);
@@ -268,10 +309,10 @@ describe('enhancement.ts', () => {
expect(dividerCount).toBe(IMPROVE_EXAMPLES.length);
});
it("should include 'Now, please enhance' before user text", () => {
it("should include 'Please enhance' before user text", () => {
const result = buildUserPrompt('improve', testText);
expect(result).toContain('Now, please enhance the following');
expect(result).toContain('Please enhance the following task description:');
expect(result).toContain(testText);
});
});
@@ -295,7 +336,14 @@ describe('enhancement.ts', () => {
const result = buildUserPrompt('technical', testText, false);
expect(result).toContain(testText);
expect(result).toContain('Please enhance');
expect(result).toContain('Generate ONLY the additional details');
});
it('should use additive phrasing for ux-reviewer mode', () => {
const result = buildUserPrompt('ux-reviewer', testText, true);
expect(result).toContain(testText);
expect(result).toContain('Here are examples of the additional details section');
});
});
@@ -310,8 +358,8 @@ describe('enhancement.ts', () => {
it('should handle empty text', () => {
const result = buildUserPrompt('improve', '');
// With examples by default, it should contain "Now, please enhance"
expect(result).toContain('Now, please enhance');
// With examples by default, it should contain "Please enhance"
expect(result).toContain('Please enhance the following task description:');
expect(result).toContain('Here are some examples');
});
@@ -331,11 +379,12 @@ describe('enhancement.ts', () => {
describe('all modes', () => {
it('should work for all valid enhancement modes', () => {
const modes: Array<'improve' | 'technical' | 'simplify' | 'acceptance'> = [
const modes: Array<'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'> = [
'improve',
'technical',
'simplify',
'acceptance',
'ux-reviewer',
];
modes.forEach((mode) => {
@@ -366,6 +415,10 @@ describe('enhancement.ts', () => {
expect(isValidEnhancementMode('acceptance')).toBe(true);
});
it("should return true for 'ux-reviewer'", () => {
expect(isValidEnhancementMode('ux-reviewer')).toBe(true);
});
it('should return false for invalid mode', () => {
expect(isValidEnhancementMode('invalid')).toBe(false);
});

View File

@@ -352,10 +352,13 @@ export function getThinkingLevelsForModel(model: string): ThinkingLevel[] {
/**
* Get the default thinking level for a given model.
* Used when selecting a model via the primary button in the two-stage selector.
* Always returns 'none' — users can configure their preferred default
* via the defaultThinkingLevel setting in the model defaults page.
* Returns 'adaptive' for Opus models (which support adaptive thinking),
* and 'none' for all other models.
*/
export function getDefaultThinkingLevel(_model: string): ThinkingLevel {
export function getDefaultThinkingLevel(model: string): ThinkingLevel {
if (isAdaptiveThinkingModel(model)) {
return 'adaptive';
}
return 'none';
}
@@ -1203,7 +1206,7 @@ export interface GlobalSettings {
/** Default maximum number of agent turns (tool call round-trips) for feature execution.
* Controls how many iterations the AI agent can perform before stopping.
* Higher values allow more complex tasks but use more API credits.
* Defaults to 1000. Range: 1-2000.
* Defaults to 10000. Range: 1-10000.
*
* Note: Currently supported by Claude (via SDK) and Codex (via CLI config).
* Gemini and OpenCode CLI providers do not support max turns configuration. */
@@ -1528,6 +1531,23 @@ export interface ProjectSettings {
*/
worktreeCopyFiles?: string[];
// Worktree Display Settings
/**
* Number of non-main worktrees to pin as tabs in the UI.
* The main worktree is always shown separately. Default: 0.
*/
pinnedWorktreesCount?: number;
/**
* Minimum number of worktrees before the list collapses into a compact dropdown selector.
* Must be >= pinnedWorktreesCount to avoid conflicting configurations. Default: 3.
*/
worktreeDropdownThreshold?: number;
/**
* When true, always show worktrees in a combined dropdown regardless of count.
* Overrides the dropdown threshold. Default: true.
*/
alwaysUseWorktreeDropdown?: boolean;
// Session Tracking
/** Last chat session selected in this project */
lastSelectedSessionId?: string;
@@ -1652,7 +1672,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
validationModel: { model: 'claude-sonnet' },
// Generation - use powerful models for quality
specGenerationModel: { model: 'claude-opus' },
specGenerationModel: { model: 'claude-opus', thinkingLevel: 'adaptive' },
featureGenerationModel: { model: 'claude-sonnet' },
backlogPlanningModel: { model: 'claude-sonnet' },
projectAnalysisModel: { model: 'claude-sonnet' },
@@ -1720,7 +1740,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
useWorktrees: true,
defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false,
defaultFeatureModel: { model: 'claude-opus' }, // Use canonical ID
defaultFeatureModel: { model: 'claude-opus', thinkingLevel: 'adaptive' }, // Use canonical ID with adaptive thinking
muteDoneSound: false,
disableSplashScreen: false,
serverLogLevel: 'info',
@@ -1728,9 +1748,9 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
showQueryDevtools: true,
enableAiCommitMessages: true,
phaseModels: DEFAULT_PHASE_MODELS,
defaultThinkingLevel: 'none',
defaultThinkingLevel: 'adaptive',
defaultReasoningEffort: 'none',
defaultMaxTurns: 1000,
defaultMaxTurns: 10000,
enhancementModel: 'sonnet', // Legacy alias still supported
validationModel: 'opus', // Legacy alias still supported
enabledCursorModels: getAllCursorModelIds(), // Returns prefixed IDs