@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
@@ -1,167 +1,481 @@
1
- import { rawKeyHint, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { sliceByColumn, truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
3
- import { type JournalTail, plainText } from "./journal-tail.ts";
1
+ import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { Container, TUI, TuiMouseEvent } from "@earendil-works/pi-tui";
3
+ import {
4
+ renderLayoutFrame,
5
+ dispatchLayoutMouse,
6
+ type LayoutFrame,
7
+ TruncatedText,
8
+ VStack,
9
+ } from "./observe-layout.ts";
10
+ import { errorMessage } from "../shared/unknown.ts";
11
+ import { plainText } from "./journal-tail.ts";
12
+ import { Reading } from "./observe-reading.ts";
13
+ import { ObservationChannel, type ObservationClient, type Watch } from "./observe-client.ts";
14
+
15
+ import {
16
+ ChildList,
17
+ Details,
18
+ activityLabel,
19
+ appendConversation,
20
+ appendHeader,
21
+ appendHints,
22
+ type ObserverRow,
23
+ type ScreenAction,
24
+ } from "./observe-chrome.ts";
4
25
 
5
- export type ObserverRow = {
6
- id: string;
7
- name: string;
8
- status: string;
9
- live: boolean;
10
- createdAt: number;
11
- };
12
- export type Watch = { tail: JournalTail; file: string; id: string; inherited: boolean };
13
26
  export interface ObserverData {
14
27
  inventory: () => ObserverRow[];
28
+ checking: () => boolean;
15
29
  row: (id: string) => ObserverRow | undefined;
16
30
  status: () => string;
17
31
  error: () => string;
18
32
  watch: (id: string) => Watch;
19
33
  }
20
- export class ObserverScreen {
34
+ interface ScreenOptions {
35
+ data: ObserverData;
36
+ tui: TUI;
37
+ theme: Theme;
38
+ keys: KeybindingsManager;
39
+ done: () => void;
40
+ selected: string;
41
+ hideThinking: boolean;
42
+ }
43
+
44
+ /** Native layout, cards and scrolling; all controls affect local reading state only. */
45
+ export class ObserverScreen extends VStack {
21
46
  private selected: string;
22
- private watching?: Watch;
47
+ private readonly list = new ChildList((id) => this.open(id));
48
+ private reading?: Reading;
49
+ private readonly connection = new ObservationChannel(() => this.schedule());
50
+ private get client(): ObservationClient | undefined {
51
+ return this.connection.current;
52
+ }
53
+ private observed = { revision: -1, status: "", signature: "" };
54
+ private readonly saved = new Map<string, Reading>();
55
+ private focus: "list" | "conversation" = "list";
56
+ private details?: Details;
23
57
  private failure = "";
24
- private readonly data: ObserverData;
25
- private readonly tui: TUI;
26
- private readonly theme: Theme;
27
- private readonly keys: KeybindingsManager;
28
- private readonly done: () => void;
29
- private readonly timer: NodeJS.Timeout;
30
- constructor(options: {
31
- data: ObserverData;
32
- tui: TUI;
33
- theme: Theme;
34
- keys: KeybindingsManager;
35
- done: () => void;
36
- selected: string;
37
- }) {
38
- this.data = options.data;
39
- this.tui = options.tui;
40
- this.theme = options.theme;
41
- this.keys = options.keys;
42
- this.done = options.done;
58
+ private timer?: NodeJS.Timeout;
59
+ private indexer?: NodeJS.Immediate;
60
+ private lastFlush = -Infinity;
61
+ private closed = false;
62
+ private frame?: LayoutFrame;
63
+ private readonly options: ScreenOptions;
64
+ constructor(options: ScreenOptions) {
65
+ super();
66
+ this.options = options;
43
67
  this.selected = options.selected;
44
- if (this.selected.length > 0) {
68
+ if (options.selected.length > 0) {
45
69
  this.open(this.selected);
46
70
  }
47
- this.timer = setInterval(() => this.update(), 250);
48
- this.timer.unref();
49
- this.update();
71
+ this.structure();
50
72
  }
51
73
  private open(id: string): void {
52
74
  try {
53
- this.watching = this.data.watch(id);
54
- this.selected = this.watching.id;
75
+ const watch = this.options.data.watch(id);
76
+ const reading: Reading =
77
+ this.saved.get(watch.launch.id) ??
78
+ new Reading(watch, {
79
+ tui: this.options.tui,
80
+ hideThinking: this.options.hideThinking,
81
+ changed: (kind) => {
82
+ if (this.reading === reading) {
83
+ if (kind === "history") {
84
+ this.flush(true);
85
+ } else {
86
+ this.paint();
87
+ }
88
+ }
89
+ },
90
+ loaded: () => {
91
+ if (this.visible() === reading) {
92
+ this.schedule();
93
+ }
94
+ },
95
+ });
96
+ this.reading?.transcript.pause();
97
+ this.connection.clear();
98
+ clearTimeout(this.timer);
99
+ this.timer = undefined;
100
+ clearImmediate(this.indexer);
101
+ this.indexer = undefined;
102
+ this.saved.delete(watch.launch.id);
103
+ this.saved.set(watch.launch.id, reading);
104
+ this.reading = reading;
105
+ this.details = undefined;
106
+ reading.transcript.resume();
107
+ this.selected = watch.launch.id;
108
+ this.focus = "conversation";
55
109
  this.failure = "";
110
+ this.connect();
111
+ this.index();
112
+ this.structure();
113
+ this.options.tui.requestRender();
114
+ for (const key of this.saved.keys()) {
115
+ if (this.saved.size > 4 && key !== this.selected) {
116
+ this.saved.delete(key);
117
+ }
118
+ }
56
119
  } catch (error) {
57
- this.failure = error instanceof Error ? plainText(error.message) : "Journal unavailable.";
120
+ this.failure = plainText(errorMessage(error));
121
+ this.paint();
58
122
  }
59
123
  }
60
- update(): void {
61
- this.watching?.tail.poll();
62
- this.tui.requestRender();
124
+ private visible(): Reading | undefined {
125
+ return this.focus === "conversation" || (this.reading?.width ?? 0) >= 100
126
+ ? this.reading
127
+ : undefined;
63
128
  }
64
- private move(data: string): void {
65
- const rows = this.data.inventory();
66
- const index = Math.max(
67
- 0,
68
- rows.findIndex((row) => row.id === this.selected),
69
- );
70
- if (this.keys.matches(data, "tui.select.confirm")) {
71
- const row = rows.at(index);
72
- if (row) {
73
- this.open(row.id);
129
+ private connect(): void {
130
+ const reading = this.visible();
131
+ if (!reading) {
132
+ return;
133
+ }
134
+ try {
135
+ const id = reading.watch.launch.id;
136
+ const watch = this.options.data.watch(id);
137
+ if (!watch.identity || this.options.data.row(id)?.live !== true) {
138
+ return;
74
139
  }
75
- } else if (this.keys.matches(data, "tui.select.down")) {
76
- this.selected = rows[Math.min(rows.length - 1, index + 1)]?.id ?? "";
77
- } else if (this.keys.matches(data, "tui.select.up")) {
78
- this.selected = rows[Math.max(0, index - 1)]?.id ?? "";
140
+ this.connection.connect(watch.identity);
141
+ } catch {
142
+ /* The owner refresh retries; preserve the last confirmed conversation meanwhile. */
79
143
  }
80
144
  }
81
- handleInput(data: string): void {
82
- if (this.keys.matches(data, "tui.select.cancel")) {
83
- if (this.watching) {
84
- this.watching = undefined;
85
- this.failure = "";
86
- } else {
87
- this.done();
145
+ private index(): void {
146
+ const reading = this.visible();
147
+ if (
148
+ !reading ||
149
+ reading.transcript.journal.native.indexed ||
150
+ this.indexer !== undefined ||
151
+ this.failure.length > 0 ||
152
+ this.closed
153
+ ) {
154
+ return;
155
+ }
156
+ this.indexer = setImmediate(() => {
157
+ this.indexer = undefined;
158
+ if (this.visible() !== reading) {
159
+ return;
88
160
  }
89
- } else if (!this.watching) {
90
- this.move(data);
161
+ try {
162
+ reading.transcript.journal.native.refresh();
163
+ this.index();
164
+ } catch (error) {
165
+ this.failure = plainText(errorMessage(error));
166
+ }
167
+ this.schedule();
168
+ });
169
+ this.indexer.unref();
170
+ }
171
+ private schedule(): void {
172
+ if (this.closed || this.timer !== undefined) {
173
+ return;
174
+ }
175
+ this.timer = setTimeout(
176
+ () => {
177
+ this.timer = undefined;
178
+ this.flush();
179
+ },
180
+ Math.max(0, this.lastFlush + 250 - performance.now()),
181
+ );
182
+ this.timer.unref();
183
+ }
184
+ private snapshot(): typeof this.observed {
185
+ const revision = this.client?.revision ?? -1;
186
+ const status = this.options.data.row(this.selected)?.status ?? "";
187
+ const signature = JSON.stringify([
188
+ this.options.data.status(),
189
+ this.options.data.inventory().map(({ id, name, status: state }) => [id, name, state]),
190
+ this.client
191
+ ? [this.client.activity, this.client.connected, this.client.model, this.client.thinking]
192
+ : undefined,
193
+ this.selected,
194
+ this.failure,
195
+ this.reading?.transcript.version,
196
+ ]);
197
+ return { revision, status, signature };
198
+ }
199
+ private flush(force = false): void {
200
+ if (this.closed) {
201
+ return;
202
+ }
203
+ const before = this.observed;
204
+ this.observed = this.snapshot();
205
+ const changed =
206
+ before.revision !== this.observed.revision || before.status !== this.observed.status;
207
+ const pending = this.reading?.pending;
208
+ const applied = this.apply(force, changed);
209
+ this.lastFlush = performance.now();
210
+ if (
211
+ force ||
212
+ before.signature !== this.observed.signature ||
213
+ applied ||
214
+ pending !== this.reading?.pending
215
+ ) {
216
+ this.structure();
217
+ this.options.tui.requestRender();
218
+ }
219
+ this.index();
220
+ this.wait();
221
+ }
222
+ private apply(force: boolean, changed: boolean): boolean {
223
+ try {
224
+ return this.refreshReading(force, changed);
225
+ } catch (error) {
226
+ this.failure = plainText(errorMessage(error));
227
+ this.saved.delete(this.reading?.watch.launch.id ?? "");
228
+ this.reading?.transcript.pause();
229
+ return true;
230
+ }
231
+ }
232
+ private wait(): void {
233
+ const reading = this.visible();
234
+ const live = this.options.data.row(this.selected)?.live === true;
235
+ if (reading && this.failure.length === 0 && reading.transcript.waiting && live) {
236
+ this.schedule();
237
+ }
238
+ }
239
+ private refreshReading(force: boolean, changed: boolean): boolean {
240
+ const reading = this.visible();
241
+ if (!reading) {
242
+ return false;
243
+ }
244
+ reading.following = reading.following && reading.scroll.isFollowingEnd;
245
+ if (!reading.following && !force) {
246
+ reading.transcript.observe(this.client);
247
+ reading.pending ||= changed;
248
+ return false;
249
+ }
250
+ const client =
251
+ this.options.data.row(reading.watch.launch.id)?.live === true ? this.client : undefined;
252
+ const applied = reading.transcript.sync(client);
253
+ if (reading.following) {
254
+ reading.scroll.scrollToEnd();
255
+ reading.pending = false;
256
+ }
257
+ return applied;
258
+ }
259
+ update(): void {
260
+ if (!this.visible()) {
261
+ this.connection.pause();
262
+ this.reading?.transcript.pause();
263
+ }
264
+ this.connect();
265
+ this.flush();
266
+ }
267
+ private back(): void {
268
+ if (this.details) {
269
+ this.details = undefined;
270
+ } else if (this.reading) {
271
+ clearTimeout(this.timer);
272
+ this.timer = undefined;
273
+ clearImmediate(this.indexer);
274
+ this.indexer = undefined;
275
+ this.connection.clear();
276
+ this.reading.transcript.pause();
277
+ this.reading = undefined;
278
+ this.focus = "list";
279
+ } else {
280
+ this.options.done();
281
+ return;
91
282
  }
92
- this.update();
283
+ this.structure();
284
+ this.options.tui.requestRender();
93
285
  }
94
- private listLines(height: number): string[] {
95
- const rows = this.data.inventory();
286
+ private move(direction: number): void {
287
+ if (this.focus === "conversation") {
288
+ if (this.details) {
289
+ this.details.scrollBy(direction);
290
+ } else {
291
+ this.visible()?.scrollBy(direction);
292
+ }
293
+ return;
294
+ }
295
+ const rows = this.options.data.inventory();
96
296
  const index = Math.max(
97
297
  0,
98
298
  rows.findIndex((row) => row.id === this.selected),
99
299
  );
100
- const size = Math.max(1, height - 1);
101
- const start = Math.max(0, index - size + 1);
102
- const page = rows.slice(start, start + size);
103
- return rows.length === 0
104
- ? ["No child runs in this native parent session."]
105
- : [
106
- `Runs ${start + 1}–${start + page.length} of ${rows.length} · active first`,
107
- ...page.map(
108
- (row, i) =>
109
- `${start + i === index ? ">" : " "} ${row.id.slice(0, 8)} ${row.status} · ${row.name}`,
110
- ),
111
- ];
112
- }
113
- private journalPath(width: number, file: string): string {
114
- const text = plainText(file.slice(-2048), 2048);
115
- const size = visibleWidth(text);
116
- const room = Math.max(0, width - 1);
117
- return size > width ? `…${sliceByColumn(text, size - room, room)}` : text;
118
- }
119
- private watchLines(height: number, watch: Watch, width: number): string[] {
120
- const row = this.data.row(this.selected);
300
+ const next = Math.max(0, Math.min(rows.length - 1, index + direction));
301
+ this.selected = rows[next]?.id ?? "";
302
+ this.list.reveal(next);
303
+ }
304
+ private page(direction: -1 | 1): void {
305
+ if (this.focus === "list") {
306
+ this.list.scrollBy(direction * this.list.viewportHeight);
307
+ return;
308
+ }
309
+ if (this.details) {
310
+ this.details.scrollBy(direction * this.details.viewportHeight);
311
+ return;
312
+ }
313
+ this.reading?.page(direction);
314
+ }
315
+ private end(): void {
316
+ if (this.details && this.focus === "conversation") {
317
+ this.details.scrollToEnd();
318
+ return;
319
+ }
320
+ if (!this.reading) {
321
+ return;
322
+ }
323
+ this.focus = "conversation";
324
+ this.reading.transcript.resume();
325
+ this.connect();
326
+ this.reading.following = true;
327
+ this.reading.scroll.scrollToEnd();
328
+ this.reading.transcript.latest();
329
+ this.flush(true);
330
+ }
331
+ private confirm(): void {
332
+ if (this.focus === "list") {
333
+ this.open(
334
+ this.selected.length > 0 ? this.selected : (this.options.data.inventory()[0]?.id ?? ""),
335
+ );
336
+ } else if (this.reading) {
337
+ this.reading.transcript.toggle();
338
+ }
339
+ }
340
+ private switchFocus(): void {
341
+ this.focus = this.focus === "list" && this.reading ? "conversation" : "list";
342
+ if (!this.visible()) {
343
+ this.connection.pause();
344
+ this.reading?.transcript.pause();
345
+ } else {
346
+ this.reading?.transcript.resume();
347
+ this.connect();
348
+ this.index();
349
+ }
350
+ }
351
+ private toggleDetails(): void {
352
+ this.details =
353
+ !this.details && this.reading ? new Details(this.reading.watch.launch) : undefined;
354
+ }
355
+ private actions(): ScreenAction[] {
121
356
  return [
122
- `${this.selected.slice(0, 8)} ${row?.status ?? "checking"} · ${row?.name ?? this.selected}`,
123
- this.journalPath(width, watch.file),
124
- ...(watch.inherited ? ["Fork context: includes inherited parent activity."] : []),
125
- ...watch.tail.lines.slice(-Math.max(1, height - 3)),
126
- watch.tail.notice,
357
+ ["tui.select.cancel", this.reading ? "back" : "close", () => this.back()],
358
+ ["tui.select.confirm", this.focus === "list" ? "open" : "expand", () => this.confirm()],
359
+ ["tui.select.up", "", () => this.move(-1)],
360
+ ["tui.select.down", "", () => this.move(1)],
361
+ ["tui.select.pageUp", "read up", () => this.page(-1)],
362
+ ["tui.select.pageDown", "read down", () => this.page(1)],
363
+ ["tui.editor.cursorLineEnd", this.details ? "details end" : "live", () => this.end()],
364
+ ["app.session.togglePath", "details", () => this.toggleDetails()],
365
+ [
366
+ "tui.input.tab",
367
+ this.focus === "list" && this.reading ? "conversation" : "children",
368
+ () => this.switchFocus(),
369
+ ],
127
370
  ];
128
371
  }
129
- render(width: number): string[] {
130
- const height = Math.max(1, Math.min(16, this.tui.terminal.rows - 9));
131
- const content = this.watching
132
- ? this.watchLines(height, this.watching, width)
133
- : this.listLines(height);
134
- const cancel = this.keys.getKeys("tui.select.cancel").join("/");
135
- const hint = this.watching
136
- ? `${rawKeyHint(cancel, "back")} · native published entries, not token deltas`
137
- : [
138
- rawKeyHint(
139
- [...this.keys.getKeys("tui.select.up"), ...this.keys.getKeys("tui.select.down")].join(
140
- "/",
141
- ),
142
- "select",
143
- ),
144
- rawKeyHint(this.keys.getKeys("tui.select.confirm").join("/"), "watch"),
145
- rawKeyHint(cancel, "close"),
146
- ].join(" · ");
147
- const error = this.failure.length > 0 ? this.failure : this.data.error();
148
- return [
149
- this.theme.fg(
150
- "accent",
151
- this.watching?.inherited === true
152
- ? "Agents · read-only · fork context"
153
- : "Agents · read-only",
154
- ),
155
- this.theme.fg("muted", this.data.status()),
156
- ...content,
157
- this.theme.fg("error", error),
158
- this.theme.fg("dim", hint),
159
- ].map((line) => truncateToWidth(line, width));
160
- }
161
- invalidate(): void {
162
- this.tui.requestRender();
372
+ private perform(action: () => void): void {
373
+ action();
374
+ this.paint();
375
+ }
376
+ private paint(): void {
377
+ if (this.closed) {
378
+ return;
379
+ }
380
+ this.structure();
381
+ this.options.tui.requestRender();
382
+ }
383
+ handleInput(data: string): void {
384
+ const action = this.actions().find(([key]) => this.options.keys.matches(data, key));
385
+ if (action) {
386
+ this.perform(action[2]);
387
+ }
388
+ }
389
+ private conversation(reading: Reading): void {
390
+ const configuration = this.client ?? reading.watch.launch;
391
+ appendHeader(this, {
392
+ launch: reading.watch.launch,
393
+ model: configuration.model,
394
+ thinking: configuration.thinking,
395
+ activity: activityLabel({
396
+ row: this.options.data.row(reading.watch.launch.id),
397
+ client: this.client,
398
+ connecting: this.connection.connecting,
399
+ }),
400
+ details: this.details,
401
+ toggle: () => this.perform(() => this.toggleDetails()),
402
+ });
403
+ appendConversation(this, {
404
+ list: this.list,
405
+ reading,
406
+ follow: () => this.end(),
407
+ });
408
+ }
409
+ private structure(): void {
410
+ this.clear();
411
+ this.addChild(new TruncatedText(this.options.theme.fg("accent", "Agents · read-only"), 0, 0), {
412
+ basis: 1,
413
+ });
414
+ const rows = this.options.data.inventory();
415
+ if (this.selected.length === 0) {
416
+ this.selected = rows[0]?.id ?? "";
417
+ }
418
+ this.list.update(rows, this.selected, this.options.data.checking());
419
+ const reading = this.visible();
420
+ if (reading) {
421
+ this.conversation(reading);
422
+ } else {
423
+ this.addChild(new TruncatedText(this.options.data.status(), 0, 0), { basis: 1 });
424
+ this.addChild(this.list, {
425
+ basis: 0,
426
+ grow: 1,
427
+ minSize: 1,
428
+ });
429
+ }
430
+ const error = this.failure.length > 0 ? this.failure : this.options.data.error();
431
+ if (error.length > 0) {
432
+ this.addChild(new TruncatedText(this.options.theme.fg("warning", error), 0, 0), { basis: 1 });
433
+ }
434
+ appendHints(this, {
435
+ actions: this.actions(),
436
+ keys: this.options.keys,
437
+ perform: (action) => this.perform(action),
438
+ });
439
+ }
440
+ override render(width: number): string[] {
441
+ const reading = this.reading;
442
+ const anchor = reading?.anchor();
443
+ if (reading?.resize(width) === true) {
444
+ this.structure();
445
+ }
446
+ const frame = () =>
447
+ renderLayoutFrame(this, width, Math.max(1, this.options.tui.terminal.rows), () =>
448
+ this.options.tui.requestRender(),
449
+ );
450
+ this.frame = frame();
451
+ const visible = this.visible();
452
+ const selected =
453
+ visible?.transcript.select(visible.scroll.scrollTop, visible.scroll.viewportHeight) === true;
454
+ if (visible && anchor) {
455
+ visible.reveal(anchor);
456
+ }
457
+ if (selected || (visible && anchor)) {
458
+ this.frame = frame();
459
+ }
460
+ return this.frame.lines;
461
+ }
462
+ override handleMouse(event: TuiMouseEvent): ReturnType<Container["handleMouse"]> {
463
+ const result = this.frame ? dispatchLayoutMouse(this, this.frame, event) : undefined;
464
+ const reading = this.visible();
465
+ if (reading?.following === true && !reading.scroll.isFollowingEnd) {
466
+ reading.following = false;
467
+ this.structure();
468
+ }
469
+ return result;
163
470
  }
164
471
  dispose(): void {
165
- clearInterval(this.timer);
472
+ this.closed = true;
473
+ clearTimeout(this.timer);
474
+ clearImmediate(this.indexer);
475
+ this.connection.clear();
476
+ for (const reading of this.saved.values()) {
477
+ reading.transcript.pause();
478
+ }
479
+ this.saved.clear();
166
480
  }
167
481
  }