Skip to content

Commit ead8806

Browse files
committed
mods: merge poteat/diff-parity; a mark's props, hyphened Choice members and the built-in panel's marks at its call sites, in the tested and linted layout
2 parents 59bb885 + bb429a9 commit ead8806

23 files changed

Lines changed: 212 additions & 84 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,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
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './base-outcomes.js'
2+
3+
export * as default from '.'

‎mods/diff/hooks/record/features/features.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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',

‎mods/diff/hooks/record/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import type { Host } from '../../host'
2+
13
/**
2-
* 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).
4+
* How a marked feature went: fine, or degraded for a reason, with the
5+
* properties the built-in panel's mark carries beside it.
6+
*
7+
* The pane never marks `bad`: a failed fetch keeps the last good diff on
8+
* screen.
49
*/
5-
export type MarkOutcome = { kind: 'ok' } | { kind: 'sad'; reason: string }
10+
export type MarkOutcome = ({ kind: 'ok' } | { kind: 'sad'; reason: string }) & {
11+
props?: Parameters<Host['mark']>[0]['props']
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './tabs.js'
2+
3+
export * as default from '.'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Where a tab-switch mark says the person went, named as the built-in
3+
* panel names it: the diff, or the conversation it closes back to.
4+
*/
5+
export const TABS = ['diff', 'convo'] as const

‎mods/diff/hooks/register.ts‎

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ export function register(on: On) {
6161
const isError = base.kind === 'error'
6262
const outcome: Record.MarkOutcome = isError
6363
? { kind: 'sad', reason: base.reason }
64-
: { kind: 'ok' }
64+
: {
65+
kind: 'ok',
66+
props: {
67+
outcome: { value: base.kind, of: Record.BASE_OUTCOMES },
68+
},
69+
}
6570

6671
if (!loggedBaseKinds.has(outcome.kind)) {
6772
loggedBaseKinds.add(outcome.kind)
@@ -148,21 +153,21 @@ export function register(on: On) {
148153
model.selectedPath,
149154
)
150155

151-
async function loadBody(engine: Host): Promise<void> {
156+
async function loadBody(engine: Host): Promise<boolean> {
152157
const { data } = model
153158
const selected = selectedOf()
154159

155160
if (!data || !selected || !backend) {
156161
bodyKey = null
157162
model = { ...model, body: null, bodyState: 'idle' }
158163

159-
return
164+
return false
160165
}
161166

162167
const key = `${generation}|${data.baseRef}|${selected.path}`
163168

164169
if (key === bodyKey) {
165-
return
170+
return false
166171
}
167172

168173
bodyKey = key
@@ -171,11 +176,13 @@ export function register(on: On) {
171176
const body = await backend.fetchFileHunks(data, selected)
172177

173178
if (bodyKey !== key) {
174-
return
179+
return body === null
175180
}
176181

177182
model = { ...model, body, bodyState: body ? 'ready' : 'failed' }
178183
redraw(engine)
184+
185+
return body === null
179186
}
180187

181188
function startPoll(engine: Host, pinned: Backend.Backend) {
@@ -240,7 +247,6 @@ export function register(on: On) {
240247
})
241248
break
242249
case 'data':
243-
record.mark(Record.FEATURES.read, { kind: 'ok' })
244250
generation += 1
245251

246252
if (pinned) {
@@ -250,7 +256,23 @@ export function register(on: On) {
250256
break
251257
}
252258

253-
await loadBody(engine)
259+
const hasHunksFailed = await loadBody(engine)
260+
261+
if (outcome.kind === 'data') {
262+
record.mark(
263+
Record.FEATURES.read,
264+
hasHunksFailed
265+
? { kind: 'sad', reason: 'git_hunks_failed' }
266+
: { kind: 'ok' },
267+
)
268+
}
269+
} catch (error) {
270+
record.mark(Record.FEATURES.read, {
271+
kind: 'sad',
272+
reason: 'git_diff_threw',
273+
})
274+
275+
throw error
254276
} finally {
255277
isRefreshing = false
256278
redraw(engine)
@@ -281,13 +303,11 @@ export function register(on: On) {
281303
const isManual = trigger === 'manual'
282304
await engine.openPane(isManual ? { ...pane, ...Names.FOCUSED_PANE } : pane)
283305
isPaneOpen = true
284-
const record = Record.recorderOf(engine)
285-
record.mark(Record.FEATURES.tabSwitch, { kind: 'ok' })
286306
const sessionId = await engine.sessionId().catch(() => null)
287307

288308
if (sessionId !== null && sessionId !== shownSessionId) {
289309
shownSessionId = sessionId
290-
record.shown(trigger, Record.widthBucketOf(columns))
310+
Record.recorderOf(engine).shown(trigger, Record.widthBucketOf(columns))
291311
}
292312

293313
void refresh(engine)
@@ -296,7 +316,13 @@ export function register(on: On) {
296316
async function closePane(engine: Host): Promise<void> {
297317
await engine.closePane({ id: Names.PANE_ID })
298318
isPaneOpen = false
299-
Record.recorderOf(engine).mark(Record.FEATURES.tabSwitch, { kind: 'ok' })
319+
}
320+
321+
function markTabSwitch(engine: Host, tab: (typeof Record.TABS)[number]) {
322+
Record.recorderOf(engine).mark(Record.FEATURES.tabSwitch, {
323+
kind: 'ok',
324+
props: { tab: { value: tab, of: Record.TABS } },
325+
})
300326
}
301327

302328
async function wasDrawnWhenProbed(engine: Host): Promise<boolean> {
@@ -367,6 +393,7 @@ export function register(on: On) {
367393
model = { ...model, requestedMode: mode, body: null, bodyState: 'idle' }
368394
Record.recorderOf(engine).mark(Record.FEATURES.baseSwitch, {
369395
kind: 'ok',
396+
props: { mode: { value: mode, of: model.baseModes } },
370397
})
371398
const toplevel = model.data?.repository.toplevel
372399

@@ -401,6 +428,7 @@ export function register(on: On) {
401428
},
402429
close: () => {
403430
void closePane(engine)
431+
.then(() => markTabSwitch(engine, 'convo'))
404432
.then(() => engine.storeSet(Names.STORE_OPEN_KEY, false))
405433
.catch(() => undefined)
406434
},
@@ -525,6 +553,7 @@ export function register(on: On) {
525553

526554
const isOpening = toggle === 'open'
527555
await (isOpening ? openPane(host, 'manual') : closePane(host))
556+
markTabSwitch(host, isOpening ? 'diff' : 'convo')
528557
await host.storeSet(Names.STORE_OPEN_KEY, isOpening).catch(() => undefined)
529558

530559
return {}

‎mods/diff/hooks/telemetry-noun.d.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ declare module 'claude-code' {
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 {

‎mods/telemetry/README.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# telemetry
22

3-
Plugin analytics as a plugin: one `engine.create` step adds `$.telemetry`
4-
to the engine interface every plugin above it is handed, built over the
3+
Plugin analytics as a plugin: one `engine.create` step adds `$.telemetry` to
4+
the engine interface every plugin above it is handed, built over the
55
`$.session` and `$.http` nouns beneath. `$.telemetry.log({ event, props })`
66
sends one event as one first-party row, `tengu_plugin_<event>`;
7-
`$.telemetry.mark({ feature, kind, reason? })` marks one use of a feature as
8-
the CLI's own feature events do, `tengu_feature_<kind>` with a
9-
`feature_name`. Each call is one POST to the event-logging ingest with the
10-
session's own credential (`$.session.authorize()`, resolved at each call),
11-
one attempt, nothing batched; a session with no first-party credential, or
12-
an ingest that refuses, rejects the caller's promise.
7+
`$.telemetry.mark({ feature, kind, reason?, props? })` marks one use of a
8+
feature as the CLI's own feature events do, `tengu_feature_<kind>` with a
9+
`feature_name` and the mark's properties beside it. Each call is one POST to
10+
the event-logging ingest with the session's own credential
11+
(`$.session.authorize()`, resolved at each call), one attempt, nothing
12+
batched; a session with no first-party credential, or an ingest that
13+
refuses, rejects the caller's promise.
1314

1415
It sends nothing wherever the CLI's own analytics are off: under
1516
`DISABLE_TELEMETRY`, `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or
@@ -22,9 +23,10 @@ to a third-party provider or a cloud gateway sends nothing more. The row's
2223

2324
Nothing free-form reaches a row. An event name and every property key is a
2425
snake_case token; a value is a finite number, a boolean, or a Choice (a
25-
string named together with the list it is chosen from); `mark` takes `ok`,
26-
`sad` or `bad`, with a `reason` required on the last two and refused on the
27-
first. An entry that breaks a rule is refused before anything is sent.
26+
string named together with the list it is chosen from), under `log` and
27+
`mark` alike; `mark` takes `ok`, `sad` or `bad`, with a `reason` required on
28+
the last two and refused on the first. An entry that breaks a rule is
29+
refused before anything is sent.
2830

2931
`hooks/register.ts` is the module; `hooks/telemetry-types/` is the noun's
3032
type as a caller sees it.

0 commit comments

Comments
 (0)