Compare commits
4
Commits
21000d48e9
...
8ea5361f3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ea5361f3d | ||
|
|
858bd1e34b | ||
|
|
cb540c5db5 | ||
|
|
3621729367 |
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.205",
|
||||
"version": "0.2.207",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
|
||||
+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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { hasReviewMarker, stripReviewMarker } from './markers'
|
||||
|
||||
const COLUMN_LABEL: Record<string, string> = {
|
||||
'todo': '待办',
|
||||
'in-progress': '进行中',
|
||||
@@ -26,8 +28,8 @@ const STATE_KEYS = new Set([
|
||||
/** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */
|
||||
export function systemCommentText(body: string): string | undefined {
|
||||
const trimmed = body.trim()
|
||||
if (/<!--\s*spx:review=1\s*-->/i.test(trimmed)) {
|
||||
const text = trimmed.replace(/<!--\s*spx:review=1\s*-->/gi, '').trim()
|
||||
if (hasReviewMarker(trimmed)) {
|
||||
const text = stripReviewMarker(trimmed)
|
||||
return text.length > 0 ? text : '提交了审查'
|
||||
}
|
||||
if (!trimmed.startsWith('{'))
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from './markers'
|
||||
|
||||
const REAL_REVIEW = `<!-- spx:review=1 -->
|
||||
## 审查意见
|
||||
|
||||
- **coordinator.ts:1452** 正则多写了反斜杠
|
||||
- 建议补单测
|
||||
|
||||
\`\`\`ts
|
||||
const x = 1
|
||||
\`\`\``
|
||||
|
||||
describe('审查标记', () => {
|
||||
it('真实审查评论:识别为审查,去标记后正文完整', () => {
|
||||
expect(hasReviewMarker(REAL_REVIEW)).toBe(true)
|
||||
expect(stripReviewMarker(REAL_REVIEW)).toBe(REAL_REVIEW.slice('<!-- spx:review=1 -->\n'.length))
|
||||
})
|
||||
|
||||
it('普通评论不是审查', () => {
|
||||
expect(hasReviewMarker('这个方案可以')).toBe(false)
|
||||
expect(hasReviewMarker('提到了 spx:review=1 但不是注释')).toBe(false)
|
||||
})
|
||||
|
||||
it('大小写和注释内空白都放宽', () => {
|
||||
expect(hasReviewMarker('<!--SPX:Review=1-->\n正文')).toBe(true)
|
||||
expect(hasReviewMarker('<!-- spx:review=1 -->')).toBe(true)
|
||||
})
|
||||
|
||||
it('反复调用结果稳定', () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(hasReviewMarker(REAL_REVIEW)).toBe(true)
|
||||
expect(stripReviewMarker(REAL_REVIEW)).toMatch(/^## 审查意见/)
|
||||
}
|
||||
})
|
||||
|
||||
it('多个标记全部去掉,只剩标记时为空串', () => {
|
||||
expect(stripReviewMarker('<!-- spx:review=1 -->\n a <!-- SPX:review=1 --> b')).toBe('a b')
|
||||
expect(stripReviewMarker('<!-- spx:review=1 -->\n')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseClosingIssueNumber', () => {
|
||||
it('多行 PR body 最后一行 Closes #462', () => {
|
||||
const body = '## 改动\n\n- 修了正则\n- 加了测试\n\nCloses #462'
|
||||
expect(parseClosingIssueNumber(body)).toBe(462)
|
||||
})
|
||||
|
||||
it('其它关键字与大小写', () => {
|
||||
expect(parseClosingIssueNumber('fixes #12')).toBe(12)
|
||||
expect(parseClosingIssueNumber('FiXeS #7')).toBe(7)
|
||||
expect(parseClosingIssueNumber('RESOLVE #3')).toBe(3)
|
||||
expect(parseClosingIssueNumber('close\t#5')).toBe(5)
|
||||
})
|
||||
|
||||
it('没有关键字返回 undefined', () => {
|
||||
expect(parseClosingIssueNumber('关联 #462')).toBeUndefined()
|
||||
expect(parseClosingIssueNumber('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('不误匹配:# 前无空白、关键字粘在别的词后面', () => {
|
||||
expect(parseClosingIssueNumber('closes#1')).toBeUndefined()
|
||||
expect(parseClosingIssueNumber('prefixcloses #1')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 评论 / PR 正文里约定标记的唯一解析入口。
|
||||
*
|
||||
* - 审查标记:审查会话发到 PR 上的评论第一行写 `<!-- spx:review=1 -->`,
|
||||
* webhook 靠它识别"这是审查意见,要转给实施会话"。
|
||||
* - 关闭关键字:PR body 里的 `Closes #N` 等,用来从 PR 反查工单号。
|
||||
*/
|
||||
|
||||
const REVIEW_MARKER = /<!--\s*spx:review=1\s*-->/i
|
||||
// 带 g 的正则只给 replace 用:replace 每次从头扫,不受 lastIndex 残留影响。
|
||||
const REVIEW_MARKER_WITH_TRAILING_SPACE = new RegExp(`${REVIEW_MARKER.source}\\s*`, 'gi')
|
||||
const CLOSING_KEYWORD = /\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i
|
||||
|
||||
export function hasReviewMarker(body: string): boolean {
|
||||
return REVIEW_MARKER.test(body)
|
||||
}
|
||||
|
||||
/** 去掉所有审查标记及其后空白,返回 trim 后的正文。 */
|
||||
export function stripReviewMarker(body: string): string {
|
||||
return body.replace(REVIEW_MARKER_WITH_TRAILING_SPACE, '').trim()
|
||||
}
|
||||
|
||||
/** 从 PR body 取第一个关闭关键字指向的工单号。 */
|
||||
export function parseClosingIssueNumber(body: string): number | undefined {
|
||||
const m = body.match(CLOSING_KEYWORD)
|
||||
return m ? Number.parseInt(m[1], 10) : undefined
|
||||
}
|
||||
@@ -857,7 +857,7 @@ export class KanbanWebviewPanel {
|
||||
/**
|
||||
* Scan `vscode.window.terminals` for an existing terminal whose name
|
||||
* matches `expectedName`. Matches exact, or `startsWith(expectedName + ' ')`
|
||||
* so a shell that appended a git branch suffix (e.g. "issue-48-实施
|
||||
* so a title sequence that appended a suffix (e.g. "issue-48-实施
|
||||
* 5f56026c") still counts. Skips terminals whose process already exited
|
||||
* (`exitStatus !== undefined`) — those are zombie tabs and shouldn't block
|
||||
* a fresh spawn.
|
||||
@@ -875,8 +875,8 @@ export class KanbanWebviewPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open (or re-use) the review terminal tab for an issue and send the codex
|
||||
* review command. Used by the webhook auto-review flow so the user can watch
|
||||
* Open (or re-use) the review terminal tab for an issue and start codex
|
||||
* review in it. Used by the webhook auto-review flow so the user can watch
|
||||
* codex work in real time instead of it running headless in the background.
|
||||
*
|
||||
* 只有已持久化 reviewSessionId 时才注入 "PR 更新了,再审一下";tab 活着但
|
||||
@@ -890,7 +890,7 @@ export class KanbanWebviewPanel {
|
||||
/** workspace-relative or absolute path; if missing/invalid we fall back to workspaceRoot. */
|
||||
worktreePath: string
|
||||
workspaceRoot: string
|
||||
}): Promise<boolean> {
|
||||
}): Promise<void> {
|
||||
return sessions.triggerAutoReviewTab(this, opts)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCcArgv } from '../../cc/ccCommand'
|
||||
import { resolveCommitProfileForRun } from '../../cc/commitProfile'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { renderBrainstormPrompt } from '../../cc/prompts'
|
||||
import { claudeProjectsRoot, installClaudeSession } from '../../cc/sessionBundle'
|
||||
import { sessionLaunch } from '../../cc/sessionTerminal'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { spawnClaude } from '../../cc/spawnClaude'
|
||||
import { gitFetch, resolveFeatureBranch } from '../../git/branches'
|
||||
@@ -1440,37 +1441,9 @@ export async function handleIssueCreate(
|
||||
nonce,
|
||||
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
||||
})
|
||||
if (prompt.includes('\'')) {
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: '创建失败:prompt 含单引号,拒绝执行',
|
||||
dismissOnTimer: 8000,
|
||||
})
|
||||
try {
|
||||
await fsp.rm(inboxDir, { recursive: true, force: true })
|
||||
}
|
||||
catch {}
|
||||
return
|
||||
}
|
||||
|
||||
// 新建工单弹窗里选的 profile > 设置里的「头脑风暴默认配置文件」> DEFAULT。
|
||||
const effectiveProfilePath = sessions.resolveBrainstormProfilePath(panel, brainstormProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: `创建失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
dismissOnTimer: 8000,
|
||||
})
|
||||
try {
|
||||
await fsp.rm(inboxDir, { recursive: true, force: true })
|
||||
}
|
||||
catch {}
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the claude projects subdir exists *before* spawning the
|
||||
// terminal so the watcher can't miss the create event.
|
||||
@@ -1492,6 +1465,7 @@ export async function handleIssueCreate(
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, prompt })),
|
||||
})
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
@@ -1513,13 +1487,10 @@ export async function handleIssueCreate(
|
||||
terminal,
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt })
|
||||
terminal.sendText(cmd)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已发送新建工单 prompt nonce=${shortNonce}`,
|
||||
message: `已启动新建工单会话 nonce=${shortNonce}`,
|
||||
})
|
||||
|
||||
panel.postMessage({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCcArgv } from '../../cc/ccCommand'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { sessionLaunch } from '../../cc/sessionTerminal'
|
||||
import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { listImportableNamedSessions } from '../../sessions/importableNamedSessions'
|
||||
@@ -71,39 +72,20 @@ export interface CreateCcSessionTabOptions {
|
||||
|
||||
/**
|
||||
* 创建 cc 会话终端 tab(核心逻辑,不要求 panel):
|
||||
* - 启动命令统一由 buildCcCommand 构建(claude --dangerously-skip-permissions
|
||||
* --settings '<profilePath>'),prompt 非空时作为位置参数传入。
|
||||
* - 启动参数统一由 buildCcArgv 构建(claude --dangerously-skip-permissions
|
||||
* --settings <profilePath>),prompt 非空时作为位置参数传入。
|
||||
* - register 为 true 时用 pollForNewSession 捕获新 sessionId(共享 projects 目录用轮询更可靠),
|
||||
* 落进 workspaceRoot 的 .spx/session-names.json;有 panel 时再登记终端并推全量列表。
|
||||
* - register 为 false 时只开终端,不落 store、不进会话管理列表。
|
||||
*
|
||||
* 校验失败时 showErrorMessage 并返回 undefined;成功返回终端名。
|
||||
*/
|
||||
export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promise<{ terminalName: string } | undefined> {
|
||||
export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promise<{ terminalName: string }> {
|
||||
const { workspaceRoot, panel } = opts
|
||||
|
||||
const effectiveProfilePath = resolveProfilePath(
|
||||
opts.profilePath && opts.profilePath.trim() !== '' ? opts.profilePath : undefined,
|
||||
)
|
||||
// 单引号会破坏下面的 shell 单引号包裹,防御性拒绝(与现有 handler 一致)。
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`创建会话失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const trimmedName = (opts.name ?? '').trim()
|
||||
if (trimmedName.includes('\'')) {
|
||||
void window.showErrorMessage('创建会话失败:会话名字含单引号,拒绝执行')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const trimmedPrompt = (opts.prompt ?? '').trim()
|
||||
if (trimmedPrompt.includes('\'')) {
|
||||
void window.showErrorMessage('创建会话失败:首个提示词含单引号,拒绝执行')
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 首个提示词:填了 prompt 用 prompt;否则填了 name 用 /rename <name>;否则交互式无提示词。
|
||||
let effectivePrompt = ''
|
||||
@@ -134,6 +116,7 @@ export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promi
|
||||
name: trimmedName || 'cc-会话',
|
||||
cwd: effectiveCwd,
|
||||
location: resolveTerminalLocation(panel, false),
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, prompt: effectivePrompt || undefined })),
|
||||
})
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
@@ -142,9 +125,6 @@ export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promi
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt: effectivePrompt || undefined })
|
||||
terminal.sendText(cmd)
|
||||
|
||||
// Fire-and-forget:会话 jsonl 出现后写进 store;有 panel 再登记终端并推全量列表。
|
||||
watchPromise?.then(async (sid) => {
|
||||
if (!sid) {
|
||||
@@ -197,9 +177,7 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
|
||||
return
|
||||
}
|
||||
|
||||
const created = await createCcSessionTab({ workspaceRoot, profilePath, name, prompt, panel, register: true })
|
||||
if (!created)
|
||||
return
|
||||
await createCcSessionTab({ workspaceRoot, profilePath, name, prompt, panel, register: true })
|
||||
|
||||
logger.add({
|
||||
level: 'info',
|
||||
@@ -280,18 +258,13 @@ export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, ses
|
||||
}
|
||||
|
||||
const effectiveProfilePath = resolveProfilePath(decided.effective)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 终端名用 store 里记录的会话名(缺省回落到 cc-会话)。
|
||||
const terminal = window.createTerminal({
|
||||
name: target?.name?.trim() || 'cc-会话',
|
||||
cwd: workspaceRoot,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })),
|
||||
})
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
@@ -300,9 +273,6 @@ export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, ses
|
||||
message: `已创建终端 "${terminal.name}" (resume ${sessionId})`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })
|
||||
terminal.sendText(cmd)
|
||||
|
||||
// 登记终端并推 show,让列表的 tabOpen 立即为 true。
|
||||
panel.managedTerminals.set(sessionId, terminal)
|
||||
await pushManagedSessions(panel)
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
import type { CherryPickConflict } from '../../git/release'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCcArgv } from '../../cc/ccCommand'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { sessionLaunch } from '../../cc/sessionTerminal'
|
||||
import { gitFetch } from '../../git/branches'
|
||||
import {
|
||||
attachPulls,
|
||||
@@ -226,18 +227,13 @@ ${remaining}
|
||||
return
|
||||
}
|
||||
const profilePath = resolveProfilePath(getSettings(panel.context).conflictResolutionProfilePath.trim() || undefined)
|
||||
if (profilePath.includes('\'')) {
|
||||
toast(panel, 'error', `冲突解决失败:profilePath 含单引号 (${profilePath})`, { dismissOnTimer: 10_000 })
|
||||
return
|
||||
}
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: conflict.worktreePath,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
...sessionLaunch(buildCcArgv({ profilePath, effort: 'high', prompt })),
|
||||
})
|
||||
terminal.show(false)
|
||||
// buildCcCommand 用单引号包提示词,内部的 ' 要转成 '\'' 才不会破壳。
|
||||
terminal.sendText(buildCcCommand({ profilePath, effort: 'high', prompt: prompt.replace(/'/g, '\'\\\'\'') }))
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
|
||||
@@ -8,11 +8,12 @@ import * as path from 'node:path'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../../auth/identity'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCodexCommandString } from '../../cc/codexCommand'
|
||||
import { buildCcArgv } from '../../cc/ccCommand'
|
||||
import { buildCodexArgv } from '../../cc/codexCommand'
|
||||
import { watchForCodexSession } from '../../cc/codexSessionWatcher'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { renderBrainstormPrompt, renderImplementPlanPrompt, renderPrototypePrompt, renderReviewPrompt, renderTestPrompt } from '../../cc/prompts'
|
||||
import { sessionLaunch } from '../../cc/sessionTerminal'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||
@@ -140,16 +141,6 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
= sessionKind === 'implement'
|
||||
? panel.resolveImplementProfilePath(profilePath)
|
||||
: resolveBrainstormProfilePath(panel, profilePath)
|
||||
// Reject paths containing single quotes — we shell-quote with single
|
||||
// quotes below, and embedded quotes would break out of the wrap. In
|
||||
// practice profile paths live under `/home/<user>/...` so this is a
|
||||
// defensive guard that should never fire.
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
// 实施会话住在 worktree 里;relCwd 可能是绝对路径 / `~/...` / 工作区相对
|
||||
// 路径,统一走 resolveWorktreePath。worktree 已被清理(典型:卡片拖到
|
||||
@@ -219,6 +210,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
...(issueIcon ? { iconPath: issueIcon.iconUri, color: issueIcon.themeColor } : {}),
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, effort: sessionKind === 'implement' ? 'high' : undefined, resumeSessionId: sessionId })),
|
||||
})
|
||||
panel.terminals.set(sessionId, terminal)
|
||||
if (issueNumber !== undefined)
|
||||
@@ -240,8 +232,6 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
}
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: sessionKind === 'implement' ? 'high' : undefined, resumeSessionId: sessionId })
|
||||
terminal.sendText(cmd)
|
||||
}
|
||||
finally {
|
||||
if (lockKey !== undefined)
|
||||
@@ -332,23 +322,23 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
// `color` option. Force a `circle-filled` codicon so the tab actually
|
||||
// shows the issue color; keep `color` too for panel-view fallback.
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
||||
const settings = getSettings(panel.context)
|
||||
const terminal = window.createTerminal({
|
||||
name: reviewTerminalName,
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCodexArgv({
|
||||
mode: 'resume',
|
||||
sessionId,
|
||||
model: settings.codexModel,
|
||||
reasoningEffort: settings.codexReasoningEffort,
|
||||
})),
|
||||
})
|
||||
panel.reviewTerminals.set(sessionId, terminal)
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'review')
|
||||
terminal.show(false)
|
||||
const settings = getSettings(panel.context)
|
||||
terminal.sendText(buildCodexCommandString({
|
||||
mode: 'resume',
|
||||
sessionId,
|
||||
model: settings.codexModel,
|
||||
reasoningEffort: settings.codexReasoningEffort,
|
||||
}))
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
@@ -362,8 +352,8 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
}
|
||||
|
||||
/**
|
||||
* Open (or re-use) the review terminal tab for an issue and send the codex
|
||||
* review command. Used by the webhook auto-review flow so the user can watch
|
||||
* Open (or re-use) the review terminal tab for an issue and start codex
|
||||
* review in it. Used by the webhook auto-review flow so the user can watch
|
||||
* codex work in real time instead of it running headless in the background.
|
||||
*
|
||||
* 路径由 {@link decideAutoReviewPath} 决定(与 TUI 对齐):
|
||||
@@ -372,10 +362,6 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
* - new: 无 session → 完整启动 codex review
|
||||
* - noop: tab 活着但 session 尚未落盘 → 跳过(避免 opened 后紧跟
|
||||
* synchronize 把首轮审查污染成再审文案)
|
||||
*
|
||||
* Returns true if the command was successfully dispatched (or intentionally
|
||||
* skipped as noop / already in-flight), false if a pre-condition failed
|
||||
* (single-quote in prompt — only checked on the new-terminal path).
|
||||
*/
|
||||
export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
||||
issueNumber: number
|
||||
@@ -384,7 +370,7 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
||||
/** workspace-relative or absolute path; if missing/invalid we fall back to workspaceRoot. */
|
||||
worktreePath: string
|
||||
workspaceRoot: string
|
||||
}): Promise<boolean> {
|
||||
}): Promise<void> {
|
||||
// 防 opened + synchronize 竞态:同 issue 的自动审查启动中不重入。
|
||||
const lockKey = `${opts.issueNumber}:auto-review`
|
||||
if (panel.resumeInFlight.has(lockKey)) {
|
||||
@@ -393,12 +379,12 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
||||
source: 'review',
|
||||
message: `自动审查 #${opts.issueNumber} 已在启动中,忽略重入`,
|
||||
})
|
||||
return true
|
||||
return
|
||||
}
|
||||
panel.resumeInFlight.add(lockKey)
|
||||
|
||||
try {
|
||||
return await triggerAutoReviewTabUnlocked(panel, opts)
|
||||
await triggerAutoReviewTabUnlocked(panel, opts)
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
@@ -411,7 +397,7 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
prompt: string
|
||||
worktreePath: string
|
||||
workspaceRoot: string
|
||||
}): Promise<boolean> {
|
||||
}): Promise<void> {
|
||||
// Resolve cwd. If worktreePath is provided but doesn't exist on disk
|
||||
// (worktree was cleaned up after merge), fall back to workspaceRoot and
|
||||
// toast the user — same fallback as handleResumeSession. Only matters on
|
||||
@@ -472,7 +458,7 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
panel.trackSessionTerminal(existing, opts.issueNumber, 'review')
|
||||
existing.show(false)
|
||||
}
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
||||
if (reviewPath === 'reuse') {
|
||||
@@ -487,7 +473,7 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
source: 'terminal',
|
||||
message: `已发送"PR 更新了,再审一下"到复用的 #${opts.issueNumber}-审查 tab`,
|
||||
})
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
||||
if (reviewPath === 'resume' && existingSessionId) {
|
||||
@@ -499,23 +485,23 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
// rollout-*.jsonl, so there's nothing to capture. No cwdFallback
|
||||
// toast either — keep parity with the reuse branch.
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(opts.issueNumber)
|
||||
const settings = getSettings(panel.context)
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCodexArgv({
|
||||
mode: 'resume',
|
||||
sessionId: existingSessionId,
|
||||
model: settings.codexModel,
|
||||
reasoningEffort: settings.codexReasoningEffort,
|
||||
})),
|
||||
})
|
||||
panel.reviewTerminals.set(existingSessionId, terminal)
|
||||
panel.trackSessionTerminal(terminal, opts.issueNumber, 'review')
|
||||
terminal.show(false)
|
||||
const settings = getSettings(panel.context)
|
||||
terminal.sendText(buildCodexCommandString({
|
||||
mode: 'resume',
|
||||
sessionId: existingSessionId,
|
||||
model: settings.codexModel,
|
||||
reasoningEffort: settings.codexReasoningEffort,
|
||||
}))
|
||||
// codex resume 要先从记录里把对话拼回来,拼完才吃得进字。
|
||||
// 已经开着的审查终端不用等;冷启动留 8 秒,够它起来又不至于拖很久。
|
||||
setTimeout(() => {
|
||||
@@ -526,36 +512,50 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
source: 'review',
|
||||
message: `已 codex resume 审查会话 ${existingSessionId} 并注入再审消息 (#${opts.issueNumber})`,
|
||||
})
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
||||
// --- new-terminal path: full codex startup + session watcher ---
|
||||
|
||||
// codex command is shell-quoted with single quotes; reject prompts that
|
||||
// would break the quoting rather than try to escape (same posture as
|
||||
// handleImplement). Only relevant on the new-terminal path — the reuse
|
||||
// path sends a fixed string, not opts.prompt.
|
||||
if (opts.prompt.includes('\'')) {
|
||||
logger.add({
|
||||
level: 'error',
|
||||
source: 'webhook',
|
||||
message: `审查 prompt 含单引号,拒绝执行 #${opts.issueNumber}`,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(opts.issueNumber)
|
||||
const settings = getSettings(panel.context)
|
||||
|
||||
// 先盯会话目录再拉起 codex,免得文件在盯上之前就建好了。拿到的 id 存成
|
||||
// reviewSessionId:PR 再更新时靠它往这个窗口发「再审」,窗口关了也能接着聊。
|
||||
// codex 启动可能拖好几分钟,一直盯到这个终端关掉为止。
|
||||
const watchAbort = new AbortController()
|
||||
const watcherPromise = watchForCodexSession({
|
||||
baseDir: defaultCodexSessionsDir(),
|
||||
cwd: effectiveCwd,
|
||||
signal: watchAbort.signal,
|
||||
})
|
||||
|
||||
// Interactive TUI (`codex` without `exec`/`review` subcommands): keeps
|
||||
// the terminal alive after the run so users can follow up with codex,
|
||||
// mirroring how claude implementation/brainstorm sessions stay open.
|
||||
// 提示词模板自带 /review 前缀,不在这里再拼。
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: effectiveCwd,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCodexArgv({
|
||||
mode: 'interactive',
|
||||
prompt: opts.prompt,
|
||||
model: settings.codexModel,
|
||||
reasoningEffort: settings.codexReasoningEffort,
|
||||
})),
|
||||
})
|
||||
const closeListener = window.onDidCloseTerminal((closed) => {
|
||||
if (closed === terminal)
|
||||
watchAbort.abort()
|
||||
})
|
||||
void watcherPromise.finally(() => closeListener.dispose())
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
message: `已创建审查终端 "${terminalName}" cwd=${effectiveCwd}`,
|
||||
message: `已创建审查终端 "${terminalName}" cwd=${effectiveCwd},codex review 已启动`,
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, opts.issueNumber, 'review')
|
||||
terminal.show(false)
|
||||
@@ -570,38 +570,6 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
})
|
||||
}
|
||||
|
||||
// 先盯会话目录再发命令,免得文件在盯上之前就建好了。拿到的 id 存成
|
||||
// reviewSessionId:PR 再更新时靠它往这个窗口发「再审」,窗口关了也能接着聊。
|
||||
// codex 启动可能拖好几分钟,一直盯到这个终端关掉为止。
|
||||
const watchAbort = new AbortController()
|
||||
const closeListener = window.onDidCloseTerminal((closed) => {
|
||||
if (closed === terminal)
|
||||
watchAbort.abort()
|
||||
})
|
||||
const watcherPromise = watchForCodexSession({
|
||||
baseDir: defaultCodexSessionsDir(),
|
||||
cwd: effectiveCwd,
|
||||
signal: watchAbort.signal,
|
||||
}).finally(() => closeListener.dispose())
|
||||
|
||||
// Interactive TUI (`codex` without `exec`/`review` subcommands): keeps
|
||||
// the terminal alive after the run so users can follow up with codex,
|
||||
// mirroring how claude implementation/brainstorm sessions stay open.
|
||||
// 提示词模板自带 /review 前缀,不在这里再拼。
|
||||
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',
|
||||
source: 'terminal',
|
||||
message: `已发送 codex review 命令到 #${opts.issueNumber}-审查 tab`,
|
||||
})
|
||||
|
||||
// Fire-and-forget: when the watcher resolves with a thread_id, persist
|
||||
// it to state JSON + push an issue/patch so the webview shows the resume
|
||||
// button. Failures here don't break the review run itself.
|
||||
@@ -652,8 +620,6 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -748,14 +714,6 @@ export async function handleImplement(
|
||||
|
||||
// 工单级 profilePath > 默认 fallback。工单的 profile 锁定可在工单详情面板里覆盖。
|
||||
const effectiveProfilePath = panel.resolveImplementProfilePath(profilePath)
|
||||
// Same guard as handleResumeSession — we wrap with single quotes and
|
||||
// embedded quotes would break out of the wrap. Defensive only.
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`实施失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const settings = getSettings(panel.context)
|
||||
const port = settings.webhookPort
|
||||
@@ -900,10 +858,6 @@ export async function handleImplement(
|
||||
issueNumber: panel.issueRefLabel(issueNumber),
|
||||
planFile,
|
||||
})
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('实施失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// 实施 cc tab 创建前钩子。tab 复用 fast path(顶部 findExistingTerminal
|
||||
// 命中)走早 return,不会执行到这里,刚好满足"复用不触发"语义。
|
||||
@@ -926,6 +880,7 @@ export async function handleImplement(
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, effort: 'high', prompt })),
|
||||
})
|
||||
terminal.show(false)
|
||||
// Track for the auto-review flow: synchronize callbacks call
|
||||
@@ -938,12 +893,10 @@ export async function handleImplement(
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: 'high', prompt })
|
||||
terminal.sendText(cmd)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'implement',
|
||||
message: `已发送 prompt 到终端 #${issueNumber}`,
|
||||
message: `已启动实施会话 #${issueNumber}`,
|
||||
})
|
||||
|
||||
// Wait in the background for the session jsonl to materialize; merge
|
||||
@@ -1035,16 +988,6 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
|
||||
}
|
||||
|
||||
const effectiveProfilePath = resolveBrainstormProfilePath(panel, brainstormProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`续聊失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('续聊失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// resume 沿用原 id,只有新开会话才要盯着 session jsonl 出现;
|
||||
// 监听要在 createTerminal 之前挂上,否则会漏掉创建事件。
|
||||
@@ -1067,6 +1010,11 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({
|
||||
profilePath: effectiveProfilePath,
|
||||
resumeSessionId: continuePath === 'resume' ? sessionId : undefined,
|
||||
prompt,
|
||||
})),
|
||||
})
|
||||
if (continuePath === 'resume' && sessionId)
|
||||
panel.terminals.set(sessionId, terminal)
|
||||
@@ -1077,18 +1025,12 @@ export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueN
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
terminal.sendText(buildCcCommand({
|
||||
profilePath: effectiveProfilePath,
|
||||
resumeSessionId: continuePath === 'resume' ? sessionId : undefined,
|
||||
prompt,
|
||||
}))
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'brainstorm',
|
||||
message: continuePath === 'resume'
|
||||
? `已带续聊 prompt 恢复 #${issueNumber} 规划会话 ${sessionId}`
|
||||
: `已发送规划 prompt 到终端 #${issueNumber}`,
|
||||
: `已启动 #${issueNumber} 规划会话`,
|
||||
})
|
||||
|
||||
// 拿到 session jsonl 后把 id 写回 state JSON,详情面板据此切到 resume 入口。
|
||||
@@ -1200,12 +1142,6 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
|
||||
// 测试会话只用 testProfilePath;空 = DEFAULT,不回退实施 profile。
|
||||
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`启动测试失败:testProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// worktree 里:代码就在当前工作目录,不用 tea 拉取,直接测;
|
||||
// 主 worktree:PR 已合并,先用 tea 熟悉改动再测。验收目标直接写进提示词,
|
||||
@@ -1217,10 +1153,6 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
? `worktree 分支 ${branch || effectiveCwd}`
|
||||
: `PR #${pr}`,
|
||||
})
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('启动测试失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the projects dir exists so the watcher can't miss the create
|
||||
// event. cwd 决定 claude session jsonl 落在哪个 projects 子目录下。
|
||||
@@ -1254,6 +1186,7 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, prompt })),
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'test')
|
||||
terminal.show(false)
|
||||
@@ -1262,13 +1195,10 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt })
|
||||
terminal.sendText(cmd)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已发送测试 prompt 到终端 #${issueNumber}`,
|
||||
message: `已启动测试会话 #${issueNumber}`,
|
||||
})
|
||||
|
||||
// Fire-and-forget: when the session jsonl materializes, persist the id as
|
||||
@@ -1335,7 +1265,7 @@ export async function handleStartReviewSession(panel: KanbanWebviewPanel, issueN
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await triggerAutoReviewTab(panel, {
|
||||
await triggerAutoReviewTab(panel, {
|
||||
issueNumber,
|
||||
prNumber: pr,
|
||||
prompt: renderReviewPrompt(panel.context, {
|
||||
@@ -1346,8 +1276,6 @@ export async function handleStartReviewSession(panel: KanbanWebviewPanel, issueN
|
||||
worktreePath,
|
||||
workspaceRoot,
|
||||
})
|
||||
if (!ok)
|
||||
void window.showErrorMessage(`启动审查会话失败 #${issueNumber},详见扩展日志`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1406,12 +1334,6 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
|
||||
// --settings 决定 provider/model/鉴权,resume 也必须与 start 一致解析 profile。
|
||||
const effectiveProfilePath = resolveTestProfilePath(panel, testProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:testProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const terminalName = `issue-${issueNumber}-测试`
|
||||
const existingByName = panel.findExistingTerminal(terminalName)
|
||||
@@ -1448,6 +1370,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })),
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'test')
|
||||
terminal.show(false)
|
||||
@@ -1456,9 +1379,6 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
source: 'terminal',
|
||||
message: `已创建测试会话终端 #${issueNumber} cwd=${effectiveCwd}`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })
|
||||
terminal.sendText(cmd)
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
@@ -1533,21 +1453,11 @@ export async function handleStartPrototypeSession(panel: KanbanWebviewPanel, iss
|
||||
|
||||
// 原型会话只用 prototypeProfilePath;空 = DEFAULT,不回退其它会话 profile。
|
||||
const effectiveProfilePath = resolvePrototypeProfilePath(panel, prototypeProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`启动原型失败:prototypeProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const prompt = renderPrototypePrompt(panel.context, {
|
||||
state,
|
||||
issueNumber: panel.issueRefLabel(issueNumber),
|
||||
})
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('启动原型失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the projects dir exists so the watcher can't miss the create
|
||||
// event. cwd 决定 claude session jsonl 落在哪个 projects 子目录下。
|
||||
@@ -1581,6 +1491,7 @@ export async function handleStartPrototypeSession(panel: KanbanWebviewPanel, iss
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, prompt })),
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'prototype')
|
||||
terminal.show(false)
|
||||
@@ -1589,13 +1500,10 @@ export async function handleStartPrototypeSession(panel: KanbanWebviewPanel, iss
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt })
|
||||
terminal.sendText(cmd)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已发送原型 prompt 到终端 #${issueNumber}`,
|
||||
message: `已启动原型会话 #${issueNumber}`,
|
||||
})
|
||||
|
||||
// Fire-and-forget: when the session jsonl materializes, persist the id as
|
||||
@@ -1688,12 +1596,6 @@ export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, se
|
||||
|
||||
// --settings 决定 provider/model/鉴权,resume 也必须与 start 一致解析 profile。
|
||||
const effectiveProfilePath = resolvePrototypeProfilePath(panel, prototypeProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`resume 失败:prototypeProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const terminalName = `issue-${issueNumber}-原型`
|
||||
const existingByName = panel.findExistingTerminal(terminalName)
|
||||
@@ -1728,6 +1630,7 @@ export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, se
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })),
|
||||
})
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'prototype')
|
||||
terminal.show(false)
|
||||
@@ -1736,9 +1639,6 @@ export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, se
|
||||
source: 'terminal',
|
||||
message: `已创建原型会话终端 #${issueNumber} cwd=${effectiveCwd}`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, resumeSessionId: sessionId })
|
||||
terminal.sendText(cmd)
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
@@ -1868,7 +1768,7 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
|
||||
})
|
||||
|
||||
// 4. 启临时 cc 会话(不进 state json、不进任何 map),cwd 设为 worktree。
|
||||
const promptRaw = `当前 git 仓库正在 merge 一个分支但出现了冲突。你的工作目录已经在 worktree 内、分支已经是 ${featureBranch}。你的任务:
|
||||
const prompt = `当前 git 仓库正在 merge 一个分支但出现了冲突。你的工作目录已经在 worktree 内、分支已经是 ${featureBranch}。你的任务:
|
||||
|
||||
1. 跑 \`git status\` 查看冲突文件
|
||||
2. 逐个解决冲突
|
||||
@@ -1881,9 +1781,6 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
|
||||
- 不要 push 到 ${devBranch}
|
||||
- 解决完后告诉用户「冲突已解决,PR #${prIndex} 已更新,请再拖一次工单到完成列」`
|
||||
|
||||
// 单引号 shell 转义:把每个 ' 转成 '\''
|
||||
const prompt = promptRaw.replace(/'/g, '\'\\\'\'')
|
||||
|
||||
// 与实施/测试会话一致:建终端前触发 impl-tab-pre-create 钩子(开 pycharm 等)。
|
||||
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
|
||||
workspaceRoot,
|
||||
@@ -1894,12 +1791,18 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
|
||||
customScriptPath: settings.implTabPreCreateScript,
|
||||
})
|
||||
|
||||
// 冲突解决只用全局设置 conflictResolutionProfilePath;空串 = 默认。
|
||||
// 工单/设置里可能持久化了跨机绝对路径,统一走 resolveProfilePath remapping。
|
||||
const effectiveProfilePath = resolveProfilePath(
|
||||
settings.conflictResolutionProfilePath.trim() || undefined,
|
||||
)
|
||||
let terminal: Terminal
|
||||
try {
|
||||
terminal = window.createTerminal({
|
||||
name: `issue-${issueNumber}-冲突解决`,
|
||||
cwd: worktreeAbs,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, effort: 'high', prompt })),
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -1920,29 +1823,6 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
|
||||
return
|
||||
}
|
||||
terminal.show(false)
|
||||
// 冲突解决只用全局设置 conflictResolutionProfilePath;空串 = 默认。
|
||||
// 工单/设置里可能持久化了跨机绝对路径,统一走 resolveProfilePath remapping。
|
||||
const effectiveProfilePath = resolveProfilePath(
|
||||
settings.conflictResolutionProfilePath.trim() || undefined,
|
||||
)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
logger.add({
|
||||
level: 'error',
|
||||
source: 'panel',
|
||||
message: `冲突解决:profilePath 含单引号,拒绝执行 (issue #${issueNumber})`,
|
||||
details: effectiveProfilePath,
|
||||
})
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: `冲突解决失败:profilePath 含单引号 (${effectiveProfilePath})`,
|
||||
dismissOnTimer: 6000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, effort: 'high', prompt })
|
||||
terminal.sendText(cmd)
|
||||
|
||||
logger.add({
|
||||
level: 'info',
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface TabLike {
|
||||
|
||||
/**
|
||||
* `TabInputTerminal` 没有任何字段,只能用 tab.label 反查 `window.terminals`。
|
||||
* 终端 name 常被 shell 的 OSC title 序列改写(追加 git branch 等后缀),
|
||||
* 终端 name 可能被 OSC 标题序列改写(在名字后追加后缀),
|
||||
* 精确匹配失败后再双向 startsWith 兜底。
|
||||
*/
|
||||
export function findTerminalByTabLabel<T extends TerminalLike>(terminals: readonly T[], label: string): T | undefined {
|
||||
|
||||
@@ -47,8 +47,8 @@ export function injectIntoImplTerminal(panel: KanbanWebviewPanel, issueNumber: n
|
||||
void isFirstReview
|
||||
let terminal = panel.implTerminals.get(issueNumber)
|
||||
if (!terminal) {
|
||||
// Match by prefix — shell OSC title escapes can append a git branch
|
||||
// suffix to terminal.name (e.g. "issue-48-实施 5f56026c").
|
||||
// Match by prefix — OSC title escapes can append a suffix to
|
||||
// terminal.name (e.g. "issue-48-实施 5f56026c").
|
||||
const wantedPrefix = `issue-${issueNumber}-实施`
|
||||
for (const t of window.terminals) {
|
||||
if (t.name.startsWith(wantedPrefix)) {
|
||||
@@ -88,7 +88,7 @@ export function submitToSessionTerminal(terminal: Terminal, text: string): void
|
||||
* 本机是否开着该工单的存活终端(exitStatus undefined)——webhook 门禁对
|
||||
* 无 assignee 存量工单的兜底证据。不依赖 panel:看板关闭时实施终端
|
||||
* 仍活着,归属判定不能因此失明。前缀匹配策略与 injectIntoImplTerminal
|
||||
* 一致(shell OSC 会在名字后追加分支后缀)。
|
||||
* 一致(终端标题序列可能在名字后追加后缀)。
|
||||
*/
|
||||
export function hasLiveIssueSessionTerminal(
|
||||
issueNumber: number,
|
||||
@@ -103,7 +103,7 @@ export function hasLiveIssueSessionTerminal(
|
||||
/**
|
||||
* Scan `vscode.window.terminals` for an existing terminal whose name
|
||||
* matches `expectedName`. Matches exact, or `startsWith(expectedName + ' ')`
|
||||
* so a shell that appended a git branch suffix (e.g. "issue-48-实施
|
||||
* so a title sequence that appended a suffix (e.g. "issue-48-实施
|
||||
* 5f56026c") still counts. Skips terminals whose process already exited
|
||||
* (`exitStatus !== undefined`) — those are zombie tabs and shouldn't block
|
||||
* a fresh spawn.
|
||||
|
||||
@@ -83,8 +83,6 @@ async function handleCreateSessionUri(uri: Uri): Promise<void> {
|
||||
panel: null,
|
||||
register: false,
|
||||
})
|
||||
if (!created)
|
||||
return
|
||||
|
||||
logger.add({
|
||||
level: 'info',
|
||||
|
||||
@@ -23,6 +23,7 @@ import { detectRepo } from '../git/remote'
|
||||
import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api'
|
||||
import { systemCommentText } from '../gitea/issueComment'
|
||||
import { loadIssues, loadSingleIssue } from '../gitea/issueLoader'
|
||||
import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from '../gitea/markers'
|
||||
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||
import { logger } from '../logging/logger'
|
||||
@@ -357,9 +358,8 @@ class WebhookCoordinator {
|
||||
return event.issueNumber
|
||||
|
||||
const body = event.body || ''
|
||||
const m = body.match(/\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i)
|
||||
if (m) {
|
||||
const n = Number.parseInt(m[1], 10)
|
||||
const n = parseClosingIssueNumber(body)
|
||||
if (n !== undefined) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
@@ -1449,9 +1449,9 @@ class WebhookCoordinator {
|
||||
repo: ctx.repo,
|
||||
index: event.prNumber,
|
||||
})
|
||||
const m = (pr.body ?? '').match(/\\b(?:closes|fixes|resolves|close|fix|resolve)\\s+#(\\d+)/i)
|
||||
if (m) {
|
||||
realIssueNumber = Number.parseInt(m[1], 10)
|
||||
const closing = parseClosingIssueNumber(pr.body ?? '')
|
||||
if (closing !== undefined) {
|
||||
realIssueNumber = closing
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
@@ -1532,7 +1532,7 @@ class WebhookCoordinator {
|
||||
})
|
||||
}
|
||||
|
||||
if (!/<!--\\s+px:review=1\\s*-->/i.test(body)) {
|
||||
if (!hasReviewMarker(body)) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
@@ -1540,7 +1540,7 @@ class WebhookCoordinator {
|
||||
})
|
||||
return
|
||||
}
|
||||
const text = body.replace(/<!--\\s+px:review=1\\s*-->\\s*/i, '').trim()
|
||||
const text = stripReviewMarker(body)
|
||||
if (!text) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
@@ -1570,7 +1570,7 @@ class WebhookCoordinator {
|
||||
repo: ctx.repo,
|
||||
index: commentsIndex,
|
||||
})
|
||||
reviewCount = comments.filter(c => /<!--\\s+px:review=1\\s*-->/i.test(c.body ?? '')).length
|
||||
reviewCount = comments.filter(c => hasReviewMarker(c.body ?? '')).length
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
@@ -1751,21 +1751,13 @@ class WebhookCoordinator {
|
||||
|
||||
const prompt = renderReviewPrompt(this.ctx, { state, issueNumber: String(issueNumber), prNumber })
|
||||
|
||||
const ok = await this.activePanel.triggerAutoReviewTab({
|
||||
await this.activePanel.triggerAutoReviewTab({
|
||||
issueNumber,
|
||||
prNumber,
|
||||
prompt,
|
||||
worktreePath,
|
||||
workspaceRoot,
|
||||
})
|
||||
if (!ok) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'webhook',
|
||||
message: `triggerAutoReviewTab 失败 #${issueNumber}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.add({
|
||||
level: 'info',
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildCcCommand } from '../../src/cc/ccCommand'
|
||||
import { buildCcArgv } from '../../src/cc/ccCommand'
|
||||
|
||||
vi.mock('../../src/cc/claudeBin', () => ({
|
||||
resolveClaudeBin: () => 'claude',
|
||||
resolveClaudeBin: () => '/Users/me/My Apps/claude',
|
||||
}))
|
||||
|
||||
describe('buildCcCommand', () => {
|
||||
const BASE = ['/Users/me/My Apps/claude', '--dangerously-skip-permissions', '--settings', '/p.json']
|
||||
|
||||
describe('buildCcArgv', () => {
|
||||
it('appends the prompt as a positional arg for a fresh session', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', prompt: 'hello' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' \'hello\'')
|
||||
expect(buildCcArgv({ profilePath: '/p.json', prompt: 'hello' }))
|
||||
.toEqual([...BASE, 'hello'])
|
||||
})
|
||||
|
||||
it('resumes without a prompt when only a session id is given', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', resumeSessionId: 'abc' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' --resume abc')
|
||||
expect(buildCcArgv({ profilePath: '/p.json', resumeSessionId: 'abc' }))
|
||||
.toEqual([...BASE, '--resume', 'abc'])
|
||||
})
|
||||
|
||||
it('resumes and hands the prompt to the restored session', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', resumeSessionId: 'abc', prompt: 'go on' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' --resume abc \'go on\'')
|
||||
expect(buildCcArgv({ profilePath: '/p.json', resumeSessionId: 'abc', prompt: 'go on' }))
|
||||
.toEqual([...BASE, '--resume', 'abc', 'go on'])
|
||||
})
|
||||
|
||||
it('puts effort first and keeps quotes / $ / newlines in paths and prompt as-is', () => {
|
||||
const prompt = 'don\'t $touch\n"this"'
|
||||
expect(buildCcArgv({ profilePath: '/it\'s/p.json', effort: 'high', prompt })).toEqual([
|
||||
'/Users/me/My Apps/claude',
|
||||
'--effort',
|
||||
'high',
|
||||
'--dangerously-skip-permissions',
|
||||
'--settings',
|
||||
'/it\'s/p.json',
|
||||
prompt,
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user