From 5188c23765e9b3ade46829d19428f9fd016fd2cd Mon Sep 17 00:00:00 2001 From: cruldra Date: Mon, 13 Jul 2026 06:43:58 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20codex=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=BB=9F=E4=B8=80=E7=BB=84=E8=A3=85=EF=BC=8C=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B/=E6=80=9D=E8=80=83=E7=BA=A7=E5=88=AB=E5=8F=AF?= =?UTF-8?q?=E5=9C=A8=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 cc/codexCommand.ts 作为 codex 调用的唯一出口(buildCodexArgs / buildCodexCommandString),把 exec-review / resume / interactive 三种形态 的 flag 拼装收敛到一处;模型与思考级别作为自由文本项写入全局设置并贯穿 webview 消息通道,空值则不传 -c,沿用 codex config.toml 默认。 --- vscode/src/cc/codexCommand.ts | 81 +++++++++++++++++++ vscode/src/cc/reviewFlow.ts | 21 ++--- vscode/src/panel/KanbanPanel.ts | 4 + vscode/src/panel/handlers/sessions.ts | 25 +++++- vscode/src/panel/handlers/settings.ts | 11 +++ vscode/src/panel/messages.ts | 4 + vscode/src/settings/store.ts | 21 +++++ vscode/webview-ui/src/App.tsx | 2 + .../src/components/SettingsModal.tsx | 56 +++++++++++++ vscode/webview-ui/src/hooks/useIssues.ts | 8 ++ vscode/webview-ui/src/lib/messages.ts | 4 + 11 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 vscode/src/cc/codexCommand.ts diff --git a/vscode/src/cc/codexCommand.ts b/vscode/src/cc/codexCommand.ts new file mode 100644 index 0000000..8c2af4b --- /dev/null +++ b/vscode/src/cc/codexCommand.ts @@ -0,0 +1,81 @@ +/** + * Single source of truth for assembling `codex` invocations. + * + * The review flow shells codex three different ways — headless + * `codex exec review` (spawned as an arg array), `codex resume ` and the + * interactive `codex ''` TUI (both sent to a terminal). Building the + * flags in one place keeps `--dangerously-bypass-approvals-and-sandbox` and the + * optional model / reasoning-effort `-c` overrides from drifting apart. + * + * Model and reasoning effort are free-form: an empty value omits the override + * so codex falls back to its own `config.toml` default. + */ + +export interface CodexCommandOpts { + /** 'exec-review' = `codex exec review ...`;'resume' = `codex resume `;'interactive' = `codex ''` */ + mode: 'exec-review' | 'resume' | 'interactive' + /** resume 模式必填:codex thread id */ + sessionId?: string + /** interactive / exec-review 模式的 prompt(positional)。调用方保证 interactive 模式下不含单引号。 */ + prompt?: string + /** 空 = 不传 `-c model=`,用 codex 默认 */ + model?: string + /** 空 = 不传 `-c model_reasoning_effort=`,用 codex 默认。合法值 minimal/low/medium/high/xhigh */ + reasoningEffort?: string + /** exec-review 模式:追加 --json */ + json?: boolean +} + +const BYPASS_FLAG = '--dangerously-bypass-approvals-and-sandbox' + +/** + * `-c key=value` overrides for model / reasoning effort. Blank values are + * dropped so codex keeps its config.toml default. + */ +function configOverrides(opts: CodexCommandOpts): string[] { + const parts: string[] = [] + const model = opts.model?.trim() + if (model) + parts.push('-c', `model=${model}`) + const effort = opts.reasoningEffort?.trim() + if (effort) + parts.push('-c', `model_reasoning_effort=${effort}`) + return parts +} + +/** + * Shared assembly for both output shapes. `quotePrompt` wraps the positional + * prompt in single quotes for the shell-string form; the spawn-array form + * passes it raw (no shell, so no quoting). + */ +function buildParts(opts: CodexCommandOpts, quotePrompt: boolean): string[] { + const overrides = configOverrides(opts) + const prompt = opts.prompt + ? (quotePrompt ? `'${opts.prompt}'` : opts.prompt) + : undefined + switch (opts.mode) { + case 'exec-review': + return [ + 'exec', + 'review', + ...overrides, + BYPASS_FLAG, + ...(opts.json ? ['--json'] : []), + ...(prompt !== undefined ? [prompt] : []), + ] + case 'resume': + return ['resume', ...overrides, BYPASS_FLAG, ...(opts.sessionId ? [opts.sessionId] : [])] + case 'interactive': + return [...overrides, BYPASS_FLAG, ...(prompt !== undefined ? [prompt] : [])] + } +} + +/** Argument array (without the leading `codex`) for `spawn('codex', args)`. */ +export function buildCodexArgs(opts: CodexCommandOpts): string[] { + return buildParts(opts, false) +} + +/** Full shell command string (with `codex` prefix) for `terminal.sendText`. */ +export function buildCodexCommandString(opts: CodexCommandOpts): string { + return ['codex', ...buildParts(opts, true)].join(' ') +} diff --git a/vscode/src/cc/reviewFlow.ts b/vscode/src/cc/reviewFlow.ts index 82b0fa8..9be162e 100644 --- a/vscode/src/cc/reviewFlow.ts +++ b/vscode/src/cc/reviewFlow.ts @@ -11,11 +11,16 @@ import { spawn } from 'node:child_process' import { logger } from '../logging/logger' +import { buildCodexArgs } from './codexCommand' export interface RunReviewOpts { workspaceRoot: string /** Already-substituted prompt — placeholders must be resolved by the caller. */ prompt: string + /** 空 = 不传 `-c model=`,用 codex config.toml 默认。 */ + model?: string + /** 空 = 不传 `-c model_reasoning_effort=`,用 codex 默认。 */ + reasoningEffort?: string /** Spawn timeout in milliseconds. Defaults to 5 minutes. */ timeoutMs?: number /** @@ -34,15 +39,13 @@ const DEFAULT_TIMEOUT_MS = 300_000 */ export async function runReview(opts: RunReviewOpts): Promise { const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS - const args: string[] = [ - 'exec', - '-c', - 'model_reasoning_effort=xhigh', - 'review', - '--dangerously-bypass-approvals-and-sandbox', - '--json', - opts.prompt, - ] + const args = buildCodexArgs({ + mode: 'exec-review', + prompt: opts.prompt, + model: opts.model, + reasoningEffort: opts.reasoningEffort, + json: true, + }) await new Promise((resolve) => { let child: ReturnType diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 0f28e19..cbb7566 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -855,6 +855,8 @@ export class KanbanWebviewPanel { implTabPreCreateScript: s.implTabPreCreateScript, implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, + codexModel: s.codexModel, + codexReasoningEffort: s.codexReasoningEffort, }) return } @@ -918,6 +920,8 @@ export class KanbanWebviewPanel { implTabPreCreateScript: s.implTabPreCreateScript, implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, + codexModel: s.codexModel, + codexReasoningEffort: s.codexReasoningEffort, }) return } diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index b05f85e..d0f572e 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -9,6 +9,7 @@ import * as path from 'node:path' import { window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' import { buildCcCommand } from '../../cc/ccCommand' +import { buildCodexCommandString } from '../../cc/codexCommand' import { watchForNewCodexSession } from '../../cc/codexSessionWatcher' import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' @@ -287,7 +288,13 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi panel.reviewTerminals.set(sessionId, terminal) panel.trackSessionTerminal(terminal, issueNumber, 'review') terminal.show(false) - terminal.sendText(`codex resume -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox ${sessionId}`) + const settings = getSettings(panel.context) + terminal.sendText(buildCodexCommandString({ + mode: 'resume', + sessionId, + model: settings.codexModel, + reasoningEffort: settings.codexReasoningEffort, + })) logger.add({ level: 'info', source: 'terminal', @@ -418,7 +425,13 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: { panel.reviewTerminals.set(existingSessionId, terminal) panel.trackSessionTerminal(terminal, opts.issueNumber, 'review') terminal.show(false) - terminal.sendText(`codex resume -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox ${existingSessionId}`) + const settings = getSettings(panel.context) + terminal.sendText(buildCodexCommandString({ + mode: 'resume', + sessionId: existingSessionId, + model: settings.codexModel, + reasoningEffort: settings.codexReasoningEffort, + })) // codex resume rebuilds the conversation from the jsonl rollout // before the TUI accepts input — empirically much slower than the // 250ms gap the reuse path uses. 8s is a conservative guess that @@ -494,7 +507,13 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: { // the terminal alive after the run so users can follow up with codex, // mirroring how claude implementation/brainstorm sessions stay open. // 提示词模板自带 /review 前缀,不在这里再拼。 - const cmd = `codex -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox '${opts.prompt}'` + const settings = getSettings(panel.context) + const cmd = buildCodexCommandString({ + mode: 'interactive', + prompt: opts.prompt, + model: settings.codexModel, + reasoningEffort: settings.codexReasoningEffort, + }) terminal.sendText(cmd) logger.add({ level: 'info', diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 88bd144..1c7fe63 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -33,6 +33,8 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -61,6 +63,9 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { const trimmedImplPost = payload.implTabPostCloseScript.trim() // conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH). const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim() + // codex 模型 / 思考级别:空串有意义(= 用 codex 默认),只 trim。 + const trimmedCodexModel = payload.codexModel.trim() + const trimmedCodexReasoningEffort = payload.codexReasoningEffort.trim() const prev = getSettings(panel.context) // Capture the previous token *for this host* before overwriting it, so // we can decide below whether the kanban needs a re-fetch. (Only host @@ -91,6 +96,8 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPreCreateScript: trimmedImplPre, implTabPostCloseScript: trimmedImplPost, conflictResolutionProfilePath: trimmedConflictProfile, + codexModel: trimmedCodexModel, + codexReasoningEffort: trimmedCodexReasoningEffort, }) return } @@ -113,6 +120,8 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPreCreateScript: trimmedImplPre, implTabPostCloseScript: trimmedImplPost, conflictResolutionProfilePath: trimmedConflictProfile, + codexModel: trimmedCodexModel, + codexReasoningEffort: trimmedCodexReasoningEffort, youtrackBaseUrl: trimmedYtBase, youtrackProjectShortName: trimmedYtProject, youtrackCloseCommand: trimmedYtClose, @@ -196,6 +205,8 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom implTabPreCreateScript: s.implTabPreCreateScript, implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, + codexModel: s.codexModel, + codexReasoningEffort: s.codexReasoningEffort, youtrackBaseUrl: s.youtrackBaseUrl, youtrackProjectShortName: s.youtrackProjectShortName, youtrackCloseCommand: s.youtrackCloseCommand, diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index c2d04ab..587c3ff 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -67,6 +67,8 @@ export type ExtensionToWebview implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl?: string youtrackProjectShortName?: string youtrackCloseCommand?: string @@ -129,6 +131,8 @@ export type WebviewToExtension implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index d832d9a..26346b2 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -169,6 +169,16 @@ export interface Settings { * (drag to 完成 when PR merge conflicts). Empty = DEFAULT_PROFILE_PATH. */ conflictResolutionProfilePath: string + /** + * codex 审查会话使用的模型(`-c model=`)。空串 = 不传,用 codex + * `config.toml` 默认。 + */ + codexModel: string + /** + * codex 审查会话的思考级别(`-c model_reasoning_effort=`)。空串 = 不传, + * 用 codex 默认。合法值 minimal/low/medium/high/xhigh。 + */ + codexReasoningEffort: string } export const SETTINGS_KEY = 'superpowers.settings' @@ -194,6 +204,8 @@ function defaults(ctx: ExtensionContext): Settings { youtrackProjectShortName: '', youtrackCloseCommand: '', conflictResolutionProfilePath: '', + codexModel: '', + codexReasoningEffort: '', } } @@ -278,6 +290,13 @@ export function getSettings(ctx: ExtensionContext): Settings { const conflictResolutionProfilePath = typeof stored.conflictResolutionProfilePath === 'string' ? stored.conflictResolutionProfilePath : base.conflictResolutionProfilePath + // codex 模型 / 思考级别:'' 有意义(= 用 codex 默认),不强制回退。 + const codexModel = typeof stored.codexModel === 'string' + ? stored.codexModel + : base.codexModel + const codexReasoningEffort = typeof stored.codexReasoningEffort === 'string' + ? stored.codexReasoningEffort + : base.codexReasoningEffort return { webhookPort, @@ -299,6 +318,8 @@ export function getSettings(ctx: ExtensionContext): Settings { youtrackProjectShortName, youtrackCloseCommand, conflictResolutionProfilePath, + codexModel, + codexReasoningEffort, } } diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index b0b48a1..3df477f 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -453,6 +453,8 @@ export function App() { initialImplTabPreCreateScript={settings?.implTabPreCreateScript ?? ''} initialImplTabPostCloseScript={settings?.implTabPostCloseScript ?? ''} initialConflictResolutionProfilePath={settings?.conflictResolutionProfilePath ?? ''} + initialCodexModel={settings?.codexModel ?? ''} + initialCodexReasoningEffort={settings?.codexReasoningEffort ?? ''} initialYoutrackBaseUrl={settings?.youtrackBaseUrl ?? ''} initialYoutrackProjectShortName={settings?.youtrackProjectShortName ?? ''} initialYoutrackCloseCommand={settings?.youtrackCloseCommand ?? ''} diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index 771a648..1baa829 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -32,6 +32,8 @@ interface SubmitValues { implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -72,6 +74,8 @@ export interface SettingsModalProps { initialImplTabPreCreateScript: string initialImplTabPostCloseScript: string initialConflictResolutionProfilePath: string + initialCodexModel: string + initialCodexReasoningEffort: string initialYoutrackBaseUrl: string initialYoutrackProjectShortName: string initialYoutrackCloseCommand: string @@ -146,6 +150,8 @@ export function SettingsModal({ initialImplTabPreCreateScript, initialImplTabPostCloseScript, initialConflictResolutionProfilePath, + initialCodexModel, + initialCodexReasoningEffort, initialYoutrackBaseUrl, initialYoutrackProjectShortName, initialYoutrackCloseCommand, @@ -174,6 +180,8 @@ export function SettingsModal({ const [implTabPreCreateScript, setImplTabPreCreateScript] = useState(initialImplTabPreCreateScript) const [implTabPostCloseScript, setImplTabPostCloseScript] = useState(initialImplTabPostCloseScript) const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath) + const [codexModel, setCodexModel] = useState(initialCodexModel) + const [codexReasoningEffort, setCodexReasoningEffort] = useState(initialCodexReasoningEffort) const [youtrackBaseUrl, setYoutrackBaseUrl] = useState(initialYoutrackBaseUrl) const [youtrackProjectShortName, setYoutrackProjectShortName] = useState(initialYoutrackProjectShortName) const [youtrackCloseCommand, setYoutrackCloseCommand] = useState(initialYoutrackCloseCommand) @@ -204,6 +212,8 @@ export function SettingsModal({ setImplTabPreCreateScript(initialImplTabPreCreateScript) setImplTabPostCloseScript(initialImplTabPostCloseScript) setConflictResolutionProfilePath(initialConflictResolutionProfilePath) + setCodexModel(initialCodexModel) + setCodexReasoningEffort(initialCodexReasoningEffort) setYoutrackBaseUrl(initialYoutrackBaseUrl) setYoutrackProjectShortName(initialYoutrackProjectShortName) setYoutrackCloseCommand(initialYoutrackCloseCommand) @@ -287,6 +297,9 @@ export function SettingsModal({ implTabPostCloseScript: implTabPostCloseScript.trim(), // 冲突解决 profile:空串 = 用 DEFAULT_PROFILE_PATH conflictResolutionProfilePath: conflictResolutionProfilePath.trim(), + // codex 模型 / 思考级别:trim 后留空 = 用 codex config.toml 默认 + codexModel: codexModel.trim(), + codexReasoningEffort: codexReasoningEffort.trim(), youtrackBaseUrl: youtrackBaseUrl.trim(), youtrackProjectShortName: youtrackProjectShortName.trim(), youtrackCloseCommand: youtrackCloseCommand.trim(), @@ -463,6 +476,49 @@ export function SettingsModal({ )} + + + 审查会话使用的 codex 模型( + -c model= + )。留空则用 codex + {' '} + config.toml + {' '} + 默认。 + + )} + > + setCodexModel(e.target.value)} + placeholder="留空用 codex 默认(如 gpt-5.6-sol)" + className={inputClass} + /> + + + + 审查会话的 + {' '} + model_reasoning_effort + ;留空则用 codex 默认。 + + )} + > + setCodexReasoningEffort(e.target.value)} + placeholder="留空用 codex 默认(minimal/low/medium/high/xhigh)" + className={inputClass} + /> + )} diff --git a/vscode/webview-ui/src/hooks/useIssues.ts b/vscode/webview-ui/src/hooks/useIssues.ts index ab30ba6..29f204b 100644 --- a/vscode/webview-ui/src/hooks/useIssues.ts +++ b/vscode/webview-ui/src/hooks/useIssues.ts @@ -41,6 +41,8 @@ export interface SettingsValues { implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -67,6 +69,8 @@ export interface SettingsOverlayState { implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -281,6 +285,8 @@ export function useIssues(): UseIssuesResult { implTabPreCreateScript: values.implTabPreCreateScript, implTabPostCloseScript: values.implTabPostCloseScript, conflictResolutionProfilePath: values.conflictResolutionProfilePath, + codexModel: values.codexModel, + codexReasoningEffort: values.codexReasoningEffort, youtrackBaseUrl: values.youtrackBaseUrl, youtrackProjectShortName: values.youtrackProjectShortName, youtrackCloseCommand: values.youtrackCloseCommand, @@ -610,6 +616,8 @@ export function useIssues(): UseIssuesResult { implTabPreCreateScript: msg.implTabPreCreateScript, implTabPostCloseScript: msg.implTabPostCloseScript, conflictResolutionProfilePath: msg.conflictResolutionProfilePath, + codexModel: msg.codexModel, + codexReasoningEffort: msg.codexReasoningEffort, youtrackBaseUrl: msg.youtrackBaseUrl ?? '', youtrackProjectShortName: msg.youtrackProjectShortName ?? '', youtrackCloseCommand: msg.youtrackCloseCommand ?? '', diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index 4ff91bf..31ac2cf 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -82,6 +82,8 @@ export type ExtensionToWebview implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl?: string youtrackProjectShortName?: string youtrackCloseCommand?: string @@ -144,6 +146,8 @@ export type WebviewToExtension implTabPreCreateScript: string implTabPostCloseScript: string conflictResolutionProfilePath: string + codexModel: string + codexReasoningEffort: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string