@sjawhar/claude-envoy 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@sjawhar/claude-envoy",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "license": "Apache-2.0",
5
- "description": "Claude Code channel plugin for Legion's Envoy bus",
5
+ "description": "Claude Code plugin for Legion's Envoy bus",
6
6
  "type": "module",
7
7
  "files": [
8
8
  ".claude-plugin/**",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # smoke-clear-rebind.sh — real Claude Code `/clear` handoff smoke: after `/clear`, the
3
3
  # registry entry of the new session id must list its own direct subject and never the old one.
4
- # See README.md "Manual smoke" for prerequisites; like smoke-channel.sh, never a CI step.
4
+ # See README.md "Manual smoke" for prerequisites; a manual smoke, never a CI step.
5
5
  set -euo pipefail
6
6
 
7
7
  readonly envoy_url="${ENVOY_URL:-http://127.0.0.1:9020}"
@@ -20,7 +20,7 @@
20
20
  # with a real listener. It does not prove that Claude Code sends the field; that rests on Claude
21
21
  # Code's own documentation.
22
22
  #
23
- # Like smoke-channel.sh and smoke-clear-rebind.sh, this is a manual smoke and never a CI step.
23
+ # Like smoke-clear-rebind.sh, this is a manual smoke and never a CI step.
24
24
  #
25
25
  # bash packages/claude-envoy/scripts/smoke-session-title.sh
26
26
  #
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Claude Code's session, as the Agents page's live view reads it (INFRA-1026).
3
+ *
4
+ * The mod (`../hooks/mod.ts`) sends what happens in the session over the link's socket as
5
+ * `ModEvent`s; this turns each one into a call on the `AgentStreamPublisher` every Legion host
6
+ * shares (`@legion/pi-shared/agent-stream`), and builds the two frames that describe the session
7
+ * rather than its conversation: its model and thinking level, and its slash commands.
8
+ *
9
+ * It owns one piece of state the publisher cannot own for it. The publisher derives a message's
10
+ * ring slot from the message itself — an assistant message's slot is its timestamp — so every
11
+ * snapshot of one streaming step has to carry the one timestamp that step was first seen at, and
12
+ * two steps must never carry the same one. The allocation below does both: a step's timestamp is
13
+ * kept under `(turnId, index)` for as long as the session lasts, and each new one is at least one
14
+ * millisecond past the last handed out, so two steps that start inside one millisecond still land
15
+ * in two slots.
16
+ *
17
+ * Nothing here opens a socket or knows about NATS: a subject is a string its caller passes in.
18
+ */
19
+ import { type AgentStreamCommand, isTypableCommandName } from "@legion/contracts"
20
+ import {
21
+ type AgentStreamPublisher,
22
+ type AgentStreamSettingsState,
23
+ modelReference,
24
+ } from "@legion/pi-shared/agent-stream"
25
+ import type {
26
+ ModAppendMessage,
27
+ ModCommandInfo,
28
+ ModConfigRow,
29
+ ModContentBlock,
30
+ ModEffort,
31
+ ModEvent,
32
+ } from "../hooks/mod-protocol"
33
+ import {
34
+ claudeEchoedCommandLine,
35
+ claudeMidTurnMessage,
36
+ claudeRowText,
37
+ isCommandBookkeeping,
38
+ isPictureAttachmentRow,
39
+ } from "./claude-rows"
40
+
41
+ /** The provider every model of Claude Code's is named under, so a frame carries the same
42
+ * `provider/id` reference an Oh My Pi session's does (`anthropic/claude-sonnet-5`). */
43
+ export const CLAUDE_PROVIDER = "anthropic"
44
+
45
+ /**
46
+ * What a reasoning part says when the model's thinking arrived with no text.
47
+ *
48
+ * Claude Code sends the thinking block's signature and no words unless the session asked for
49
+ * summarized thinking, so publishing the block as it stands would show the viewer an empty
50
+ * reasoning box. Measured on 2.1.310: every `thinking` block of a plain session carries
51
+ * `"thinking": ""`, and a session started with `--thinking-display summarized` carries the
52
+ * summary. A terminal session turns the summaries on with the `showThinkingSummaries` setting; a
53
+ * Legion pane gets them from the launch flag the daemon passes, which no reader of this view can
54
+ * change, so the setting is what the sentence names.
55
+ *
56
+ * One sentence, published once per step however many empty blocks the step holds: a browser test
57
+ * and the Dispatch documentation both quote it.
58
+ */
59
+ export const CLAUDE_THINKING_HIDDEN =
60
+ "Claude Code hides what the model thought here: it sends the thinking text only when the terminal session turns on the showThinkingSummaries setting."
61
+
62
+ /**
63
+ * The effort levels `/effort` offers, in the order Claude Code lists them (`ModEffort`). Claude
64
+ * Code reports a request's effort only on its `turn.step`, so this list is what a viewer can pick
65
+ * from and the step's own `effort` is which one is in force.
66
+ */
67
+ export const CLAUDE_EFFORT_LEVELS = [
68
+ "low",
69
+ "medium",
70
+ "high",
71
+ "xhigh",
72
+ "max",
73
+ ] as const satisfies readonly ModEffort[]
74
+
75
+ /** One content part as the publisher reads one (`conversationParts` in
76
+ * `@legion/pi-shared/agent-stream`): a `text`, a `thinking`, a `toolCall`, or any other `type`,
77
+ * which the publisher renders as the literal `[<type>]`. */
78
+ interface ClaudeStreamPart {
79
+ readonly type: string
80
+ readonly text?: string
81
+ readonly thinking?: string
82
+ readonly id?: string
83
+ readonly name?: string
84
+ readonly arguments?: unknown
85
+ }
86
+
87
+ /** One message as the publisher reads one (`readMessage`): `role` and `timestamp` are what it
88
+ * needs to keep the message at all, and the rest is what each role carries. */
89
+ interface ClaudeStreamMessage {
90
+ readonly role: "assistant" | "user" | "toolResult"
91
+ readonly timestamp: number
92
+ readonly content: string | readonly ClaudeStreamPart[]
93
+ readonly provider?: string
94
+ readonly model?: string
95
+ readonly tag?: string
96
+ readonly toolCallId?: string
97
+ readonly toolName?: string
98
+ readonly isError?: boolean
99
+ }
100
+
101
+ /** The effort the last `turn.step` of the session ran under, and the model it ran on. Claude Code
102
+ * names no effort for a model that has none, so the absent `effort` is the fact that there is no
103
+ * thinking level to show. */
104
+ interface ObservedEffort {
105
+ readonly model: string
106
+ readonly effort: ModEffort | undefined
107
+ }
108
+
109
+ export interface ClaudeAgentStreamDeps {
110
+ readonly publisher: AgentStreamPublisher
111
+ /** The clock a step's timestamp is allocated from, and every other message is stamped with. */
112
+ readonly now: () => number
113
+ /**
114
+ * Whether this session is a Legion pane rather than a terminal one. A pane is seeded with one
115
+ * command before anything else happens in it (`PANE_SEEDED_COMMAND`), which is the daemon
116
+ * naming the pane rather than a person writing to the agent, so the view draws neither the
117
+ * prompt that carried it nor the echo Claude Code wrote of it.
118
+ */
119
+ readonly isPane: boolean
120
+ /**
121
+ * The Dispatch message a row of the conversation is, where it is one
122
+ * (`./user-turn-state`). A person's own message would otherwise reach the page twice - as the
123
+ * message Dispatch stored and as the row the session took it as - and the row's tag is how the
124
+ * page knows the two are one message (`conversation.ts` in the Agents page). A row of the
125
+ * session's own keeps its own id as its tag, as every row did before.
126
+ */
127
+ readonly tagFor?: (row: string) => string | undefined
128
+ /**
129
+ * Whether a row of the `command` door is the echo row a `$.command.run` of this session's
130
+ * claimed (`./mod-session`, `runsCommandRow`), asked by the row's own id. Claude Code echoes
131
+ * the command it ran rather than the one that was asked for - `/cost` ran as `/usage`, and
132
+ * `/model anthropic/haiku` as `/model haiku`, measured live on 2.1.310 - so the echo row
133
+ * drawn is a second message the person never wrote, beside Dispatch's stored copy of the one
134
+ * they did send (INFRA-1026). Absent where no run of this session's can exist, and every
135
+ * command row is a person's own.
136
+ */
137
+ readonly runsCommandRow?: (row: string) => boolean
138
+ /**
139
+ * Whether a row of the `prompt` door is the row Claude Code wrote for this session's prompt
140
+ * at the messages a turn never read (`./mod-session`, `runsUnreadPrompt`), asked by the row's
141
+ * own id. That prompt quotes rows this view has already drawn, so drawn it shows a viewer
142
+ * each of those messages twice (INFRA-1026). A card submitted to an idle session is the other
143
+ * prompt this plugin submits without `asUser`, and it is drawn: the card reaches the
144
+ * conversation nowhere else. Absent where no prompt of this session's can exist, and every
145
+ * prompt row is a person's own.
146
+ */
147
+ readonly runsUnreadPrompt?: (row: string) => boolean
148
+ /**
149
+ * Whether a row is one this session appended into the conversation (`./mod-session`,
150
+ * `appendedRow`), asked by the row's own id: the card a running turn is handed, and a
151
+ * person's own message handed to one. Claude Code marks every appended row `isMeta`, which
152
+ * this view draws nothing of, so this is what says which `isMeta` row is a message of the
153
+ * conversation rather than the session talking to the model. Absent where this session
154
+ * appends nothing, and no `isMeta` row is drawn.
155
+ */
156
+ readonly appendedRow?: (row: string) => boolean
157
+ }
158
+
159
+ export interface ClaudeAgentStream {
160
+ /** Record what `event` says on `subject`, which is where the publisher sends its frames. An
161
+ * event that is no message of the conversation records nothing. */
162
+ readonly record: (subject: string, event: ModEvent) => void
163
+ /**
164
+ * The settings frame's state for the session's `model` (`$.session.model()`'s answer) and the
165
+ * `/config` rows `$.config.list()` answered with. Unreported until the rows have been read:
166
+ * they are the only list of the models a pick may name.
167
+ */
168
+ readonly settings: (
169
+ model: string | undefined,
170
+ rows: readonly ModConfigRow[] | undefined,
171
+ ) => AgentStreamSettingsState
172
+ /**
173
+ * The effort a person's pick put in force. Claude Code reports an effort only on a request's
174
+ * own `turn.step`, so until the session's next turn its report is the level before the pick;
175
+ * the pick itself is what set the effort the mod puts on every later request that names an
176
+ * effort of its own (`../hooks/mod`, `turn.step`), so from the moment it lands the settings
177
+ * frame says so and keeps saying so until another pick changes it or a step reports that the
178
+ * request ran at another level, or at none - which is what a model with no effort to set does.
179
+ */
180
+ readonly pickedEffort: (effort: ModEffort) => void
181
+ /** Forget the conversation's timestamps and the effort read off it: the session id moved, and
182
+ * the publisher was reset with it. */
183
+ readonly reset: () => void
184
+ }
185
+
186
+ /** One block of a row as a part the publisher reads. A kind it does not know crosses as its type
187
+ * alone, which the viewer shows as `[image]` and the like. */
188
+ function partOf(block: ModContentBlock): ClaudeStreamPart {
189
+ const text = block["text"]
190
+ if (block.type === "text" && typeof text === "string") {
191
+ // A line a person typed while a turn was running reaches the view inside framing Claude Code
192
+ // wrote for the model, and the view shows the person's words alone (`./claude-rows`,
193
+ // `claudeMidTurnMessage`), as it shows a command without the tags it was recorded in. Read
194
+ // here rather than in `recordAppend` so that a row carrying this block beside others keeps
195
+ // every other block exactly as it crosses today.
196
+ return { type: "text", text: claudeMidTurnMessage(text) ?? text }
197
+ }
198
+ const thinking = block["thinking"]
199
+ if (block.type === "thinking" && typeof thinking === "string") {
200
+ return { type: "thinking", thinking }
201
+ }
202
+ const id = block["id"]
203
+ if (block.type === "tool_use" && typeof id === "string") {
204
+ const name = block["name"]
205
+ return {
206
+ arguments: block["input"],
207
+ id,
208
+ name: typeof name === "string" ? name : "tool",
209
+ type: "toolCall",
210
+ }
211
+ }
212
+ return { type: block.type }
213
+ }
214
+
215
+ /** Whether a part puts anything in front of a reader: text or thinking with characters in it, a
216
+ * tool call, or a kind the publisher names in brackets (`[image]`). An empty text part is the
217
+ * one that shows nothing, since `conversationParts` keeps it as the empty string it is. */
218
+ function shows(part: ClaudeStreamPart): boolean {
219
+ if (part.type === "text") return part.text !== ""
220
+ if (part.type === "thinking") return part.thinking !== ""
221
+ return true
222
+ }
223
+
224
+ /** Under which role a row of the conversation is shown. Claude Code names the role on the rows
225
+ * that have one and leaves it off the rest, whose `type` says it instead; anything else is the
226
+ * session talking to the model, which the view shows on the person's side. */
227
+ function roleOf(message: ModAppendMessage): "assistant" | "user" {
228
+ if (message.role !== undefined) return message.role
229
+ return message.type === "assistant" ? "assistant" : "user"
230
+ }
231
+
232
+ /** The command the daemon's bridge seeds a fresh Legion pane with before anything else
233
+ * (`packages/daemon/internal/claudebridge/bridge.go:210-213`), which names the pane. */
234
+ const PANE_SEEDED_COMMAND = "/rename"
235
+
236
+ /** Whether a line is the command a pane is seeded with, with or without the title it names. */
237
+ function isPaneSeededRename(line: string): boolean {
238
+ return line === PANE_SEEDED_COMMAND || line.startsWith(`${PANE_SEEDED_COMMAND} `)
239
+ }
240
+
241
+ /**
242
+ * A streamed tool call's arguments as the publisher should render them. They arrive as JSON text
243
+ * a piece at a time, so a snapshot taken mid-block does not parse: the parsed value is handed
244
+ * over once there is one, and until then the text as it arrived, which is what the viewer shows
245
+ * while the call is still being written.
246
+ */
247
+ function callArguments(args: string): unknown {
248
+ if (args === "") return undefined
249
+ try {
250
+ return JSON.parse(args)
251
+ } catch {
252
+ return args
253
+ }
254
+ }
255
+
256
+ /**
257
+ * The models a person's pick may send back, from the `/config` rows.
258
+ *
259
+ * The `model` row's `options` are carried and its `value` is not. Measured on 2.1.310: the value
260
+ * is the model's own name while the session names one (`claude-sonnet-4-5`), and the row's label
261
+ * for the default while it does not (`Default (recommended)`), which is no name any pick can
262
+ * send. The options are the row's own choices, so every one of them is a value `/config` takes —
263
+ * a shorter list of picks that all run beats a longer one holding a pick that cannot.
264
+ */
265
+ function listedModels(rows: readonly ModConfigRow[]): string[] {
266
+ const row = rows.find((candidate) => candidate.key === "model")
267
+ const options = row?.options ?? []
268
+ // A nameless option is no reference a pick could send back, and `modelReference` is the rule
269
+ // for what a `provider/id` is.
270
+ return options.flatMap((option) => modelReference(CLAUDE_PROVIDER, option) ?? [])
271
+ }
272
+
273
+ /**
274
+ * Claude Code's slash commands as the viewer offers them: the name without its slash, and the
275
+ * source mapped onto the four the frame knows. A name a person could not type is dropped
276
+ * (`isTypableCommandName`), and nothing is marked `terminalOnly`: Claude Code says of no command
277
+ * whether running it opens a dialog, so marking any would be a guess.
278
+ */
279
+ export function claudeAgentStreamCommands(
280
+ commands: readonly ModCommandInfo[],
281
+ ): AgentStreamCommand[] {
282
+ const listed: AgentStreamCommand[] = []
283
+ for (const command of commands) {
284
+ const name = command.name.startsWith("/") ? command.name.slice(1) : command.name
285
+ if (!isTypableCommandName(name)) continue
286
+ listed.push({
287
+ name,
288
+ source:
289
+ command.source === "builtin"
290
+ ? "builtin"
291
+ : command.source === "plugin"
292
+ ? "extension"
293
+ : "prompt",
294
+ ...(command.description === "" ? {} : { description: command.description }),
295
+ })
296
+ }
297
+ return listed
298
+ }
299
+
300
+ export function createClaudeAgentStream(deps: ClaudeAgentStreamDeps): ClaudeAgentStream {
301
+ const { isPane, publisher, now } = deps
302
+ /** The timestamp each `(turnId, index)` step was first seen at, which is its ring slot. */
303
+ const stepAt = new Map<string, number>()
304
+ /** The last timestamp handed to a step, so the next one is strictly past it. */
305
+ let allocated = 0
306
+ let observed: ObservedEffort | undefined
307
+ /** The effort the last pick put in force, which is the effort the mod puts on every later
308
+ * request that names an effort of its own, until a step disagrees or another pick changes it. */
309
+ let picked: ModEffort | undefined
310
+
311
+ /** The timestamp this step keeps for every snapshot of it. */
312
+ const timestampFor = (turnId: string, index: number): number => {
313
+ const key = `${turnId}\u0000${index}`
314
+ const kept = stepAt.get(key)
315
+ if (kept !== undefined) return kept
316
+ const at = Math.max(now(), allocated + 1)
317
+ allocated = at
318
+ stepAt.set(key, at)
319
+ return at
320
+ }
321
+
322
+ const record = (
323
+ subject: string,
324
+ message: ClaudeStreamMessage,
325
+ streaming: boolean,
326
+ /** The Dispatch message this row delivered, where it is one. */
327
+ dispatchMessageId?: string,
328
+ ): void => {
329
+ publisher.record(subject, message, streaming, dispatchMessageId)
330
+ }
331
+
332
+ /** One row of the conversation as the viewer draws it: never streaming, since a row arrives
333
+ * whole, keyed into its own slot by the row's id, and tagged with the Dispatch message it
334
+ * delivered where it is one. */
335
+ const recordRow = (
336
+ subject: string,
337
+ uuid: string,
338
+ role: "assistant" | "user",
339
+ content: readonly ClaudeStreamPart[],
340
+ ): void => {
341
+ record(subject, { content, role, tag: uuid, timestamp: now() }, false, deps.tagFor?.(uuid))
342
+ }
343
+
344
+ /** One `turn.step` snapshot as the assistant message of that step. */
345
+ const recordStep = (subject: string, event: Extract<ModEvent, { kind: "step" }>): void => {
346
+ // The step reports the effort its own request carried, which is the truth about that
347
+ // request: a pick stands only while the steps agree with it. A step whose own effort is
348
+ // another level, or none at all, ends the pick and leaves the step itself to answer -
349
+ // measured live on 2.1.310, a pick of `low` with `claude-haiku-4-5` in force left the header
350
+ // reading `low` while the session's next request carried no effort at all (INFRA-1026).
351
+ if (event.effort !== picked) picked = undefined
352
+ observed = { model: event.model, effort: event.effort }
353
+ const content: ClaudeStreamPart[] = []
354
+ let hiddenThinkingShown = false
355
+ for (const part of event.parts) {
356
+ if (part.type === "text") {
357
+ content.push({ text: part.text, type: "text" })
358
+ } else if (part.type === "reasoning") {
359
+ if (part.text !== "") {
360
+ content.push({ thinking: part.text, type: "thinking" })
361
+ } else if (!hiddenThinkingShown) {
362
+ hiddenThinkingShown = true
363
+ content.push({ thinking: CLAUDE_THINKING_HIDDEN, type: "thinking" })
364
+ }
365
+ } else {
366
+ content.push({
367
+ arguments: callArguments(part.args),
368
+ id: part.id,
369
+ name: part.name,
370
+ type: "toolCall",
371
+ })
372
+ }
373
+ }
374
+ record(
375
+ subject,
376
+ {
377
+ content,
378
+ model: event.model,
379
+ provider: CLAUDE_PROVIDER,
380
+ role: "assistant",
381
+ timestamp: timestampFor(event.turnId, event.index),
382
+ },
383
+ !event.done,
384
+ )
385
+ }
386
+
387
+ /** One row of the conversation, by the door it came in. */
388
+ const recordAppend = (subject: string, event: Extract<ModEvent, { kind: "append" }>): void => {
389
+ const { door, message, origin, uuid } = event
390
+ // The step that produced the answer already carried it, and the publisher would key this row
391
+ // into a slot of its own: publishing both shows the viewer the same answer twice.
392
+ if (door === "response") return
393
+ // A row Claude Code marks `isMeta` is one the person does not see as typed
394
+ // (`ModAppendMessage.isMeta`), so drawing it attributes to a person something they never
395
+ // wrote - unless this session appended the row itself, which is the one `isMeta` row the
396
+ // page must show. Measured on live Claude Code 2.1.310 (INFRA-1026):
397
+ //
398
+ // - a row `$.session.append` writes carries `isMeta: true` (the engine sets it,
399
+ // `hooks/claude-mod.d.ts`, `SessionAppendArgs`): the card this server delivers into a
400
+ // running turn is such a row, and it is all such a card ever becomes, so dropped it
401
+ // leaves the viewer an answer to a message they never saw. It reaches this mapper
402
+ // because the mod reports the rows it appends itself - Claude Code does not re-enter a
403
+ // mod's own `session.append` hook for them (`hooks/mod.ts`) - and `appendedRow` names
404
+ // it by the uuid the session claimed from the stamp the mod put on that report
405
+ // (`ModEvent`'s `wrote`, `./mod-session`). A rule
406
+ // keeping every `isMeta` row of a `plugin` origin cannot stand in for the claim: the
407
+ // engine gives another installed mod's appended rows `{"kind":"plugin","name":"<that
408
+ // mod>"}` (`/legion/scratch/tmp/infra-1026-spike/logs`, where `spike-mod2` and
409
+ // `spike-mod3p` name themselves), so it drew that mod's model-facing context as the
410
+ // person's own words (INFRA-1026 review, round ten);
411
+ // - a `/rename` typed at a terminal left, behind its echo and its
412
+ // `<local-command-stdout>` row, a `type: "user"` row carrying `isMeta: true` and
413
+ // `origin.kind` `engine` whose whole text is `<system-reminder> The user named this
414
+ // session "…". …</system-reminder>`, which the live view drew as the person's own
415
+ // message - model-facing XML under their name;
416
+ // - the `<local-command-caveat>` row of a command run carries `isMeta: true`, and no
417
+ // session appended it: this rule is what the view draws nothing of it by, and the
418
+ // bookkeeping rule of the `command` door below covers the same row where a build
419
+ // leaves the mark off (`./claude-rows`, `isCommandBookkeeping`);
420
+ // - a line the person types while a turn runs is not marked at all: it arrives
421
+ // `origin composer`, `type: "attachment"`, `name: "queued_command"` and no `isMeta`,
422
+ // so it reaches the reader this rule leaves untouched (`./claude-rows`,
423
+ // `claudeMidTurnMessage`).
424
+ if (message.isMeta === true && deps.appendedRow?.(uuid) !== true) return
425
+ // Claude Code's own note of where it put a picture's bytes, which it writes beside the
426
+ // message that carried the picture (`./claude-rows`, `isPictureAttachmentRow`). Nobody wrote
427
+ // it, and published it draws a second user row for the one message the person sent: measured
428
+ // on 2.1.310 in a Legion pane, where a picture sent from the Agents page drew the person's
429
+ // row and this one beside it (INFRA-1026). Checked for every door, since the row is a
430
+ // companion of the prompt that carried the picture and no door of its own.
431
+ if (isPictureAttachmentRow(message.content)) return
432
+ if (door === "prompt") {
433
+ // The prompt at the rows a turn never read is drawn nowhere: it quotes rows this view has
434
+ // already drawn and would show a viewer each of those messages twice
435
+ // (`../hooks/mod-protocol`, `unreadRowPrompt`). The session claims the row Claude Code
436
+ // wrote for it by uuid, as a command run claims its echo row (`runsUnreadPrompt`). Read
437
+ // instead off the prompt's
438
+ // wording, the rule dropped any card carrying those words - a review of this code, or a
439
+ // message quoting one - which reaches the model and is drawn for nobody (INFRA-1026
440
+ // review, round ten). Every other prompt row is drawn: a person's own, and the card an
441
+ // idle session is handed, which is that card and reaches the conversation nowhere else.
442
+ if (deps.runsUnreadPrompt?.(uuid) === true) return
443
+ // The command a pane is seeded with arrives by this door, written to the session's input
444
+ // by the daemon's bridge before anything else happens in the pane: it names the pane, and
445
+ // no person sent it.
446
+ if (isPane && isPaneSeededRename(claudeRowText(message.content))) return
447
+ // Each block crosses as the part it is, as it does by every other door: the person's text
448
+ // stays a part of its own and a picture becomes the `[image]` part the publisher names it
449
+ // (`conversationParts` in `@legion/pi-shared/agent-stream`). Flattened into one
450
+ // "Look at this\n[image]" part the row says what no stored message says, and the Agents
451
+ // page shows the person's message twice (INFRA-1026).
452
+ const content = message.content.map(partOf)
453
+ if (!content.some(shows)) return
454
+ recordRow(subject, uuid, "user", content)
455
+ return
456
+ }
457
+ if (door === "tool-result") {
458
+ for (const block of message.content) {
459
+ const toolCallId = block["tool_use_id"]
460
+ // A tool result's slot is its call's id; a block naming none joins no call, and the
461
+ // publisher would drop it.
462
+ if (typeof toolCallId !== "string") continue
463
+ record(
464
+ subject,
465
+ {
466
+ content: claudeRowText(block["content"]),
467
+ isError: block["is_error"] === true,
468
+ role: "toolResult",
469
+ timestamp: now(),
470
+ toolCallId,
471
+ ...(origin.tool === undefined ? {} : { toolName: origin.tool }),
472
+ },
473
+ false,
474
+ )
475
+ }
476
+ return
477
+ }
478
+ if (door === "command") {
479
+ // A command a person ran writes three rows by this door and only one of them is a message
480
+ // (`./claude-rows`, `COMMAND_BOOKKEEPING_TAGS`): the echo of what was typed, which the
481
+ // view draws as the plain command line the person would have typed - `/clear`,
482
+ // `/effort low` - rather than as the tags Claude Code wrote it in. A row that is neither
483
+ // the echo nor the bookkeeping goes the way every other row does, below.
484
+ const line = claudeEchoedCommandLine(message.content)
485
+ if (line !== undefined) {
486
+ if (isPane && isPaneSeededRename(line)) return
487
+ // The echo of a command this session ran for a person is not drawn at all: it is the
488
+ // command Claude Code ran rather than the one the person sent, and their own line
489
+ // reaches the page as the message Dispatch stored, which the Agents page shows beside
490
+ // this stream. Drawn, a `/cost` the person sent put `/cost` and `/usage` in front of
491
+ // them as two messages of theirs, measured live on 2.1.310 (INFRA-1026).
492
+ if (deps.runsCommandRow?.(uuid) === true) return
493
+ recordRow(subject, uuid, "user", [{ text: line, type: "text" }])
494
+ return
495
+ }
496
+ if (isCommandBookkeeping(message.content)) return
497
+ }
498
+ const content = message.content.map(partOf)
499
+ // A row that shows a reader nothing is the session's bookkeeping, not its conversation, so
500
+ // nothing is recorded for it: the publisher would key it into a slot of its own and the view
501
+ // would draw an empty bubble. Claude Code's engine writes one such row per turn, by the
502
+ // `notice` door (`type: "system"`, `name: "turn_duration"`, no content blocks, measured on
503
+ // 2.1.310), which is one empty bubble per turn of the conversation. A tool result with no
504
+ // output is a result rather than a message and left alone: it went out above, by its call's
505
+ // id.
506
+ if (!content.some(shows)) return
507
+ recordRow(subject, uuid, roleOf(message), content)
508
+ }
509
+
510
+ return {
511
+ record(subject: string, event: ModEvent): void {
512
+ if (event.kind === "step") recordStep(subject, event)
513
+ else if (event.kind === "append") recordAppend(subject, event)
514
+ // Every other event says something about the session rather than its conversation: its id,
515
+ // its model, a turn's boundaries, a request going out. The server reads those; the view
516
+ // shows no message for them.
517
+ },
518
+ pickedEffort(effort: ModEffort): void {
519
+ picked = effort
520
+ },
521
+ settings(
522
+ model: string | undefined,
523
+ rows: readonly ModConfigRow[] | undefined,
524
+ ): AgentStreamSettingsState {
525
+ if (rows === undefined) return { reported: false }
526
+ const reference = modelReference(CLAUDE_PROVIDER, model)
527
+ return {
528
+ models: listedModels(rows),
529
+ reported: true,
530
+ // Claude Code reports the effort on each request's `turn.step` and nowhere else, so a
531
+ // pick is the only other thing that knows one: it set the effort the mod puts on every
532
+ // later request that names an effort of its own, so from the moment it lands it is the
533
+ // level in force, and it survives a model pick whose later step reports that same
534
+ // effort. A step that reports another level, or none at all - a model Claude Code names
535
+ // no effort for takes none whatever the pick set - ends the pick and answers for itself
536
+ // (`recordStep`). Before this session's first request and before any pick there is a
537
+ // selector and no level to show; a step that named no effort is a model with no thinking
538
+ // to adjust. A step of another model is no answer about this one.
539
+ thinking:
540
+ picked !== undefined
541
+ ? { level: String(picked), levels: CLAUDE_EFFORT_LEVELS }
542
+ : observed !== undefined && observed.model === model
543
+ ? observed.effort === undefined
544
+ ? null
545
+ : { level: String(observed.effort), levels: CLAUDE_EFFORT_LEVELS }
546
+ : { level: null, levels: CLAUDE_EFFORT_LEVELS },
547
+ // Absent when the session has no model selected, which is `modelReference`'s own answer
548
+ // for a name that is empty (`AgentStreamSettingsReported.model`).
549
+ ...(reference === undefined ? {} : { model: reference }),
550
+ }
551
+ },
552
+ reset(): void {
553
+ stepAt.clear()
554
+ observed = undefined
555
+ picked = undefined
556
+ },
557
+ }
558
+ }