🐛 fix(vscode): 修好单个会话 tab 从看板点回时不反向选中所属工单
Claude-Session: https://claude.ai/code/session_01FAA5YEyR7fzWj7gYQiLSqy
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user