🐛 fix(vscode): 修复新建工单后会话关联丢失
This commit is contained in:
1 parent
172e6ba77d
commit
9345b6a16e
12 files changed
+836
-435
No files matched your search
@@ -54,7 +54,7 @@ vi.mock('../sessions/sessionFiles', () => ({ annotateMissingSessionIds: mocks.an
|
||||
vi.mock('../settings/store', () => ({
|
||||
getSettings: () => ({ autoReview: true, youtrackBaseUrl: '', youtrackProjectShortName: '' }),
|
||||
}))
|
||||
vi.mock('../webhook/coordinator', () => ({ webhookCoordinator: { setActivePanel: mocks.setActivePanel } }))
|
||||
vi.mock('../webhook/coordinator', () => ({ webhookCoordinator: { setActivePanel: mocks.setActivePanel, restoreIssueCreationTerminals: vi.fn() } }))
|
||||
vi.mock('../webhook/server', () => ({
|
||||
WebhookServer: class {
|
||||
setSecret() {}
|
||||
|
||||
@@ -117,45 +117,7 @@ export class KanbanWebviewPanel {
|
||||
// internal: handler 模块访问
|
||||
readonly managedTerminals = new Map<string, Terminal>()
|
||||
|
||||
/**
|
||||
* Tracks in-flight "create issue" runs keyed by the nonce embedded in the
|
||||
* cc prompt. Populated synchronously in `handleIssueCreate` before the
|
||||
* terminal is shown; the session watcher fills in `sessionId` once cc
|
||||
* starts writing its jsonl. The webhook coordinator drains entries via
|
||||
* `takePendingIssueCreation` when the corresponding `issues opened`
|
||||
* payload arrives, then merges the column / sessionId / brainstormProfilePath /
|
||||
* color into the state-JSON comment and cleans up the inbox tmpdir.
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
readonly pendingIssueCreations = new Map<string, {
|
||||
sessionId?: string
|
||||
brainstormProfilePath?: string
|
||||
/** Palette id (e.g. `terminal.ansiBlue`) — same shape stored in state JSON. */
|
||||
color: string
|
||||
workspaceRoot: string
|
||||
inboxDir: string
|
||||
terminalName: string
|
||||
/**
|
||||
* The brainstorm terminal created synchronously in `handleIssueCreate`.
|
||||
* Stored here so `linkPendingTerminalToIssue` can promote it into
|
||||
* `newIssueTerminals` once the webhook tells us the issueNumber.
|
||||
*/
|
||||
terminal: Terminal
|
||||
createdAt: number
|
||||
}>()
|
||||
|
||||
/**
|
||||
* issueNumber → the brainstorm terminal originally created via the new-issue
|
||||
* flow (its name is `issue-new-{shortNonce}-规划`, NOT `issue-${N}-规划`).
|
||||
* Populated by `linkPendingTerminalToIssue` after the webhook coordinator
|
||||
* matches the nonce in `issue.body` to an entry in `pendingIssueCreations`.
|
||||
*
|
||||
* VS Code's Terminal API has no rename method, so we can't fix the tab name
|
||||
* after the fact. Instead we keep this side-map so `handleSessionFocus`
|
||||
* (card → terminal) and `handleActiveTerminalChanged` (terminal → card) can
|
||||
* still find the tab and round-trip selection.
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
/** 保存新建工单对应的终端,让卡片与规划会话能够互相定位。 */
|
||||
readonly newIssueTerminals = new Map<number, Terminal>()
|
||||
|
||||
/**
|
||||
@@ -1040,6 +1002,7 @@ export class KanbanWebviewPanel {
|
||||
this.activeLoad = undefined
|
||||
this.loadInFlight = undefined
|
||||
this.postMessage({ type: 'issues/loaded' })
|
||||
webhookCoordinator.restoreIssueCreationTerminals(this)
|
||||
})
|
||||
return this.loadInFlight
|
||||
}
|
||||
@@ -1380,43 +1343,6 @@ export class KanbanWebviewPanel {
|
||||
terminals.untrackClosedTerminal(this, closed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the {@link pendingIssueCreations} entry for the
|
||||
* given nonce, if any. Called by the webhook coordinator when a matching
|
||||
* `issues opened` payload arrives. Returning `undefined` (and leaving the
|
||||
* map untouched) signals "no match — treat as external issue creation".
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
takePendingIssueCreation(nonce: string): {
|
||||
sessionId?: string
|
||||
brainstormProfilePath?: string
|
||||
color: string
|
||||
workspaceRoot: string
|
||||
inboxDir: string
|
||||
terminalName: string
|
||||
terminal: Terminal
|
||||
createdAt: number
|
||||
} | undefined {
|
||||
return terminals.takePendingIssueCreation(this, nonce)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the webhook coordinator after it parses a `spx:nonce=...` token
|
||||
* out of `issue.body` and matches it to an entry in `pendingIssueCreations`.
|
||||
* Promotes the in-flight brainstorm terminal into the issueNumber-keyed
|
||||
* `newIssueTerminals` map so subsequent `handleSessionFocus(issueNumber)`
|
||||
* and `handleActiveTerminalChanged` calls can find it.
|
||||
*
|
||||
* Also drops the terminal into `terminals` keyed by sessionId (when known)
|
||||
* so `handleResumeSession` reuses the same tab instead of spawning a new
|
||||
* one. Does NOT remove the pending entry — `takePendingIssueCreation` is
|
||||
* still responsible for cleanup on its own path.
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
public linkPendingTerminalToIssue(nonce: string, issueNumber: number): void {
|
||||
terminals.linkPendingTerminalToIssue(this, nonce, issueNumber)
|
||||
}
|
||||
|
||||
private dispose(): void {
|
||||
if (this.disposed)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { handleIssueCreate } from './issues'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mkdir: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
createTerminal: vi.fn(),
|
||||
register: vi.fn(),
|
||||
attach: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
watch: vi.fn(),
|
||||
log: vi.fn(),
|
||||
prompt: vi.fn(),
|
||||
}))
|
||||
vi.mock('vscode', () => ({
|
||||
commands: {},
|
||||
env: { shell: '/bin/zsh' },
|
||||
Uri: {},
|
||||
ThemeColor: class { constructor(public id: string) {} },
|
||||
window: { createTerminal: mocks.createTerminal },
|
||||
workspace: { workspaceFolders: [{ uri: { fsPath: '/repo' } }] },
|
||||
}))
|
||||
vi.mock('node:fs', async importOriginal => ({
|
||||
...(await importOriginal<typeof import('node:fs')>()),
|
||||
promises: { ...(await importOriginal<typeof import('node:fs')>()).promises, mkdir: mocks.mkdir, rm: mocks.rm },
|
||||
}))
|
||||
vi.mock('../../auth/secrets', () => ({ getToken: async () => 'token' }))
|
||||
vi.mock('../../cc/claudeBin', () => ({ resolveClaudeBin: () => '/bin/claude' }))
|
||||
vi.mock('../../cc/prompts', () => ({ renderBrainstormPrompt: mocks.prompt }))
|
||||
vi.mock('../../cc/sessionWatcher', () => ({ projectsDirFor: () => '/sessions', watchForNewSession: mocks.watch }))
|
||||
vi.mock('../../git/remote', () => ({ detectRepo: async () => ({ host: 'host', owner: 'owner', repo: 'repo' }) }))
|
||||
vi.mock('../../logging/logger', () => ({ logger: { add: mocks.log } }))
|
||||
vi.mock('../../settings/store', () => ({ getSettings: () => ({ webhookPort: 12345 }) }))
|
||||
vi.mock('../../webhook/coordinator', () => ({ webhookCoordinator: {
|
||||
ensurePort: async () => undefined,
|
||||
registerIssueCreation: mocks.register,
|
||||
attachIssueCreationTerminal: mocks.attach,
|
||||
cancelIssueCreation: mocks.cancel,
|
||||
} }))
|
||||
vi.mock('../../webview/html', () => ({ makeNonce: () => 'toast' }))
|
||||
vi.mock('../issueColor', () => ({ pickRandomIssueColor: () => 'terminal.ansiBlue', themeColorIdToIconUri: () => 'icon' }))
|
||||
vi.mock('./sessions', () => ({ resolveBrainstormProfilePath: () => '/profiles/brainstorm.json' }))
|
||||
|
||||
function fixture() {
|
||||
const panel = { context: {}, resolveTerminalLocation: () => ({ viewColumn: 1 }), postMessage: vi.fn() } as unknown as KanbanWebviewPanel
|
||||
const terminal = { name: 'new issue', show: vi.fn(), dispose: vi.fn() }
|
||||
mocks.createTerminal.mockReturnValue(terminal)
|
||||
return { panel, terminal }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.mkdir.mockResolvedValue(undefined)
|
||||
mocks.rm.mockResolvedValue(undefined)
|
||||
mocks.register.mockReset()
|
||||
mocks.attach.mockReset()
|
||||
mocks.cancel.mockReset()
|
||||
mocks.createTerminal.mockReset()
|
||||
mocks.watch.mockReturnValue(new Promise(() => {}))
|
||||
mocks.prompt.mockReturnValue('create issue')
|
||||
})
|
||||
|
||||
describe('create issue with a predetermined session', () => {
|
||||
it('registers an independent UUID before terminal creation and passes exactly that identity', async () => {
|
||||
const f = fixture()
|
||||
await handleIssueCreate(f.panel, 'create this', undefined, '/profiles/chosen.json')
|
||||
expect(mocks.register).toHaveBeenCalledOnce()
|
||||
const [nonce, record] = mocks.register.mock.calls[0]
|
||||
expect(record.sessionId).toMatch(/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/)
|
||||
expect(record.sessionId).not.toBe(nonce)
|
||||
expect(record).toMatchObject({ remote: { host: 'host', owner: 'owner', repo: 'repo' }, workspaceRoot: '/repo', brainstormProfilePath: '/profiles/chosen.json' })
|
||||
expect(mocks.register.mock.invocationCallOrder[0]).toBeLessThan(mocks.createTerminal.mock.invocationCallOrder[0])
|
||||
const args = mocks.createTerminal.mock.calls[0][0].shellArgs as string[]
|
||||
expect(args[args.indexOf('--session-id') + 1]).toBe(record.sessionId)
|
||||
expect(mocks.attach).toHaveBeenCalledWith(nonce, f.terminal)
|
||||
expect(mocks.prompt.mock.calls[0][1].nonce).toBe(nonce)
|
||||
expect(mocks.watch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('separate creations never share a session identity', async () => {
|
||||
const f = fixture()
|
||||
await handleIssueCreate(f.panel, 'first')
|
||||
await handleIssueCreate(f.panel, 'second')
|
||||
expect(mocks.register).toHaveBeenCalledTimes(2)
|
||||
const ids = mocks.register.mock.calls.map(([, record]) => record.sessionId)
|
||||
expect(new Set(ids).size).toBe(2)
|
||||
})
|
||||
|
||||
it.each(['create', 'show'])('cancels and reports a terminal %s failure', async (failure) => {
|
||||
const f = fixture()
|
||||
if (failure === 'create') {
|
||||
mocks.createTerminal.mockImplementation(() => {
|
||||
throw new Error('terminal unavailable')
|
||||
})
|
||||
}
|
||||
else {
|
||||
f.terminal.show.mockImplementation(() => {
|
||||
throw new Error('terminal unavailable')
|
||||
})
|
||||
}
|
||||
await handleIssueCreate(f.panel, 'create this')
|
||||
expect(mocks.cancel).toHaveBeenCalledWith(mocks.register.mock.calls[0][0])
|
||||
expect(f.panel.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'toast/show', level: 'error', message: expect.stringContaining('terminal unavailable') }))
|
||||
expect(mocks.rm).toHaveBeenCalledWith(expect.any(String), { recursive: true, force: true })
|
||||
if (failure === 'show')
|
||||
expect(f.terminal.dispose).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -16,7 +16,7 @@ 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 { projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { spawnClaude } from '../../cc/spawnClaude'
|
||||
import { gitFetch, resolveFeatureBranch } from '../../git/branches'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
} from '../../gitea/api'
|
||||
import { isValidSpxFilePath, loadIssues } from '../../gitea/issueLoader'
|
||||
import { mergeStateJsonComment } from '../../gitea/stateJson'
|
||||
import { mergeLocalIssueState } from '../../issues/localState'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store'
|
||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||
@@ -44,7 +43,6 @@ import { evaluateLockedReason } from '../issueLock'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
import { clearSessionConfirmText, clearSessionDoneText, SESSION_KINDS } from './sessionKinds'
|
||||
import * as sessions from './sessions'
|
||||
import { issueNumberForNewIssueTerminal } from './terminals'
|
||||
import { canResetToTodo, todoResetStateExtra, todoResetUiPatch } from './todoReset'
|
||||
|
||||
export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: number, toColumn: IssueColumn): Promise<void> {
|
||||
@@ -1366,8 +1364,7 @@ export async function handleIssueCreate(
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the always-on webhook listener is up so we can receive the
|
||||
// `issues opened` callback that drives state-JSON fill-in.
|
||||
// 先启动工单通知接收器,确保新工单能关联到本次会话。
|
||||
const settings = getSettings(panel.context)
|
||||
try {
|
||||
await webhookCoordinator.ensurePort(settings.webhookPort)
|
||||
@@ -1385,6 +1382,7 @@ export async function handleIssueCreate(
|
||||
}
|
||||
|
||||
const nonce = randomUUID()
|
||||
const sessionId = randomUUID()
|
||||
const shortNonce = nonce.slice(0, 8)
|
||||
const inboxDir = path.join(os.tmpdir(), 'spx-inbox', nonce)
|
||||
try {
|
||||
@@ -1445,54 +1443,54 @@ export async function handleIssueCreate(
|
||||
// 新建工单弹窗里选的 profile > 设置里的「头脑风暴默认配置文件」> DEFAULT。
|
||||
const effectiveProfilePath = sessions.resolveBrainstormProfilePath(panel, brainstormProfilePath)
|
||||
|
||||
// Ensure the claude projects subdir exists *before* spawning the
|
||||
// terminal so the watcher can't miss the create event.
|
||||
const projDir = projectsDirFor(workspaceRoot)
|
||||
try {
|
||||
await fsp.mkdir(projDir, { recursive: true })
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to mkdir claude projects dir:', err)
|
||||
}
|
||||
const watchPromise = watchForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
|
||||
const terminalName = `issue-new-${shortNonce}-规划`
|
||||
const themeColor = new ThemeColor(color)
|
||||
const iconUri = themeColorIdToIconUri(color)
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
cwd: workspaceRoot,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, prompt })),
|
||||
})
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
// 新建工单会话是头脑风暴;只把用户选中的 brainstorm profile 写入 pending,
|
||||
// 空串不落盘(webhook 侧也会跳过空值),实施/测试 profile 各自独立配置。
|
||||
const storedBrainstormProfile
|
||||
= brainstormProfilePath && brainstormProfilePath.trim() !== '' ? brainstormProfilePath.trim() : undefined
|
||||
panel.pendingIssueCreations.set(nonce, {
|
||||
const storedBrainstormProfile = brainstormProfilePath?.trim() || undefined
|
||||
webhookCoordinator.registerIssueCreation(nonce, {
|
||||
sessionId,
|
||||
remote,
|
||||
brainstormProfilePath: storedBrainstormProfile,
|
||||
color,
|
||||
workspaceRoot,
|
||||
inboxDir,
|
||||
terminalName,
|
||||
terminal,
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
|
||||
let terminal: ReturnType<typeof window.createTerminal> | undefined
|
||||
try {
|
||||
terminal = window.createTerminal({
|
||||
name: `issue-new-${shortNonce}-规划`,
|
||||
cwd: workspaceRoot,
|
||||
location: panel.resolveTerminalLocation(false),
|
||||
iconPath: themeColorIdToIconUri(color),
|
||||
color: new ThemeColor(color),
|
||||
...sessionLaunch(buildCcArgv({ profilePath: effectiveProfilePath, sessionId, prompt })),
|
||||
})
|
||||
webhookCoordinator.attachIssueCreationTerminal(nonce, terminal)
|
||||
terminal.show(false)
|
||||
}
|
||||
catch (err) {
|
||||
webhookCoordinator.cancelIssueCreation(nonce)
|
||||
terminal?.dispose()
|
||||
try {
|
||||
await fsp.rm(inboxDir, { recursive: true, force: true })
|
||||
}
|
||||
catch (cleanupError) {
|
||||
logger.add({ level: 'warn', source: 'panel', message: '新建工单临时目录清理失败', details: String(cleanupError) })
|
||||
}
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: `issue-new-${shortNonce}`,
|
||||
level: 'error',
|
||||
message: `新建工单会话启动失败: ${err instanceof Error ? err.message : String(err)}`,
|
||||
dismissOnTimer: 8000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已启动新建工单会话 nonce=${shortNonce}`,
|
||||
details: `sessionId=${sessionId} terminal=${terminal.name}`,
|
||||
})
|
||||
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: `issue-new-${shortNonce}`,
|
||||
@@ -1501,56 +1499,6 @@ export async function handleIssueCreate(
|
||||
spinner: true,
|
||||
dismissOnTimer: 4000,
|
||||
})
|
||||
|
||||
// 工单往往比会话文件先出现。webhook 把 pending 取走时 sessionId 还空,
|
||||
// 这里在文件落地后补写本机会话 id。
|
||||
watchPromise.then(async (sid) => {
|
||||
if (!sid) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `新建工单会话监听超时 nonce=${shortNonce}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const pending = panel.pendingIssueCreations.get(nonce)
|
||||
if (pending) {
|
||||
pending.sessionId = sid
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `已捕获新建工单会话 ${sid} nonce=${shortNonce}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const issueNumber = issueNumberForNewIssueTerminal(panel, terminal)
|
||||
if (issueNumber === undefined) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `新建工单会话 ${sid} 已捕获,但 nonce=${shortNonce} 还没对上工单`,
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const state = await panel.readIssueState(issueNumber)
|
||||
if (typeof state.sessionId === 'string' && state.sessionId.length > 0)
|
||||
return
|
||||
}
|
||||
catch {
|
||||
// 本机状态还没有,照写
|
||||
}
|
||||
await mergeLocalIssueState(panel.context, 'gitea', issueNumber, { sessionId: sid })
|
||||
panel.terminals.set(sid, terminal)
|
||||
panel.postMessage({ type: 'issue/patch', issueNumber, patch: { sessionId: sid } })
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'panel',
|
||||
message: `补写工单 #${issueNumber} 头脑风暴会话 ${sid}`,
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.warn('[superpowers] new-issue session watch failed:', err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,11 +137,7 @@ export function handleSessionFocus(panel: KanbanWebviewPanel, issueNumber: numbe
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Priority 0: new-issue flow terminal whose name is `issue-new-{nonce}-规划`,
|
||||
// not `issue-${N}-规划`. The webhook coordinator stitches issueNumber →
|
||||
// terminal into `newIssueTerminals` via `linkPendingTerminalToIssue`.
|
||||
// We can't rename the terminal tab (VS Code API limitation), so this
|
||||
// side-map is the only way to find it by issueNumber.
|
||||
// 新建会话的终端名不含工单号,必须通过已保存的映射定位。
|
||||
const newIssueTerm = panel.newIssueTerminals.get(issueNumber)
|
||||
if (newIssueTerm && newIssueTerm.exitStatus === undefined) {
|
||||
if (newIssueTerm !== window.activeTerminal)
|
||||
@@ -190,9 +186,7 @@ export function handleActiveTerminalChanged(panel: KanbanWebviewPanel, terminal:
|
||||
}
|
||||
return
|
||||
}
|
||||
// Fallback: `issue-new-${shortNonce}-规划` — created via the new-issue flow
|
||||
// before we knew the issueNumber. Reverse-scan `newIssueTerminals` (which
|
||||
// was populated by the webhook coordinator via `linkPendingTerminalToIssue`).
|
||||
// 从新建终端反查所属工单,让切换终端时看板也选中相应卡片。
|
||||
if (terminal.name.startsWith('issue-new-')) {
|
||||
for (const [num, term] of panel.newIssueTerminals) {
|
||||
if (term === terminal) {
|
||||
@@ -355,54 +349,16 @@ export function handleCloseSessionTab(panel: KanbanWebviewPanel, issueNumber: nu
|
||||
terminal.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the {@link pendingIssueCreations} entry for the
|
||||
* given nonce, if any. Called by the webhook coordinator when a matching
|
||||
* `issues opened` payload arrives. Returning `undefined` (and leaving the
|
||||
* map untouched) signals "no match — treat as external issue creation".
|
||||
*/
|
||||
export function takePendingIssueCreation(panel: KanbanWebviewPanel, nonce: string): {
|
||||
sessionId?: string
|
||||
brainstormProfilePath?: string
|
||||
color: string
|
||||
workspaceRoot: string
|
||||
inboxDir: string
|
||||
terminalName: string
|
||||
terminal: Terminal
|
||||
createdAt: number
|
||||
} | undefined {
|
||||
const entry = panel.pendingIssueCreations.get(nonce)
|
||||
if (entry)
|
||||
panel.pendingIssueCreations.delete(nonce)
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the webhook coordinator after it parses a `spx:nonce=...` token
|
||||
* out of `issue.body` and matches it to an entry in `pendingIssueCreations`.
|
||||
* Promotes the in-flight brainstorm terminal into the issueNumber-keyed
|
||||
* `newIssueTerminals` map so subsequent `handleSessionFocus(issueNumber)`
|
||||
* and `handleActiveTerminalChanged` calls can find it.
|
||||
*
|
||||
* Also drops the terminal into `terminals` keyed by sessionId (when known)
|
||||
* so `handleResumeSession` reuses the same tab instead of spawning a new
|
||||
* one. Does NOT remove the pending entry — `takePendingIssueCreation` is
|
||||
* still responsible for cleanup on its own path.
|
||||
*/
|
||||
export function linkPendingTerminalToIssue(panel: KanbanWebviewPanel, nonce: string, issueNumber: number): void {
|
||||
const pending = panel.pendingIssueCreations.get(nonce)
|
||||
if (!pending)
|
||||
/** 将已知会话绑定到工单,让卡片和仍打开的终端能够互相定位。 */
|
||||
export function linkIssueCreationTerminal(
|
||||
panel: KanbanWebviewPanel,
|
||||
creation: { sessionId: string, terminal?: Terminal },
|
||||
issueNumber: number,
|
||||
): void {
|
||||
const terminal = creation.terminal
|
||||
if (!terminal || terminal.exitStatus !== undefined || !window.terminals.includes(terminal))
|
||||
return
|
||||
panel.newIssueTerminals.set(issueNumber, pending.terminal)
|
||||
if (pending.sessionId && pending.sessionId.length > 0)
|
||||
panel.terminals.set(pending.sessionId, pending.terminal)
|
||||
trackSessionTerminal(panel, pending.terminal, issueNumber, 'brainstorm')
|
||||
}
|
||||
|
||||
export function issueNumberForNewIssueTerminal(panel: KanbanWebviewPanel, terminal: Terminal): number | undefined {
|
||||
for (const [issueNumber, linked] of panel.newIssueTerminals) {
|
||||
if (linked === terminal)
|
||||
return issueNumber
|
||||
}
|
||||
return undefined
|
||||
panel.newIssueTerminals.set(issueNumber, terminal)
|
||||
panel.terminals.set(creation.sessionId, terminal)
|
||||
trackSessionTerminal(panel, terminal, issueNumber, 'brainstorm')
|
||||
}
|
||||
Reference in new issue
Block a user