Skip to content

Commit 56bd552

Browse files
committed
Hide the context panel for agents that aren't Claude-backed
Context occupancy is read out of Claude's session jsonl, so a Codex or Cursor tab could never produce a snapshot. The panel still rendered "No context data yet. Updates after each turn." for them, promising something that was never coming. It now hides entirely for those tabs, and for shell / diff / browser tabs, the way JsonClaudeTodosPanel does. A Claude tab with no turns yet still gets the message, because there it is true. The same assumption was a live bug on the main side. Every agent tab was eligible for the latestSessionId fallback, which looks in ~/.claude/projects — so a Codex tab in a worktree that had also been used with Claude would adopt Claude's transcript and report another agent's numbers as its own. Codex and Cursor also fire Stop hooks, and their transcript_path points at their own format, which analyzeContext would have read as Claude jsonl and turned into confident nonsense. Both paths are now gated. The predicate lives in shared/state/terminals.ts rather than being written twice, since main and renderer both need it. A missing agentKind counts as Claude — that's the PanesFSM default and what tabs created before the field have.
1 parent 4a4bc89 commit 56bd552

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

src/main/context-tracker.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ import { join } from 'path'
2626
import type { Store } from './store'
2727
import type { StateEvent } from '../shared/state'
2828
import { onStopEvent, type StopEvent } from './hooks'
29-
import { findLeafByTabId, getLeaves } from '../shared/state/terminals'
29+
import {
30+
findLeafByTabId,
31+
findTabById,
32+
getLeaves,
33+
isClaudeBackedTab
34+
} from '../shared/state/terminals'
3035
import type { ContextSnapshot } from '../shared/state/context-window'
3136
import { analyzeContext } from './context-window'
3237
import { latestSessionId } from './agents/claude'
@@ -95,9 +100,25 @@ export class ContextTracker {
95100
}
96101

97102
private handleStop(ev: StopEvent): void {
103+
// Codex and Cursor fire Stop hooks too, but `transcript_path` then
104+
// points at their own format — analyzeContext would read it as Claude
105+
// jsonl and produce confident nonsense. Drop it before it's recorded,
106+
// so backfillAll can't resurrect it later either.
107+
if (!this.isClaudeBacked(ev.terminalId)) return
98108
this.lastStops.set(ev.terminalId, ev)
99109
if (this.interestedClients.size === 0) return
100-
this.analyzeAndDispatch(ev.terminalId, ev.sessionId, ev.transcriptPath, this.worktreeForTerminal(ev.terminalId))
110+
this.analyzeAndDispatch(
111+
ev.terminalId,
112+
ev.sessionId,
113+
ev.transcriptPath,
114+
this.worktreeForTerminal(ev.terminalId)
115+
)
116+
}
117+
118+
private isClaudeBacked(terminalId: string): boolean {
119+
return isClaudeBackedTab(
120+
findTabById(this.store.getSnapshot().state.terminals.panes, terminalId)
121+
)
101122
}
102123

103124
private handleStoreEvent(event: StateEvent): void {
@@ -213,6 +234,11 @@ export class ContextTracker {
213234
for (const leaf of getLeaves(tree)) {
214235
for (const tab of leaf.tabs) {
215236
if (tab.type !== 'agent') continue
237+
// Without this, a Codex or Cursor tab in a worktree that also
238+
// has Claude history would adopt Claude's transcript via the
239+
// latestSessionId fallback below and show another agent's
240+
// numbers as its own.
241+
if (!isClaudeBackedTab(tab)) continue
216242
if (this.lastStops.has(tab.id)) continue
217243
const cwd = tab.cwd || worktreePath
218244
// Prefer the tab's own session id, but fall back to the most

src/renderer/components/ContextPanel.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useMemo, useState } from 'react'
22
import { RightPanel } from './RightPanel'
3-
import { useContextWindow } from '../store'
3+
import { useContextWindow, usePanes } from '../store'
44
import { useBackend } from '../backend'
5+
import { findTabById, isClaudeBackedTab } from '../../shared/state/terminals'
56
import type { ContextSnapshot } from '../../shared/state/context-window'
67

78
interface ContextPanelProps {
@@ -117,11 +118,24 @@ function buildRows(snapshot: ContextSnapshot): Row[] {
117118

118119
export function ContextPanel({ focusedTabId }: ContextPanelProps): JSX.Element | null {
119120
const backend = useBackend()
121+
const panes = usePanes()
120122
const snapshot = useContextWindow(focusedTabId)
121123
const [showDiscoverable, setShowDiscoverable] = useState(false)
122124

125+
// Codex and Cursor never produce a snapshot, so a "no data yet"
126+
// message would promise something that is never coming.
127+
const supported = useMemo(
128+
() => isClaudeBackedTab(focusedTabId ? findTabById(panes, focusedTabId) : null),
129+
[panes, focusedTabId]
130+
)
131+
123132
const rows = useMemo(() => (snapshot ? buildRows(snapshot) : []), [snapshot])
124133

134+
// Hide the panel outright rather than render an empty shell — same
135+
// pattern as JsonClaudeTodosPanel. Shell / diff / browser tabs take
136+
// this path too.
137+
if (!supported) return null
138+
125139
const body = (): JSX.Element => {
126140
if (!snapshot) {
127141
return (

src/shared/state/terminals.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import {
1111
findLeafByTabId,
1212
findTabById,
1313
hasAnyTabs,
14+
isClaudeBackedTab,
1415
mapLeaves,
1516
replaceNode,
16-
removeLeaf
17+
removeLeaf,
18+
type TerminalTab
1719
} from './terminals'
1820

1921
function apply(state: TerminalsState, event: TerminalsEvent): TerminalsState {
@@ -1070,3 +1072,39 @@ describe('terminalsReducer', () => {
10701072
).toBe(start)
10711073
})
10721074
})
1075+
1076+
describe('isClaudeBackedTab', () => {
1077+
const tab = (over: Partial<TerminalTab>): TerminalTab =>
1078+
({ id: 't', type: 'agent', label: 'Agent', ...over }) as TerminalTab
1079+
1080+
it('accepts a json-claude chat tab', () => {
1081+
expect(isClaudeBackedTab(tab({ type: 'json-claude' }))).toBe(true)
1082+
})
1083+
1084+
it('accepts a claude agent tab', () => {
1085+
expect(isClaudeBackedTab(tab({ type: 'agent', agentKind: 'claude' }))).toBe(true)
1086+
})
1087+
1088+
it('treats a missing agentKind as claude', () => {
1089+
// Tabs created before agentKind existed, plus PanesFSM's default.
1090+
expect(isClaudeBackedTab(tab({ type: 'agent', agentKind: undefined }))).toBe(true)
1091+
})
1092+
1093+
it('rejects codex and cursor agent tabs', () => {
1094+
// They write their own transcript formats; reading those as Claude
1095+
// jsonl would produce confident nonsense.
1096+
expect(isClaudeBackedTab(tab({ type: 'agent', agentKind: 'codex' }))).toBe(false)
1097+
expect(isClaudeBackedTab(tab({ type: 'agent', agentKind: 'cursor' }))).toBe(false)
1098+
})
1099+
1100+
it('rejects non-agent tab types', () => {
1101+
for (const type of ['shell', 'diff', 'file', 'browser', 'review'] as const) {
1102+
expect(isClaudeBackedTab(tab({ type }))).toBe(false)
1103+
}
1104+
})
1105+
1106+
it('rejects null and undefined', () => {
1107+
expect(isClaudeBackedTab(null)).toBe(false)
1108+
expect(isClaudeBackedTab(undefined)).toBe(false)
1109+
})
1110+
})

src/shared/state/terminals.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ export function findTabById(
131131
return null
132132
}
133133

134+
/** Whether a tab talks to Claude, and so writes a session jsonl under
135+
* ~/.claude/projects. Codex and Cursor agent tabs write their own
136+
* formats, so anything reading Claude transcripts must exclude them
137+
* rather than assume every agent tab is Claude.
138+
*
139+
* A missing `agentKind` means Claude — tabs created before the field
140+
* existed, and the default in PanesFSM.addTab. */
141+
export function isClaudeBackedTab(tab: TerminalTab | null | undefined): boolean {
142+
if (!tab) return false
143+
if (tab.type === 'json-claude') return true
144+
return tab.type === 'agent' && (tab.agentKind ?? 'claude') === 'claude'
145+
}
146+
134147
export function hasAnyTabs(node: PaneNode): boolean {
135148
if (node.type === 'leaf') return node.tabs.length > 0
136149
return hasAnyTabs(node.children[0]) || hasAnyTabs(node.children[1])

0 commit comments

Comments
 (0)