diff --git a/vscode/src/cc/ccCommand.ts b/vscode/src/cc/ccCommand.ts new file mode 100644 index 0000000..2345b69 --- /dev/null +++ b/vscode/src/cc/ccCommand.ts @@ -0,0 +1,35 @@ +/** + * Single source of truth for the `claude` (cc) terminal launch command. + * + * Every session entry point (brainstorm / implement / test / review / resume / + * managed session) shells out `claude` with the same flags; building the + * string in one place keeps those call sites from drifting apart. + * + * We deliberately pass no `--system-prompt`; cc runs with its own default + * system prompt. + */ + +export interface CcCommandOpts { + /** Path passed to `--settings` (caller guarantees it contains no single quote). */ + profilePath: string + /** e.g. 'high' → `--effort high`; omitted means no `--effort`. */ + effort?: string + /** When set, append `--resume `. Mutually exclusive with prompt; resume wins. */ + resumeSessionId?: string + /** When set (and no resume), append the prompt as a positional `''` arg. */ + prompt?: string +} + +export function buildCcCommand(opts: CcCommandOpts): string { + const parts = ['claude'] + if (opts.effort) + parts.push(`--effort ${opts.effort}`) + parts.push('--dangerously-skip-permissions', `--settings '${opts.profilePath}'`) + // resume and prompt are mutually exclusive; a resumed session restores its + // own conversation, so a fresh prompt would be meaningless. + if (opts.resumeSessionId) + parts.push(`--resume ${opts.resumeSessionId}`) + else if (opts.prompt) + parts.push(`'${opts.prompt}'`) + return parts.join(' ') +} diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index 7c9eed1..6ec93bc 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -10,6 +10,7 @@ import * as path from 'node:path' import { resolveWorktreePath } from '../../git/worktree' import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' +import { buildCcCommand } from '../../cc/ccCommand' import { getBrainstormPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' import { spawnClaude } from '../../cc/spawnClaude' @@ -1283,7 +1284,7 @@ export async function handleIssueCreate( createdAt: Date.now(), }) - const cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" '${prompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt }) terminal.sendText(cmd) logger.add({ level: 'info', diff --git a/vscode/src/panel/handlers/managedSessions.ts b/vscode/src/panel/handlers/managedSessions.ts index ce6c386..ec82740 100644 --- a/vscode/src/panel/handlers/managedSessions.ts +++ b/vscode/src/panel/handlers/managedSessions.ts @@ -1,6 +1,7 @@ import type { KanbanWebviewPanel } from '../KanbanPanel' import { promises as fsp } from 'node:fs' import { window, workspace } from 'vscode' +import { buildCcCommand } from '../../cc/ccCommand' import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher' import { logger } from '../../logging/logger' import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore' @@ -53,9 +54,8 @@ export async function handleManagedSessionsGet(panel: KanbanWebviewPanel): Promi /** * 从会话管理 tab 创建一个新的 cc 会话: * - cwd 用项目根(workspaceRoot,非 worktree)。 - * - 启动命令照搬头脑风暴风格(claude --dangerously-skip-permissions --settings - * '' --system-prompt="$(serena prompts ...)"),prompt 非空时 - * 再追加 ' '。 + * - 启动命令统一由 buildCcCommand 构建(claude --dangerously-skip-permissions + * --settings ''),prompt 非空时作为位置参数传入。 * - 用 pollForNewSession 捕获新 sessionId(共享 projects 目录用轮询更可靠), * 落进 .spx/session-names.json 后推全量列表。 */ @@ -121,9 +121,7 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro message: `已创建终端 "${terminal.name}"`, }) - let cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)"` - if (effectivePrompt) - cmd += ` '${effectivePrompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt: effectivePrompt || undefined }) terminal.sendText(cmd) logger.add({ level: 'info', @@ -243,7 +241,7 @@ export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, ses message: `已创建终端 "${terminal.name}" (resume ${sessionId})`, }) - const cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" --resume ${sessionId}` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId }) terminal.sendText(cmd) // 登记终端并推 show,让列表的 tabOpen 立即为 true。 diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index c4f2b0b..59e3549 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -8,6 +8,7 @@ import * as os from 'node:os' import * as path from 'node:path' import { window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' +import { buildCcCommand } from '../../cc/ccCommand' import { watchForNewCodexSession } from '../../cc/codexSessionWatcher' import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' @@ -205,8 +206,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: dismissOnTimer: 5000, }) } - const effortArg = sessionKind === 'implement' ? ' --effort high' : '' - const cmd = `claude${effortArg} --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" --resume ${sessionId}` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: sessionKind === 'implement' ? 'high' : undefined, resumeSessionId: sessionId }) terminal.sendText(cmd) } finally { @@ -870,7 +870,7 @@ export async function handleImplement( source: 'terminal', message: `已创建终端 "${terminal.name}"`, }) - const cmd = `claude --effort high --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" '${prompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: 'high', prompt }) terminal.sendText(cmd) logger.add({ level: 'info', @@ -1020,7 +1020,7 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is message: `已创建终端 "${terminal.name}"`, }) - const cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" '${prompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt }) terminal.sendText(cmd) logger.add({ level: 'info', @@ -1220,7 +1220,7 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum message: `已创建终端 "${terminal.name}"`, }) - const cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" '${prompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt }) terminal.sendText(cmd) logger.add({ level: 'info', @@ -1366,7 +1366,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session message: `已创建测试会话终端 #${issueNumber} cwd=${effectiveCwd}`, }) - const cmd = `claude --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt=\"$(serena prompts print-cc-system-prompt-override)\" --resume ${sessionId}` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId }) terminal.sendText(cmd) } finally { @@ -1568,7 +1568,7 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: { }) return } - const cmd = `claude --effort high --dangerously-skip-permissions --settings '${effectiveProfilePath}' --system-prompt="$(serena prompts print-cc-system-prompt-override)" '${prompt}'` + const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: 'high', prompt }) terminal.sendText(cmd) logger.add({