🐛 fix(vscode): 修好单个会话 tab 从看板点回时不反向选中所属工单

Claude-Session: https://claude.ai/code/session_01FAA5YEyR7fzWj7gYQiLSqy
This commit is contained in:
2026-08-26 13:42:30 +08:00
parent eee0170a63
commit 06fb5ecec2
4 changed files with 157 additions and 77 deletions
+23 -51
View File
@@ -33,6 +33,7 @@ import * as profileAssets from './handlers/profileAssets'
import * as sessions from './handlers/sessions'
import * as settings from './handlers/settings'
import * as terminals from './handlers/terminals'
import { focusedTerminal, FocusedTerminalTracker } from './handlers/terminalTabs'
import * as toolbar from './handlers/toolbar'
import * as worktree from './handlers/worktree'
import * as youtrackIssues from './handlers/youtrackIssues'
@@ -195,7 +196,7 @@ export class KanbanWebviewPanel {
gitStateDisposable: { dispose: () => void } | undefined
/**
* 最近一次 onDidChangeActiveTerminal 触发反选 webview 的时间戳(ms epoch)。
* 最近一次终端 tab 切换触发反选 webview 的时间戳(ms epoch)。
* `handleSessionFocus` 收到 webview 回发的 session/focus 时检查这个,
* 距离 <200ms 且工单号相同 → 跳过优先级跳转,避免点审查 tab 自动弹回实施 tab。
*/
@@ -205,22 +206,11 @@ export class KanbanWebviewPanel {
lastReverseSelectIssueNumber = -1
/**
* 上一次被 `handleActiveTerminalChanged` 处理的 terminal 引用。同一 terminal
* 短时间内重复触发(OSC title 改写、shell prompt 重绘等导致的 onDidChangeTabs
* 误触)直接 noop,避免 webview 反复 setPendingSelectId 造成 UI 闪烁
* `onDidCloseTerminal` 中清掉该 ref。
* 当前有焦点的会话终端 tab。由 tab 事件推导「活动分组的活动 tab」再做 diff,
* 只有焦点真正落到另一个终端 tab 时才反向选中工单:OSC title 改写不会重复
* 触发,单 tab 分组从看板点回来(无 isActive / activeTerminal 变化)也能触发
*/
// internal: handler 模块访问
lastActiveTerminalRef: Terminal | undefined
/**
* 我们自己 `show()` 引发的 active terminal 变更会回声到
* `handleActiveTerminalChanged`,若再反向选中工单就会和正向(选中→聚焦)
* 形成死循环、CPU 飙升。handleSessionFocus show 前把目标终端记在这里,
* handleActiveTerminalChanged 命中即吞掉这一次回声(基于引用、不靠时间窗)。
*/
// internal: handler 模块访问
programmaticTabReveal: Terminal | undefined
private readonly focusedTerminalTracker = new FocusedTerminalTracker<Terminal>()
// internal: context 供 handler 模块访问
private constructor(public readonly context: ExtensionContext, panel: WebviewPanel) {
@@ -285,8 +275,6 @@ export class KanbanWebviewPanel {
const origin = this.terminalOrigin.get(closed)
// 反查 terminalOrigin 给详情面板推 *TabOpen: false,让关闭按钮消失。
this.untrackClosedTerminal(closed)
if (closed === this.lastActiveTerminalRef)
this.lastActiveTerminalRef = undefined
if (origin?.kind === 'implement' || origin?.kind === 'test')
void this.dispatchImplTabPostCloseAsync(origin.issueNumber)
// 冲突解决会话不进 terminalOrigin,按终端名触发 post-close 钩子。
@@ -294,39 +282,12 @@ export class KanbanWebviewPanel {
if (conflictMatch)
void this.dispatchImplTabPostCloseAsync(Number(conflictMatch[1]))
}),
// 用户 column 2 切换终端 tab 时,反向选中看板上对应的工单卡片。
window.onDidChangeActiveTerminal((terminal) => {
if (!terminal)
return
this.handleActiveTerminalChanged(terminal)
}),
// 补充监听 tab 切换事件:当 column 2 只有一个 terminal tab 时,
// 它始终是 active terminal`onDidChangeActiveTerminal` 不会触发;
// 但用户点击该 tab 仍会让它成为 active tab`onDidChangeTabs` 会以
// `changed` + `isActive=true` 的形式投递事件。两者并存时
// `handleActiveTerminalChanged` 内的 `lastReverseSelectAt` 时间窗
// 已能去重,不会回环放大。
window.tabGroups.onDidChangeTabs((e) => {
// e.opened 也要处理:新开的终端 tab(如双击会话 id 打开)走 opened,
// 不在 changed 里,且 show(preserveFocus) 不一定触发 onDidChangeActiveTerminal
// 否则新 tab 不会反向选中对应工单。
for (const tab of [...e.opened, ...e.changed]) {
if (!tab.isActive)
continue
if (!(tab.input instanceof TabInputTerminal))
continue
// TabInputTerminal 只有构造器、无任何字段,只能用 tab.label
// 反查 `window.terminals` 里的实例。终端 name 常被 shell 的
// OSC title 序列改写(追加 git branch 等后缀),所以双向 startsWith
// 兜底,参考 `injectIntoImplTerminal` 里的命名匹配策略。
const label = tab.label
const term = window.terminals.find(
t => label.startsWith(t.name) || t.name.startsWith(label),
)
if (term)
this.handleActiveTerminalChanged(term)
}
}),
// 用户把焦点切到 column 2 终端 tab 时,反向选中看板上对应的工单卡片。
// 会话终端一律开在编辑区(见 resolveTerminalLocation),tabs API 即唯一真相:
// 分组内切 tab 走 onDidChangeTabs,分组激活(单 tab 分组从看板点回来)
// 走 onDidChangeTabGroups,两者都只是触发一次重新推导。
window.tabGroups.onDidChangeTabs(() => this.syncFocusedTerminalTab()),
window.tabGroups.onDidChangeTabGroups(() => this.syncFocusedTerminalTab()),
)
// Start observing the workspace repo so we can hide the "提交代码"
@@ -335,6 +296,17 @@ export class KanbanWebviewPanel {
toolbar.setupGitWatcher(this)
}
private syncFocusedTerminalTab(): void {
const term = focusedTerminal(
window.tabGroups.activeTabGroup.activeTab,
input => input instanceof TabInputTerminal,
window.terminals,
)
const activated = this.focusedTerminalTracker.sync(term)
if (activated)
this.handleActiveTerminalChanged(activated)
}
static createOrShow(context: ExtensionContext): void {
if (KanbanWebviewPanel.current) {
KanbanWebviewPanel.current.panel.reveal(ViewColumn.Active)