fix(tab-bar): keep launchAgent in the terminal-tab status contract (#9040)

resolveTerminalTabActivityStatus narrows its tab with Pick<'id'|'title'>, so the
type said launchAgent is never read even though the status gate now reads it.
Structural typing means the real caller (SortableTab passes a whole TerminalTab)
still worked at runtime, but the contract was wrong: re-narrowing or building an
input object literal would silently drop the tab-bar dot back to pre-#9040
behavior with no type error, because launchAgent is optional.

Widen the Pick to match what the resolver actually consumes, and pin the tab-bar
path with its own test so the regression cannot land silently.
This commit is contained in:
Neil
2026-07-25 21:51:03 -07:00
parent 6ae5fed24b
commit e841974b42
2 changed files with 52 additions and 1 deletions
@@ -104,7 +104,10 @@ function parseAgentStatusPaneKey(paneKey: string): { tabId: string; paneId: stri
const EMPTY_PANE_IDS: ReadonlySet<string> = new Set()
type TerminalTabActivityInput = {
tab: Pick<TerminalTab, 'id' | 'title'>
// Why: launchAgent is read, not just carried — the status gate needs it to attribute a
// bare spinner title to an agent (#9040). Narrowing it away here compiles (it is optional)
// but silently drops the tab-bar dot back to the pre-#9040 behavior.
tab: Pick<TerminalTab, 'id' | 'title' | 'launchAgent'>
agentStatusByPaneKey?: Record<string, AgentStatusEntry>
// Why: the store bumps this at the 30m stale boundary without replacing the
// pane-status map; it is the flag cache's invalidation key (see above).
@@ -0,0 +1,48 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { TerminalTab } from '../../../../shared/types'
import {
resetTerminalTabActivityFlagsCacheForTest,
resolveTerminalTabActivityStatus
} from './terminal-tab-activity-status'
// Why: #9040 — the tab-bar dot shares the sidebar's attribution gate, so Claude's
// token-less spinner title must reach 'working' here too. TerminalTabActivityInput
// narrows `tab` with a Pick; because launchAgent is optional, dropping it from that
// Pick compiles cleanly and would silently revert this path alone.
describe('#9040 terminal tab dot attributes spinner titles to the launched agent', () => {
beforeEach(() => {
resetTerminalTabActivityFlagsCacheForTest()
})
it('reports working for a Claude spinner title on a live tab', () => {
const status = resolveTerminalTabActivityStatus({
tab: {
id: 'tab-1',
title: '⠋ implementing the feature',
launchAgent: 'claude'
} satisfies Partial<TerminalTab> as TerminalTab,
ptyIdsByTabId: { 'tab-1': ['pty-0'] }
})
expect(status).toBe('working')
})
// Control: the named-provider path this must stay at parity with.
it('reports working for a named-provider title', () => {
const status = resolveTerminalTabActivityStatus({
tab: { id: 'tab-1', title: 'claude [working]' } as TerminalTab,
ptyIdsByTabId: { 'tab-1': ['pty-0'] }
})
expect(status).toBe('working')
})
it('stays out of working for a spinner title with no launch identity', () => {
const status = resolveTerminalTabActivityStatus({
tab: { id: 'tab-1', title: '⠐ Review branch for regressions' } as TerminalTab,
ptyIdsByTabId: { 'tab-1': ['pty-0'] }
})
expect(status).not.toBe('working')
})
})