spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

mods/diff: tab, base-switch, base-resolve and read marks carry the built-in panel's props and reasons at its call sitesBrowse files
poteat authored on Sep 11, 2026 parent 1e6e5c7 · commit bb429a97b872
9 changed files+74 -15compared with 1e6e5c7
Amods/diff/hooks/record/base-outcomes/base-outcomes.ts
Amods/diff/hooks/record/base-outcomes/index.ts
Mmods/diff/hooks/record/features/features.ts
Mmods/diff/hooks/record/index.ts
Mmods/diff/hooks/record/mark-outcome/mark-outcome.ts
Amods/diff/hooks/record/tabs/index.ts
Amods/diff/hooks/record/tabs/tabs.ts
Mmods/diff/hooks/register.ts
Mmods/diff/hooks/telemetry-noun.d.ts
@@ -1,0 +1,5 @@
1+/**
2+ * What branch mode resolved its base to, as a base-resolve mark names it,
3+ * the built-in panel's own: the merge base, HEAD itself, or nothing.
4+ */
5+export const BASE_OUTCOMES = ['merge-base', 'head-is-base', 'none'] as const
@@ -1,0 +1,3 @@
1+export * from './base-outcomes.js'
2+
3+export * as default from '.'
@@ -2,8 +2,10 @@
22 * The feature names the plugin marks, kept as the built-in panel's so the
33 * feature surface keeps one series across the swap.
44 *
5- * `read` per fetch round, `tabSwitch` per open or close, `baseSwitch` per
6- * base pick, `baseResolve` once a session, `selectionAttach` per diff asked.
5+ * `read` per fetch round, `tabSwitch` per `/diff` open or close and per
6+ * close by the pane's own control, `baseSwitch` per base pick, `baseResolve`
7+ * on the first resolve and the first failure, `selectionAttach` per diff
8+ * asked.
79 */
810 export const FEATURES = {
911 read: 'repl_diff_read',
@@ -1,9 +1,11 @@
1+export * from './base-outcomes'
12 export * from './features'
23 export * from './mark-outcome'
34 export * from './recorder'
45 export * from './recorder-of.js'
56 export * from './safely'
67 export * from './shown-triggers'
8+export * from './tabs'
79 export * from './width-bucket-of'
810 export * from './width-buckets'
911
@@ -1,5 +1,10 @@
1+import type { Host } from '../../host'
2+
13 /**
24 * How a marked feature went: fine, or degraded for a reason (the pane never
3- * marks `bad`: a failed fetch keeps the last good diff on screen).
5+ * marks `bad`: a failed fetch keeps the last good diff on screen), with the
6+ * properties the built-in panel's mark carries beside it.
47 */
5-export type MarkOutcome = { kind: 'ok' } | { kind: 'sad'; reason: string }
8+export type MarkOutcome = ({ kind: 'ok' } | { kind: 'sad'; reason: string }) & {
9+ props?: Parameters<Host['mark']>[0]['props']
10+}
@@ -1,0 +1,3 @@
1+export * from './tabs.js'
2+
3+export * as default from '.'
@@ -1,0 +1,5 @@
1+/**
2+ * The tabs a tab-switch mark names, the built-in panel's own: the diff, and
3+ * the conversation it closes back to.
4+ */
5+export const TABS = ['diff', 'convo'] as const
@@ -66,7 +66,12 @@
6666 const isError = base.kind === 'error'
6767 const outcome: Record.MarkOutcome = isError
6868 ? { kind: 'sad', reason: base.reason }
69- : { kind: 'ok' }
69+ : {
70+ kind: 'ok',
71+ props: {
72+ outcome: { value: base.kind, of: Record.BASE_OUTCOMES },
73+ },
74+ }
7075
7176 if (!loggedBaseKinds.has(outcome.kind)) {
7277 loggedBaseKinds.add(outcome.kind)
@@ -153,7 +158,7 @@
153158 model.selectedPath,
154159 )
155160
156- async function loadBody(engine: Host): Promise<void> {
161+ async function loadBody(engine: Host): Promise<boolean> {
157162 const { data } = model
158163 const selected = selectedOf()
159164
@@ -161,13 +166,13 @@
161166 bodyKey = null
162167 model = { ...model, body: null, bodyState: 'idle' }
163168
164- return
169+ return false
165170 }
166171
167172 const key = `${generation}|${data.baseRef}|${selected.path}`
168173
169174 if (key === bodyKey) {
170- return
175+ return false
171176 }
172177
173178 bodyKey = key
@@ -176,11 +181,13 @@
176181 const body = await backend.fetchFileHunks(data, selected)
177182
178183 if (bodyKey !== key) {
179- return
184+ return body === null
180185 }
181186
182187 model = { ...model, body, bodyState: body ? 'ready' : 'failed' }
183188 redraw(engine)
189+
190+ return body === null
184191 }
185192
186193 function startPoll(engine: Host, pinned: Backend.Backend) {
@@ -245,7 +252,6 @@
245252 })
246253 break
247254 case 'data':
248- record.mark(Record.FEATURES.read, { kind: 'ok' })
249255 generation += 1
250256
251257 if (pinned) {
@@ -255,7 +261,23 @@
255261 break
256262 }
257263
258- await loadBody(engine)
264+ const hasHunksFailed = await loadBody(engine)
265+
266+ if (outcome.kind === 'data') {
267+ record.mark(
268+ Record.FEATURES.read,
269+ hasHunksFailed
270+ ? { kind: 'sad', reason: 'git_hunks_failed' }
271+ : { kind: 'ok' },
272+ )
273+ }
274+ } catch (error) {
275+ record.mark(Record.FEATURES.read, {
276+ kind: 'sad',
277+ reason: 'git_diff_threw',
278+ })
279+
280+ throw error
259281 } finally {
260282 isRefreshing = false
261283 redraw(engine)
@@ -287,7 +309,6 @@
287309 await engine.openPane(isManual ? { ...pane, ...Names.FOCUSED_PANE } : pane)
288310 isPaneOpen = true
289311 const record = Record.recorderOf(engine)
290- record.mark(Record.FEATURES.tabSwitch, { kind: 'ok' })
291312 const sessionId = await engine.sessionId().catch(() => null)
292313
293314 if (sessionId !== null && sessionId !== shownSessionId) {
@@ -301,9 +322,18 @@
301322 async function closePane(engine: Host): Promise<void> {
302323 await engine.closePane({ id: Names.PANE_ID })
303324 isPaneOpen = false
304- Record.recorderOf(engine).mark(Record.FEATURES.tabSwitch, { kind: 'ok' })
305325 }
306326
327+ function markTabSwitch(
328+ engine: Host,
329+ tab: (typeof Record.TABS)[number],
330+ ): void {
331+ Record.recorderOf(engine).mark(Record.FEATURES.tabSwitch, {
332+ kind: 'ok',
333+ props: { tab: { value: tab, of: Record.TABS } },
334+ })
335+ }
336+
307337 async function wasDrawnWhenProbed(engine: Host): Promise<boolean> {
308338 wasDrawnSinceProbe = false
309339 engine.invalidate()
@@ -372,6 +402,7 @@
372402 model = { ...model, requestedMode: mode, body: null, bodyState: 'idle' }
373403 Record.recorderOf(engine).mark(Record.FEATURES.baseSwitch, {
374404 kind: 'ok',
405+ props: { mode: { value: mode, of: model.baseModes } },
375406 })
376407 const toplevel = model.data?.repository.toplevel
377408
@@ -406,6 +437,7 @@
406437 },
407438 close: () => {
408439 void closePane(engine)
440+ .then(() => markTabSwitch(engine, 'convo'))
409441 .then(() => engine.storeSet(Names.STORE_OPEN_KEY, false))
410442 .catch(() => undefined)
411443 },
@@ -530,6 +562,7 @@
530562
531563 const isOpening = toggle === 'open'
532564 await (isOpening ? openPane(host, 'manual') : closePane(host))
565+ markTabSwitch(host, isOpening ? 'diff' : 'convo')
533566 await host.storeSet(Names.STORE_OPEN_KEY, isOpening).catch(() => undefined)
534567
535568 return {}
@@ -34,13 +34,14 @@
3434 }
3535
3636 /**
37- * What `$.telemetry.mark` takes: the feature, how it went, and why when
38- * not ok.
37+ * What `$.telemetry.mark` takes: the feature, how it went, why when not
38+ * ok, and the properties the row carries beside them by snake_case key.
3939 */
4040 type DiffTelemetryMarkEntry = {
4141 feature: string
4242 kind: DiffTelemetryMarkKind
4343 reason?: string
44+ props?: Readonly<Record<string, DiffTelemetryProp>>
4445 }
4546
4647 interface EngineInterface {