🐛 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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user