mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
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:
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user