@fitchmultz/pi-subagents 0.50.0 → 0.51.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -0,0 +1,304 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
+ import type { Component, TUI, TuiMouseEvent } from "@earendil-works/pi-tui";
3
+ import { isRecord } from "../shared/unknown.ts";
4
+ import type { ViewedBlock, ViewedTool } from "./observe-client.ts";
5
+ import type { ConversationPage } from "./journal-tail.ts";
6
+ import {
7
+ AssistantMessageComponent,
8
+ Container,
9
+ Text,
10
+ ToolExecutionComponent,
11
+ createBashToolDefinition,
12
+ createEditToolDefinition,
13
+ createFindToolDefinition,
14
+ createGrepToolDefinition,
15
+ createLsToolDefinition,
16
+ createPowerShellToolDefinition,
17
+ createReadToolDefinition,
18
+ createWriteToolDefinition,
19
+ } from "./observe-layout.ts";
20
+
21
+ export interface CardOptions {
22
+ tui: TUI;
23
+ cwd: string;
24
+ hideThinking: boolean;
25
+ }
26
+ const renderers = Object.entries({
27
+ bash: createBashToolDefinition,
28
+ edit: createEditToolDefinition,
29
+ find: createFindToolDefinition,
30
+ grep: createGrepToolDefinition,
31
+ ls: createLsToolDefinition,
32
+ powershell: createPowerShellToolDefinition,
33
+ read: createReadToolDefinition,
34
+ write: createWriteToolDefinition,
35
+ });
36
+ function nativeTool(
37
+ name: string,
38
+ id: string,
39
+ args: unknown,
40
+ options: CardOptions,
41
+ ): InstanceType<typeof ToolExecutionComponent> {
42
+ return new ToolExecutionComponent(
43
+ name,
44
+ id,
45
+ args,
46
+ { showImages: false, outputPad: 0 },
47
+ renderers.find(([key]) => key === name)?.[1](options.cwd) ?? {},
48
+ options.tui,
49
+ options.cwd,
50
+ );
51
+ }
52
+
53
+ export function blocks(value: unknown): ViewedBlock[] {
54
+ if (typeof value === "string") {
55
+ return [{ type: "text", text: value }];
56
+ }
57
+ if (!Array.isArray(value)) {
58
+ return [];
59
+ }
60
+ return (value as unknown[]).flatMap((part): ViewedBlock[] => {
61
+ if (!isRecord(part)) {
62
+ return [];
63
+ }
64
+ if (part.type === "text" && typeof part.text === "string") {
65
+ return [{ type: "text", text: part.text }];
66
+ }
67
+ if (part.type === "thinking" && typeof part.thinking === "string") {
68
+ return [{ type: "thinking", text: part.thinking }];
69
+ }
70
+ if (part.type === "toolCall" && typeof part.id === "string" && typeof part.name === "string") {
71
+ return [{ type: "toolCall", id: part.id, name: part.name, args: part.arguments }];
72
+ }
73
+ return [
74
+ {
75
+ type: "text",
76
+ text: `[${typeof part.type === "string" ? part.type : "non-text"} attachment]`,
77
+ },
78
+ ];
79
+ });
80
+ }
81
+ export function projected(value: unknown): unknown {
82
+ if (!isRecord(value)) {
83
+ return undefined;
84
+ }
85
+ return "json" in value ? value.json : undefined;
86
+ }
87
+ export function assistant(content: ViewedBlock[], streaming: boolean): AssistantMessage {
88
+ // Rendering only. Accounting and completion still use native journals and receipts.
89
+ return {
90
+ role: "assistant",
91
+ api: "observer",
92
+ provider: "observer",
93
+ model: "",
94
+ timestamp: 0,
95
+ stopReason: streaming ? "pending" : "stop",
96
+ usage: {
97
+ input: 0,
98
+ output: 0,
99
+ cacheRead: 0,
100
+ cacheWrite: 0,
101
+ totalTokens: 0,
102
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
103
+ },
104
+ content: content.flatMap((block): AssistantMessage["content"] => {
105
+ if (block.type === "thinking") {
106
+ return [{ type: "thinking", thinking: block.text ?? "" }];
107
+ }
108
+ return block.type === "text" ? [{ type: "text", text: block.text ?? "" }] : [];
109
+ }),
110
+ };
111
+ }
112
+
113
+ export class Card implements Component {
114
+ id: string;
115
+ readonly content = new Container();
116
+ readonly footer = new Container();
117
+ assistant?: InstanceType<typeof AssistantMessageComponent>;
118
+ status?: InstanceType<typeof Text>;
119
+ blocks?: ViewedBlock[];
120
+ ended?: boolean;
121
+ value?: unknown;
122
+ tool?: InstanceType<typeof ToolExecutionComponent>;
123
+ selected = false;
124
+ expanded = false;
125
+ ready = false;
126
+ page = 0;
127
+ pages = 1;
128
+ private cache?: { width: number; lines: string[] };
129
+ private arguments?: unknown;
130
+ private phase = "";
131
+ constructor(id: string) {
132
+ this.id = id;
133
+ }
134
+ publish(page: ConversationPage, args: unknown, options: CardOptions): void {
135
+ this.content.clear();
136
+ this.footer.clear();
137
+ this.ready = true;
138
+ this.page = page.page;
139
+ this.pages = page.pages;
140
+ const { message } = page;
141
+ const content = blocks(message.content);
142
+ if (message.role === "assistant") {
143
+ this.content.addChild(
144
+ new AssistantMessageComponent(assistant(content, false), options.hideThinking),
145
+ );
146
+ if (typeof message.errorMessage === "string") {
147
+ this.content.addChild(new Text(message.errorMessage, 0, 0));
148
+ }
149
+ } else if (message.role === "toolResult") {
150
+ this.publishTool(message, args, options);
151
+ } else {
152
+ this.content.addChild(
153
+ new Text(
154
+ `${typeof message.role === "string" ? message.role : "message"}\n${content.map((part) => part.text ?? "").join("\n")}`,
155
+ 0,
156
+ 0,
157
+ ),
158
+ );
159
+ }
160
+ }
161
+ private publishTool(message: Record<string, unknown>, args: unknown, options: CardOptions): void {
162
+ this.tool ??= nativeTool(
163
+ typeof message.toolName === "string" ? message.toolName : "tool",
164
+ typeof message.toolCallId === "string" ? message.toolCallId : this.id,
165
+ args ?? {},
166
+ options,
167
+ );
168
+ this.tool.updateResult({
169
+ content: blocks(message.content)
170
+ .filter((part) => part.type === "text")
171
+ .map((part) => ({ type: "text", text: part.text })),
172
+ isError: message.isError === true,
173
+ });
174
+ this.tool.setExpanded(this.expanded);
175
+ this.content.addChild(this.tool);
176
+ this.footer.addChild(
177
+ new Text("Arguments are a bounded preview. Exact call in Details journal.", 0, 0),
178
+ );
179
+ }
180
+ private configureTool(value: ViewedTool, options: CardOptions): void {
181
+ if (!this.tool) {
182
+ this.tool = nativeTool(value.name, value.id, projected(value.args) ?? {}, options);
183
+ this.tool.setExpanded(this.expanded);
184
+ this.content.addChild(this.tool);
185
+ this.status = new Text("", 0, 0);
186
+ this.content.addChild(this.status);
187
+ }
188
+ if (this.arguments !== value.args) {
189
+ this.tool.updateArgs(projected(value.args) ?? {});
190
+ this.arguments = value.args;
191
+ this.invalidate();
192
+ }
193
+ }
194
+ updateTool(value: ViewedTool, options: CardOptions, preparing: boolean): void {
195
+ this.configureTool(value, options);
196
+ const ended = value.end !== undefined;
197
+ const source = value.end ?? { result: value.partial, isError: false };
198
+ let phase = preparing ? "Preparing tool call" : "Running";
199
+ if (ended) {
200
+ phase = "Awaiting journal publication";
201
+ }
202
+ if (this.value !== source.result || this.phase !== phase) {
203
+ if (source.result !== undefined) {
204
+ this.preview(source.result, source.isError, !ended);
205
+ }
206
+ this.status?.setText(phase);
207
+ this.phase = phase;
208
+ this.value = source.result;
209
+ this.ended = ended;
210
+ this.invalidate();
211
+ }
212
+ }
213
+ private preview(value: unknown, isError: boolean, partial: boolean): void {
214
+ const result = projected(value) ?? {
215
+ content: [
216
+ { type: "text", text: "Live preview unavailable; full result appears after publication." },
217
+ ],
218
+ };
219
+ if (isRecord(result)) {
220
+ this.tool?.updateResult(
221
+ {
222
+ content: blocks(result.content)
223
+ .filter((part) => part.type === "text")
224
+ .map((part) => ({ type: "text", text: part.text })),
225
+ isError,
226
+ },
227
+ partial,
228
+ );
229
+ }
230
+ }
231
+ stream(message: { blocks: ViewedBlock[]; ended: boolean }, hideThinking: boolean): void {
232
+ if (!this.assistant) {
233
+ this.assistant = new AssistantMessageComponent(undefined, hideThinking);
234
+ this.status = new Text("", 0, 0);
235
+ this.content.addChild(this.assistant);
236
+ this.content.addChild(this.status);
237
+ }
238
+ if (this.matches(message.blocks, message.ended)) {
239
+ return;
240
+ }
241
+ this.assistant.updateContent(
242
+ assistant(message.blocks.filter(Boolean), !message.ended),
243
+ !message.ended,
244
+ );
245
+ this.status?.setText(message.ended ? "Awaiting journal publication" : "In progress");
246
+ this.blocks = message.blocks.slice();
247
+ this.ended = message.ended;
248
+ this.invalidate();
249
+ }
250
+ invalidate(): void {
251
+ this.cache = undefined;
252
+ this.content.invalidate();
253
+ this.footer.invalidate();
254
+ }
255
+ render(width: number): string[] {
256
+ if (this.cache?.width !== width) {
257
+ const lines = [...this.content.render(width), ...this.footer.render(width)];
258
+ if (this.selected && this.tool && lines.length > 0) {
259
+ lines[0] = "> Selected tool".slice(0, width);
260
+ }
261
+ this.cache = { width, lines };
262
+ }
263
+ return this.cache.lines;
264
+ }
265
+ private matches(content: ViewedBlock[], ended: boolean): boolean {
266
+ return (
267
+ this.ended === ended &&
268
+ this.blocks?.length === content.length &&
269
+ content.every((block, index) => this.blocks?.[index] === block)
270
+ );
271
+ }
272
+ toggle(): void {
273
+ if (this.tool) {
274
+ this.expanded = !this.expanded;
275
+ this.tool.setExpanded(this.expanded);
276
+ this.cache = undefined;
277
+ }
278
+ }
279
+ handleMouse(event: TuiMouseEvent): ReturnType<NonNullable<Component["handleMouse"]>> {
280
+ const body = this.content.render(event.width).length;
281
+ if (event.y >= body) {
282
+ return this.footer.handleMouse({
283
+ ...event,
284
+ y: event.y - body,
285
+ height: Math.max(0, event.height - body),
286
+ });
287
+ }
288
+ if (
289
+ this.tool &&
290
+ event.button === "left" &&
291
+ (event.type === "press" || event.type === "click")
292
+ ) {
293
+ if (event.type === "click") {
294
+ this.toggle();
295
+ }
296
+ return { handled: true, render: event.type === "click" };
297
+ }
298
+ const result = this.content.handleMouse(event);
299
+ if (result?.handled) {
300
+ this.cache = undefined;
301
+ }
302
+ return result;
303
+ }
304
+ }
@@ -0,0 +1,170 @@
1
+ import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
+ import type { Component } from "@earendil-works/pi-tui";
3
+ import { actionHints, actionText } from "../shared/tui-action.ts";
4
+ import {
5
+ Container,
6
+ HStack,
7
+ rawKeyHint,
8
+ ScrollView,
9
+ Text,
10
+ TruncatedText,
11
+ type VStack,
12
+ } from "./observe-layout.ts";
13
+ import type { Launch } from "./contracts.ts";
14
+ import { plainText } from "./journal-tail.ts";
15
+
16
+ export interface ObserverRow {
17
+ id: string;
18
+ name: string;
19
+ status: string;
20
+ live: boolean;
21
+ createdAt: number;
22
+ activity?: string;
23
+ }
24
+
25
+ export class Details extends ScrollView {
26
+ constructor(launch: Launch) {
27
+ super(
28
+ new Text(
29
+ `${launch.context === "fork" ? "Fork context: includes inherited parent activity.\n" : ""}${launch.id}\nAssignment preview: ${plainText(launch.prompt, 160)}\n${launch.sessionFile}`,
30
+ 0,
31
+ 0,
32
+ ),
33
+ { primary: false, scrollbar: "always" },
34
+ );
35
+ }
36
+ }
37
+
38
+ export class ChildList extends ScrollView {
39
+ private readonly list: InstanceType<typeof Container>;
40
+ private readonly open: (id: string) => void;
41
+ constructor(open: (id: string) => void) {
42
+ const list = new Container();
43
+ super(list, { primary: false, scrollbar: "always" });
44
+ this.list = list;
45
+ this.open = open;
46
+ }
47
+ update(rows: ObserverRow[], selected: string, checking: boolean): void {
48
+ this.list.clear();
49
+ if (rows.length === 0) {
50
+ this.list.addChild(
51
+ new Text(
52
+ checking
53
+ ? "Checking saved child runs…"
54
+ : "No child runs to show. Completed runs are hidden.",
55
+ 0,
56
+ 0,
57
+ ),
58
+ );
59
+ }
60
+ for (const row of rows) {
61
+ this.list.addChild(
62
+ actionText(`${row.id === selected ? ">" : " "} ${row.status} · ${row.name}`, () =>
63
+ this.open(row.id),
64
+ ),
65
+ );
66
+ }
67
+ }
68
+ reveal(index: number): void {
69
+ this.scrollTo(Math.max(0, index - Math.floor(this.viewportHeight / 2)));
70
+ }
71
+ }
72
+
73
+ export type ScreenAction = [Parameters<KeybindingsManager["matches"]>[1], string, () => void];
74
+
75
+ /** Header activity label: a live connection wins over the last known row status. */
76
+ export function activityLabel(options: {
77
+ row: ObserverRow | undefined;
78
+ client: { connected: boolean; activity: string } | undefined;
79
+ connecting: boolean;
80
+ }): string {
81
+ const { row, client } = options;
82
+ if (row?.live !== true) {
83
+ return row?.status ?? "checking";
84
+ }
85
+ if (client?.connected === true) {
86
+ return client.activity;
87
+ }
88
+ if (client) {
89
+ return "Reconnecting";
90
+ }
91
+ return options.connecting ? "Connecting" : (row.activity ?? row.status);
92
+ }
93
+
94
+ /** Footer key hints press the same actions as the keyboard table. */
95
+ export function appendHints(
96
+ target: InstanceType<typeof VStack>,
97
+ options: {
98
+ actions: readonly ScreenAction[];
99
+ keys: KeybindingsManager;
100
+ perform: (action: () => void) => void;
101
+ },
102
+ ): void {
103
+ const hints = options.actions
104
+ .filter(([, label]) => label.length > 0)
105
+ .map(([key, label, action]) => ({
106
+ text: rawKeyHint(options.keys.getKeys(key)[0] ?? "", label),
107
+ action: () => options.perform(action),
108
+ }));
109
+ target.addChild(actionHints(hints), { basis: 1 });
110
+ }
111
+
112
+ /** Conversation body: persistent child strip beside the transcript, plus the follow footer. */
113
+ export function appendConversation(
114
+ target: InstanceType<typeof VStack>,
115
+ options: {
116
+ list: ChildList;
117
+ reading: { scroll: Component; following: boolean; pending: boolean };
118
+ follow: () => void;
119
+ },
120
+ ): void {
121
+ target.addChild(
122
+ new HStack(
123
+ [
124
+ {
125
+ component: options.list,
126
+ basis: 27,
127
+ visible: (viewport) => viewport.width >= 100,
128
+ },
129
+ { component: options.reading.scroll, basis: 0, grow: 1, minSize: 1 },
130
+ ],
131
+ { gap: 1 },
132
+ ),
133
+ { basis: 0, grow: 1, minSize: 1 },
134
+ );
135
+ const { following, pending } = options.reading;
136
+ let follow = pending
137
+ ? "New activity below · End to follow live"
138
+ : "Reading earlier · End to follow live";
139
+ if (following) {
140
+ follow = "Following live";
141
+ }
142
+ target.addChild(actionText(follow, options.follow), { basis: 1 });
143
+ }
144
+
145
+ export function appendHeader(
146
+ target: InstanceType<typeof VStack>,
147
+ options: {
148
+ launch: Launch;
149
+ model: string;
150
+ thinking: string;
151
+ activity: string;
152
+ details?: Details;
153
+ toggle: () => void;
154
+ },
155
+ ): void {
156
+ const { launch } = options;
157
+ target.addChild(
158
+ actionText(
159
+ `${plainText(launch.name ?? "Child", 32)} · ${options.activity} · Details`,
160
+ options.toggle,
161
+ ),
162
+ { basis: 1 },
163
+ );
164
+ target.addChild(new TruncatedText(`${options.model} · Thinking ${options.thinking}`, 0, 0), {
165
+ basis: 1,
166
+ });
167
+ if (options.details) {
168
+ target.addChild(options.details, { maxSize: 5 });
169
+ }
170
+ }