feat(cli-tools): add effort level configuration for Claude CLI

- Introduced effort level options (low, medium, high) in the CLI tool settings.
- Updated the SettingsPage and CliToolCard components to handle effort level updates.
- Enhanced CLI command options to accept effort level via --effort parameter.
- Modified backend routes to support effort level updates in tool configurations.
- Created a new CliViewerToolbar component for improved CLI viewer interactions.
- Implemented logic to manage and display execution statuses and layouts in the CLI viewer.
This commit is contained in:
catlog22
2026-02-17 20:02:44 +08:00
parent 41c6f07ee0
commit c67bf86244
27 changed files with 696 additions and 241 deletions

View File

@@ -192,6 +192,8 @@ export function run(argv: string[]): void {
.option('--inject-mode <mode>', 'Inject mode: none, full, progressive (default: codex=full, others=none)')
// Template/Rules options
.option('--rule <template>', 'Template name for auto-discovery (defines $PROTO and $TMPL env vars)')
// Claude-specific options
.option('--effort <level>', 'Effort level for claude session (low, medium, high)')
// Codex review options
.option('--uncommitted', 'Review uncommitted changes (codex review)')
.option('--base <branch>', 'Review changes against base branch (codex review)')

View File

@@ -140,6 +140,8 @@ interface CliExecOptions {
title?: string; // Optional title for review summary
// Template/Rules options
rule?: string; // Template name for auto-discovery (defines $PROTO and $TMPL env vars)
// Claude-specific options
effort?: string; // Effort level for claude: low, medium, high
// Output options
raw?: boolean; // Raw output only (best for piping)
final?: boolean; // Final agent result only (best for piping)
@@ -612,6 +614,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
commit,
title,
rule,
effort,
toFile,
raw,
final: finalOnly,
@@ -1044,7 +1047,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
uncommitted,
base,
commit,
title
title,
effort
// Rules are now concatenated directly into prompt (no env vars)
}, onOutput); // Always pass onOutput for real-time dashboard streaming
@@ -1497,6 +1501,7 @@ export async function cliCommand(
console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto, review (default: analysis)'));
console.log(chalk.gray(' -d, --debug Enable debug logging for troubleshooting'));
console.log(chalk.gray(' --model <model> Model override (supports PRIMARY_MODEL, SECONDARY_MODEL aliases)'));
console.log(chalk.gray(' --effort <level> Effort level for claude (low, medium, high)'));
console.log(chalk.gray(' --cd <path> Working directory'));
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
// --timeout removed - controlled by external caller (bash timeout)

View File

@@ -320,7 +320,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
if (req.method === 'PUT') {
handlePostRequest(req, res, async (body: unknown) => {
try {
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string; availableModels?: string[]; tags?: string[]; envFile?: string | null; settingsFile?: string | null };
const updates = body as { enabled?: boolean; primaryModel?: string; secondaryModel?: string; availableModels?: string[]; tags?: string[]; envFile?: string | null; settingsFile?: string | null; effort?: string | null };
const updated = updateToolConfig(initialPath, tool, updates);
// Broadcast config updated event

View File

@@ -186,12 +186,9 @@ async function buildFileTree(
for (const entry of entries) {
const isDirectory = entry.isDirectory();
// Check if should be ignored
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
// Allow hidden files if includeHidden is true and it's .claude or .workflow
if (!includeHidden || (!entry.name.startsWith('.claude') && !entry.name.startsWith('.workflow'))) {
continue;
}
// Check if should be ignored (pass includeHidden as showAll to skip all filtering)
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory, includeHidden)) {
continue;
}
const entryPath = join(normalizedPath, entry.name);
@@ -326,17 +323,21 @@ function parseGitignore(gitignorePath: string): string[] {
* @param {string} name - File or directory name
* @param {string[]} patterns - Gitignore patterns
* @param {boolean} isDirectory - Whether the entry is a directory
* @param {boolean} showAll - When true, skip hardcoded excludes and hidden file filtering (only apply gitignore)
* @returns {boolean}
*/
function shouldIgnore(name: string, patterns: string[], isDirectory: boolean): boolean {
// Always exclude certain directories
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
return true;
}
function shouldIgnore(name: string, patterns: string[], isDirectory: boolean, showAll: boolean = false): boolean {
// When showAll is true, only apply gitignore patterns (skip hardcoded excludes and hidden files)
if (!showAll) {
// Always exclude certain directories
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
return true;
}
// Skip hidden files/directories (starting with .)
if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') {
return true;
// Skip hidden files/directories (starting with .)
if (name.startsWith('.')) {
return true;
}
}
for (const pattern of patterns) {
@@ -626,6 +627,8 @@ export async function handleFilesRoutes(ctx: RouteContext): Promise<boolean> {
const maxDepth = parseInt(url.searchParams.get('maxDepth') || '6', 10);
const includeHidden = url.searchParams.get('includeHidden') === 'true';
console.log(`[Explorer] Tree request - rootPath: ${rootPath}, includeHidden: ${includeHidden}`);
const startTime = Date.now();
try {

View File

@@ -21,6 +21,7 @@ import { remoteNotificationService } from '../core/services/remote-notification-
import {
addPendingQuestion,
getPendingQuestion,
updatePendingQuestion,
removePendingQuestion,
getAllPendingQuestions,
clearAllPendingQuestions,
@@ -451,19 +452,30 @@ export async function execute(params: AskQuestionParams): Promise<ToolResult<Ask
// Generate surface ID
const surfaceId = params.surfaceId || `question-${question.id}-${Date.now()}`;
// Check if this question was restored from disk (e.g., after MCP restart)
const existingPending = getPendingQuestion(question.id);
// Create promise for answer
const resultPromise = new Promise<AskQuestionResult>((resolve, reject) => {
// Store pending question
// Store pending question with real resolve/reject
const pendingQuestion: PendingQuestion = {
id: question.id,
surfaceId,
question,
timestamp: Date.now(),
timestamp: existingPending?.timestamp || Date.now(),
timeout: params.timeout || DEFAULT_TIMEOUT_MS,
resolve,
reject,
};
addPendingQuestion(pendingQuestion);
// If question exists (restored from disk), update it with real resolve/reject
// This fixes the "no promise attached" issue when MCP restarts
if (existingPending) {
updatePendingQuestion(question.id, pendingQuestion);
console.log(`[AskQuestion] Updated restored question "${question.id}" with real resolve/reject`);
} else {
addPendingQuestion(pendingQuestion);
}
// Set timeout
setTimeout(() => {

View File

@@ -62,6 +62,12 @@ export interface ClaudeCliTool {
* Supports ~, absolute, relative, and Windows paths
*/
settingsFile?: string;
/**
* Default effort level for Claude CLI (builtin claude only)
* Passed to Claude CLI via --effort parameter
* Valid values: 'low', 'medium', 'high'
*/
effort?: string;
}
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode' | string;
@@ -1030,6 +1036,7 @@ export function getToolConfig(projectDir: string, tool: string): {
tags?: string[];
envFile?: string;
settingsFile?: string;
effort?: string;
} {
const config = loadClaudeCliTools(projectDir);
const toolConfig = config.tools[tool];
@@ -1050,7 +1057,8 @@ export function getToolConfig(projectDir: string, tool: string): {
secondaryModel: toolConfig.secondaryModel ?? '',
tags: toolConfig.tags,
envFile: toolConfig.envFile,
settingsFile: toolConfig.settingsFile
settingsFile: toolConfig.settingsFile,
effort: toolConfig.effort
};
}
@@ -1068,6 +1076,7 @@ export function updateToolConfig(
tags: string[];
envFile: string | null;
settingsFile: string | null;
effort: string | null;
}>
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
@@ -1104,6 +1113,14 @@ export function updateToolConfig(
config.tools[tool].settingsFile = updates.settingsFile;
}
}
// Handle effort: set to undefined if null/empty, otherwise set value
if (updates.effort !== undefined) {
if (updates.effort === null || updates.effort === '') {
delete config.tools[tool].effort;
} else {
config.tools[tool].effort = updates.effort;
}
}
saveClaudeCliTools(projectDir, config);
}

View File

@@ -30,6 +30,7 @@ export interface CliToolConfig {
envFile?: string | null;
type?: 'builtin' | 'cli-wrapper' | 'api-endpoint'; // Tool type for frontend routing
settingsFile?: string | null; // Claude CLI settings file path
effort?: string | null; // Effort level for Claude CLI (low, medium, high)
}
export interface CliConfig {
@@ -160,7 +161,8 @@ export function getFullConfigResponse(baseDir: string): {
tags: tool.tags,
envFile: tool.envFile,
type: tool.type, // Preserve type field for frontend routing
settingsFile: tool.settingsFile // Preserve settingsFile for Claude CLI
settingsFile: tool.settingsFile, // Preserve settingsFile for Claude CLI
effort: tool.effort // Preserve effort level for Claude CLI
};
}

View File

@@ -429,6 +429,8 @@ const ParamsSchema = z.object({
base: z.string().optional(), // Review changes against base branch
commit: z.string().optional(), // Review changes from specific commit
title: z.string().optional(), // Optional title for review summary
// Claude-specific options
effort: z.enum(['low', 'medium', 'high']).optional(), // Effort level for claude
// Rules env vars (PROTO, TMPL) - will be passed to subprocess environment
rulesEnv: z.object({
PROTO: z.string().optional(),
@@ -458,7 +460,7 @@ async function executeCliTool(
throw new Error(`Invalid params: ${parsed.error.message}`);
}
const { tool, prompt, mode, format, model, cd, includeDirs, resume, id: customId, noNative, category, parentExecutionId, outputFormat, uncommitted, base, commit, title, rulesEnv } = parsed.data;
const { tool, prompt, mode, format, model, cd, includeDirs, resume, id: customId, noNative, category, parentExecutionId, outputFormat, uncommitted, base, commit, title, effort, rulesEnv } = parsed.data;
// Validate and determine working directory early (needed for conversation lookup)
let workingDir: string;
@@ -881,6 +883,7 @@ async function executeCliTool(
// Load and validate settings file for Claude tool (builtin only)
let settingsFilePath: string | undefined;
let effectiveEffort = effort;
if (tool === 'claude') {
const toolConfig = getToolConfig(workingDir, tool);
if (toolConfig.settingsFile) {
@@ -896,6 +899,11 @@ async function executeCliTool(
errorLog('SETTINGS_FILE', `Failed to resolve Claude settings file`, { configured: toolConfig.settingsFile, error: (err as Error).message });
}
}
// Use default effort from config if not explicitly provided, fallback to 'high'
if (!effectiveEffort) {
effectiveEffort = toolConfig.effort || 'high';
debugLog('EFFORT', `Using effort level`, { effort: effectiveEffort, source: toolConfig.effort ? 'config' : 'default' });
}
}
// Build command
@@ -908,7 +916,8 @@ async function executeCliTool(
include: includeDirs,
nativeResume: nativeResumeConfig,
settingsFile: settingsFilePath,
reviewOptions: mode === 'review' ? { uncommitted, base, commit, title } : undefined
reviewOptions: mode === 'review' ? { uncommitted, base, commit, title } : undefined,
effort: effectiveEffort
});
// Use auto-detected format (from buildCommand) if available, otherwise use passed outputFormat

View File

@@ -166,8 +166,10 @@ export function buildCommand(params: {
commit?: string;
title?: string;
};
/** Effort level for claude (low, medium, high) */
effort?: string;
}): { command: string; args: string[]; useStdin: boolean; outputFormat?: 'text' | 'json-lines' } {
const { tool, prompt, mode = 'analysis', model, dir, include, nativeResume, settingsFile, reviewOptions } = params;
const { tool, prompt, mode = 'analysis', model, dir, include, nativeResume, settingsFile, reviewOptions, effort } = params;
debugLog('BUILD_CMD', `Building command for tool: ${tool}`, {
mode,
@@ -331,6 +333,10 @@ export function buildCommand(params: {
if (model) {
args.push('--model', model);
}
// Effort level: claude --effort <low|medium|high>
if (effort) {
args.push('--effort', effort);
}
// Permission modes: write/auto → bypassPermissions, analysis → default
if (mode === 'write' || mode === 'auto') {
args.push('--permission-mode', 'bypassPermissions');