🐛 fix(vscode): 会话终端直接拉起 claude/codex,不再往 shell 里打命令,启动命令不再污染 zsh 历史
新建工单、实施、审查、测试、原型、续聊、冲突解决、会话管理和各种恢复会话, 都改成开终端时直接指定要跑的程序(用户默认 shell 以 -ilc 'exec "$@"' 拉起), 环境变量照常加载,参数原样传入。程序退出时标签页随之关闭。 提示词和配置文件路径里带单引号也不再拒绝执行,相关的转义和拦截全部删掉。
This commit is contained in:
1 parent
21000d48e9
commit
3621729367
16 files changed
+232
-330
No files matched your search
+14
-18
@@ -1,43 +1,39 @@
|
||||
import { resolveClaudeBin } from './claudeBin'
|
||||
|
||||
/**
|
||||
* Single source of truth for the `claude` (cc) terminal launch command.
|
||||
* Single source of truth for the `claude` (cc) session argv.
|
||||
*
|
||||
* 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.
|
||||
* managed session) launches `claude` with the same flags; building the argv
|
||||
* 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). */
|
||||
/** Path passed to `--settings`. */
|
||||
profilePath: string
|
||||
/** e.g. 'high' → `--effort high`; omitted means no `--effort`. */
|
||||
effort?: string
|
||||
/** When set, append `--resume <id>` so cc restores that conversation. */
|
||||
resumeSessionId?: string
|
||||
/**
|
||||
* When set, append the prompt as a positional `'<prompt>'` arg. Combined
|
||||
* with `resumeSessionId` it is submitted as the next message of the
|
||||
* restored conversation.
|
||||
* When set, append the prompt as a positional arg. Combined with
|
||||
* `resumeSessionId` it is submitted as the next message of the restored
|
||||
* conversation.
|
||||
*/
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
function quoteIfNeeded(bin: string): string {
|
||||
return bin.includes(' ') ? `'${bin}'` : bin
|
||||
}
|
||||
|
||||
export function buildCcCommand(opts: CcCommandOpts): string {
|
||||
const parts = [quoteIfNeeded(resolveClaudeBin() ?? 'claude')]
|
||||
export function buildCcArgv(opts: CcCommandOpts): string[] {
|
||||
const argv = [resolveClaudeBin() ?? 'claude']
|
||||
if (opts.effort)
|
||||
parts.push(`--effort ${opts.effort}`)
|
||||
parts.push('--dangerously-skip-permissions', `--settings '${opts.profilePath}'`)
|
||||
argv.push('--effort', opts.effort)
|
||||
argv.push('--dangerously-skip-permissions', '--settings', opts.profilePath)
|
||||
if (opts.resumeSessionId)
|
||||
parts.push(`--resume ${opts.resumeSessionId}`)
|
||||
argv.push('--resume', opts.resumeSessionId)
|
||||
if (opts.prompt)
|
||||
parts.push(`'${opts.prompt}'`)
|
||||
return parts.join(' ')
|
||||
argv.push(opts.prompt)
|
||||
return argv
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildCodexArgv } from './codexCommand'
|
||||
|
||||
describe('buildCodexArgv', () => {
|
||||
it('resume 模式带上模型覆盖与会话 id', () => {
|
||||
expect(buildCodexArgv({ mode: 'resume', sessionId: 'tid', model: ' gpt-5 ', reasoningEffort: 'high' })).toEqual([
|
||||
'codex',
|
||||
'resume',
|
||||
'-c',
|
||||
'model=gpt-5',
|
||||
'-c',
|
||||
'model_reasoning_effort=high',
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
'tid',
|
||||
])
|
||||
})
|
||||
|
||||
it('interactive 模式空覆盖被丢弃,提示词原样作为最后一个参数', () => {
|
||||
const prompt = '/review it\'s $PR\n第二行'
|
||||
expect(buildCodexArgv({ mode: 'interactive', prompt, model: '', reasoningEffort: ' ' })).toEqual([
|
||||
'codex',
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
prompt,
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Single source of truth for assembling `codex` invocations.
|
||||
*
|
||||
* The review flow shells codex two ways — `codex resume <id>` and the
|
||||
* interactive `codex '<prompt>'` TUI — both sent to a terminal. Building the
|
||||
* The review flow launches codex two ways — `codex resume <id>` and the
|
||||
* interactive `codex <prompt>` TUI — each as a session terminal's process. Building the
|
||||
* flags in one place keeps `--dangerously-bypass-approvals-and-sandbox` and the
|
||||
* optional model / reasoning-effort `-c` overrides from drifting apart.
|
||||
*
|
||||
@@ -11,11 +11,11 @@
|
||||
*/
|
||||
|
||||
export interface CodexCommandOpts {
|
||||
/** 'resume' = `codex resume <id>`;'interactive' = `codex '<prompt>'` */
|
||||
/** 'resume' = `codex resume <id>`;'interactive' = `codex <prompt>` */
|
||||
mode: 'resume' | 'interactive'
|
||||
/** resume 模式必填:codex thread id */
|
||||
sessionId?: string
|
||||
/** interactive 模式的 prompt(positional)。调用方保证不含单引号。 */
|
||||
/** interactive 模式的 prompt(positional) */
|
||||
prompt?: string
|
||||
/** 空 = 不传 `-c model=`,用 codex 默认 */
|
||||
model?: string
|
||||
@@ -40,11 +40,11 @@ function configOverrides(opts: CodexCommandOpts): string[] {
|
||||
return parts
|
||||
}
|
||||
|
||||
/** Full shell command string (with `codex` prefix) for `terminal.sendText`. */
|
||||
export function buildCodexCommandString(opts: CodexCommandOpts): string {
|
||||
/** Full argv, starting with `codex`. */
|
||||
export function buildCodexArgv(opts: CodexCommandOpts): string[] {
|
||||
const overrides = configOverrides(opts)
|
||||
const parts = opts.mode === 'resume'
|
||||
? ['resume', ...overrides, BYPASS_FLAG, ...(opts.sessionId ? [opts.sessionId] : [])]
|
||||
: [...overrides, BYPASS_FLAG, ...(opts.prompt ? [`'${opts.prompt}'`] : [])]
|
||||
return ['codex', ...parts].join(' ')
|
||||
: [...overrides, BYPASS_FLAG, ...(opts.prompt ? [opts.prompt] : [])]
|
||||
return ['codex', ...parts]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildSessionLaunch } from './sessionLaunch'
|
||||
|
||||
describe('buildSessionLaunch', () => {
|
||||
it('用登录交互 shell 执行 exec "$@",$0 占位为 shell 名', () => {
|
||||
expect(buildSessionLaunch('/opt/homebrew/bin/bash', ['claude', '--resume', 'abc'])).toEqual({
|
||||
shellPath: '/opt/homebrew/bin/bash',
|
||||
shellArgs: ['-ilc', 'exec "$@"', 'bash', 'claude', '--resume', 'abc'],
|
||||
})
|
||||
})
|
||||
|
||||
it('默认 shell 为空时回退 /bin/zsh', () => {
|
||||
expect(buildSessionLaunch('', ['codex'])).toEqual({
|
||||
shellPath: '/bin/zsh',
|
||||
shellArgs: ['-ilc', 'exec "$@"', 'zsh', 'codex'],
|
||||
})
|
||||
})
|
||||
|
||||
it('含空格、单引号、$、换行的提示词原样作为一个参数', () => {
|
||||
const prompt = 'it\'s a "test" with $HOME and `cmd`\n第二行 \'\\\'\''
|
||||
const { shellArgs } = buildSessionLaunch('/bin/zsh', ['claude', prompt])
|
||||
expect(shellArgs.at(-1)).toBe(prompt)
|
||||
expect(shellArgs).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as path from 'node:path'
|
||||
|
||||
/**
|
||||
* 会话终端直接由登录 shell 拉起目标程序,而不是开个空 shell 再往里打字:
|
||||
* - `-il` 照常加载 .zprofile / .zshrc,PATH、mise 等环境与手动开终端一致
|
||||
* - `-c` 执行的命令不进 shell 历史,超长的启动命令不污染历史补全
|
||||
* - argv 走 `"$@"` 原样传进去,空格、引号、`$`、换行都不经 shell 解析
|
||||
* - `exec` 让程序顶替 shell,程序退出时终端标签页随之关闭
|
||||
*/
|
||||
|
||||
export interface SessionLaunch {
|
||||
shellPath: string
|
||||
shellArgs: string[]
|
||||
}
|
||||
|
||||
const FALLBACK_SHELL = '/bin/zsh'
|
||||
|
||||
export function buildSessionLaunch(shell: string, argv: readonly string[]): SessionLaunch {
|
||||
const shellPath = shell || FALLBACK_SHELL
|
||||
// `-c` 后的第一个参数成为 $0,占位用 shell 自己的名字,后面的才是 "$@"。
|
||||
return {
|
||||
shellPath,
|
||||
shellArgs: ['-ilc', 'exec "$@"', path.basename(shellPath), ...argv],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { SessionLaunch } from './sessionLaunch'
|
||||
import { env } from 'vscode'
|
||||
import { buildSessionLaunch } from './sessionLaunch'
|
||||
|
||||
/** 用户在 VS Code 里的默认 shell 拉起会话程序,展开进 `window.createTerminal` 的选项。 */
|
||||
export function sessionLaunch(argv: readonly string[]): SessionLaunch {
|
||||
return buildSessionLaunch(env.shell, argv)
|
||||
}
|
||||
Reference in new issue
Block a user