@fitchmultz/pi-subagents 0.50.0 → 0.52.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 (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. package/src/shared/stream-json.d.ts +0 -10
@@ -0,0 +1,84 @@
1
+ import { ObserverTranscript } from "./observe-transcript.js";
2
+ import { ScrollView } from "./observe-layout.js";
3
+ export class Reading {
4
+ watch;
5
+ transcript;
6
+ scroll;
7
+ following = true;
8
+ pending = false;
9
+ width = 0;
10
+ destination;
11
+ changed;
12
+ constructor(watch, options) {
13
+ this.watch = watch;
14
+ this.changed = options.changed;
15
+ const { sessionFile: file, sessionId, cwd } = watch.launch;
16
+ this.transcript = new ObserverTranscript({
17
+ ...options,
18
+ file,
19
+ sessionId,
20
+ cwd,
21
+ changed: (event) => {
22
+ this.following = false;
23
+ if (event.kind === "history") {
24
+ this.history(event.direction);
25
+ return;
26
+ }
27
+ if (event.kind === "content") {
28
+ this.destination = { id: event.id, offset: 0, end: event.direction === -1 };
29
+ }
30
+ this.changed("content");
31
+ },
32
+ });
33
+ this.transcript.sync();
34
+ this.scroll = new ScrollView(this.transcript, {
35
+ follow: "end",
36
+ primary: false,
37
+ overscroll: "contain",
38
+ scrollbar: "always",
39
+ });
40
+ }
41
+ anchor() {
42
+ return (this.destination ??
43
+ (this.following ? undefined : this.transcript.anchor(this.scroll.scrollTop)));
44
+ }
45
+ resize(width) {
46
+ const changed = this.width >= 100 !== width >= 100;
47
+ this.width = width;
48
+ return changed;
49
+ }
50
+ reveal(anchor) {
51
+ const line = this.transcript.line(anchor);
52
+ const top = line - (anchor.end === true ? this.scroll.viewportHeight : 0);
53
+ const row = line <= 1 && anchor.end !== true && this.scroll.scrollTop === 0;
54
+ this.scroll.scrollTo(row ? 0 : top, { disableFollow: true });
55
+ this.destination = undefined;
56
+ }
57
+ scrollBy(lines) {
58
+ this.following = false;
59
+ this.scroll.scrollTo(this.scroll.scrollTop + lines, { disableFollow: true });
60
+ }
61
+ page(direction) {
62
+ this.following = false;
63
+ const height = Math.max(1, this.scroll.viewportHeight);
64
+ if (this.transcript.contentPage(this.scroll.scrollTop, height, direction)) {
65
+ return;
66
+ }
67
+ // A history row keeps a full scroll above line zero; use the actual scroll result.
68
+ const remaining = this.scroll.scrollBy(direction * height);
69
+ if (remaining === 0) {
70
+ return;
71
+ }
72
+ this.history(direction);
73
+ }
74
+ history(direction) {
75
+ const id = this.transcript.history(direction);
76
+ if (id !== undefined) {
77
+ this.destination = {
78
+ id,
79
+ offset: direction < 0 ? -Math.max(1, this.scroll.viewportHeight) : 1,
80
+ };
81
+ this.changed("history");
82
+ }
83
+ }
84
+ }
@@ -1,132 +1,436 @@
1
- import { rawKeyHint } from "@earendil-works/pi-coding-agent";
2
- import { sliceByColumn, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1
+ import { renderLayoutFrame, dispatchLayoutMouse, TruncatedText, VStack, } from "./observe-layout.js";
2
+ import { errorMessage } from "../shared/unknown.js";
3
3
  import { plainText } from "./journal-tail.js";
4
- export class ObserverScreen {
4
+ import { Reading } from "./observe-reading.js";
5
+ import { ObservationChannel } from "./observe-client.js";
6
+ import { ChildList, Details, activityLabel, appendConversation, appendHeader, appendHints, } from "./observe-chrome.js";
7
+ /** Native layout, cards and scrolling; all controls affect local reading state only. */
8
+ export class ObserverScreen extends VStack {
5
9
  selected;
6
- watching;
10
+ list = new ChildList((id) => this.open(id));
11
+ reading;
12
+ connection = new ObservationChannel(() => this.schedule());
13
+ get client() {
14
+ return this.connection.current;
15
+ }
16
+ observed = { revision: -1, status: "", signature: "" };
17
+ saved = new Map();
18
+ focus = "list";
19
+ details;
7
20
  failure = "";
8
- data;
9
- tui;
10
- theme;
11
- keys;
12
- done;
13
21
  timer;
22
+ indexer;
23
+ lastFlush = -Infinity;
24
+ closed = false;
25
+ frame;
26
+ options;
14
27
  constructor(options) {
15
- this.data = options.data;
16
- this.tui = options.tui;
17
- this.theme = options.theme;
18
- this.keys = options.keys;
19
- this.done = options.done;
28
+ super();
29
+ this.options = options;
20
30
  this.selected = options.selected;
21
- if (this.selected.length > 0) {
31
+ if (options.selected.length > 0) {
22
32
  this.open(this.selected);
23
33
  }
24
- this.timer = setInterval(() => this.update(), 250);
25
- this.timer.unref();
26
- this.update();
34
+ this.structure();
27
35
  }
28
36
  open(id) {
29
37
  try {
30
- this.watching = this.data.watch(id);
31
- this.selected = this.watching.id;
38
+ const watch = this.options.data.watch(id);
39
+ const reading = this.saved.get(watch.launch.id) ??
40
+ new Reading(watch, {
41
+ tui: this.options.tui,
42
+ hideThinking: this.options.hideThinking,
43
+ changed: (kind) => {
44
+ if (this.reading === reading) {
45
+ if (kind === "history") {
46
+ this.flush(true);
47
+ }
48
+ else {
49
+ this.paint();
50
+ }
51
+ }
52
+ },
53
+ loaded: () => {
54
+ if (this.visible() === reading) {
55
+ this.schedule();
56
+ }
57
+ },
58
+ });
59
+ this.reading?.transcript.pause();
60
+ this.connection.clear();
61
+ clearTimeout(this.timer);
62
+ this.timer = undefined;
63
+ clearImmediate(this.indexer);
64
+ this.indexer = undefined;
65
+ this.saved.delete(watch.launch.id);
66
+ this.saved.set(watch.launch.id, reading);
67
+ this.reading = reading;
68
+ this.details = undefined;
69
+ reading.transcript.resume();
70
+ this.selected = watch.launch.id;
71
+ this.focus = "conversation";
32
72
  this.failure = "";
73
+ this.connect();
74
+ this.index();
75
+ this.structure();
76
+ this.options.tui.requestRender();
77
+ for (const key of this.saved.keys()) {
78
+ if (this.saved.size > 4 && key !== this.selected) {
79
+ this.saved.delete(key);
80
+ }
81
+ }
33
82
  }
34
83
  catch (error) {
35
- this.failure = error instanceof Error ? plainText(error.message) : "Journal unavailable.";
84
+ this.failure = plainText(errorMessage(error));
85
+ this.paint();
36
86
  }
37
87
  }
38
- update() {
39
- this.watching?.tail.poll();
40
- this.tui.requestRender();
88
+ visible() {
89
+ return this.focus === "conversation" || (this.reading?.width ?? 0) >= 100
90
+ ? this.reading
91
+ : undefined;
41
92
  }
42
- move(data) {
43
- const rows = this.data.inventory();
44
- const index = Math.max(0, rows.findIndex((row) => row.id === this.selected));
45
- if (this.keys.matches(data, "tui.select.confirm")) {
46
- const row = rows.at(index);
47
- if (row) {
48
- this.open(row.id);
93
+ connect() {
94
+ const reading = this.visible();
95
+ if (!reading) {
96
+ return;
97
+ }
98
+ try {
99
+ const id = reading.watch.launch.id;
100
+ const watch = this.options.data.watch(id);
101
+ if (!watch.identity || this.options.data.row(id)?.live !== true) {
102
+ return;
49
103
  }
104
+ this.connection.connect(watch.identity);
50
105
  }
51
- else if (this.keys.matches(data, "tui.select.down")) {
52
- this.selected = rows[Math.min(rows.length - 1, index + 1)]?.id ?? "";
106
+ catch {
107
+ /* The owner refresh retries; preserve the last confirmed conversation meanwhile. */
108
+ }
109
+ }
110
+ index() {
111
+ const reading = this.visible();
112
+ if (!reading ||
113
+ reading.transcript.journal.native.indexed ||
114
+ this.indexer !== undefined ||
115
+ this.failure.length > 0 ||
116
+ this.closed) {
117
+ return;
53
118
  }
54
- else if (this.keys.matches(data, "tui.select.up")) {
55
- this.selected = rows[Math.max(0, index - 1)]?.id ?? "";
119
+ this.indexer = setImmediate(() => {
120
+ this.indexer = undefined;
121
+ if (this.visible() !== reading) {
122
+ return;
123
+ }
124
+ try {
125
+ reading.transcript.journal.native.refresh();
126
+ this.index();
127
+ }
128
+ catch (error) {
129
+ this.failure = plainText(errorMessage(error));
130
+ }
131
+ this.schedule();
132
+ });
133
+ this.indexer.unref();
134
+ }
135
+ schedule() {
136
+ if (this.closed || this.timer !== undefined) {
137
+ return;
56
138
  }
139
+ this.timer = setTimeout(() => {
140
+ this.timer = undefined;
141
+ this.flush();
142
+ }, Math.max(0, this.lastFlush + 250 - performance.now()));
143
+ this.timer.unref();
57
144
  }
58
- handleInput(data) {
59
- if (this.keys.matches(data, "tui.select.cancel")) {
60
- if (this.watching) {
61
- this.watching = undefined;
62
- this.failure = "";
145
+ snapshot() {
146
+ const revision = this.client?.revision ?? -1;
147
+ const status = this.options.data.row(this.selected)?.status ?? "";
148
+ const signature = JSON.stringify([
149
+ this.options.data.status(),
150
+ this.options.data.inventory().map(({ id, name, status: state }) => [id, name, state]),
151
+ this.client
152
+ ? [this.client.activity, this.client.connected, this.client.model, this.client.thinking]
153
+ : undefined,
154
+ this.selected,
155
+ this.failure,
156
+ this.reading?.transcript.version,
157
+ ]);
158
+ return { revision, status, signature };
159
+ }
160
+ flush(force = false) {
161
+ if (this.closed) {
162
+ return;
163
+ }
164
+ const before = this.observed;
165
+ this.observed = this.snapshot();
166
+ const changed = before.revision !== this.observed.revision || before.status !== this.observed.status;
167
+ const pending = this.reading?.pending;
168
+ const applied = this.apply(force, changed);
169
+ this.lastFlush = performance.now();
170
+ if (force ||
171
+ before.signature !== this.observed.signature ||
172
+ applied ||
173
+ pending !== this.reading?.pending) {
174
+ this.structure();
175
+ this.options.tui.requestRender();
176
+ }
177
+ this.index();
178
+ this.wait();
179
+ }
180
+ apply(force, changed) {
181
+ try {
182
+ return this.refreshReading(force, changed);
183
+ }
184
+ catch (error) {
185
+ this.failure = plainText(errorMessage(error));
186
+ this.saved.delete(this.reading?.watch.launch.id ?? "");
187
+ this.reading?.transcript.pause();
188
+ return true;
189
+ }
190
+ }
191
+ wait() {
192
+ const reading = this.visible();
193
+ const live = this.options.data.row(this.selected)?.live === true;
194
+ if (reading && this.failure.length === 0 && reading.transcript.waiting && live) {
195
+ this.schedule();
196
+ }
197
+ }
198
+ refreshReading(force, changed) {
199
+ const reading = this.visible();
200
+ if (!reading) {
201
+ return false;
202
+ }
203
+ reading.following = reading.following && reading.scroll.isFollowingEnd;
204
+ if (!reading.following && !force) {
205
+ reading.transcript.observe(this.client);
206
+ reading.pending ||= changed;
207
+ return false;
208
+ }
209
+ const client = this.options.data.row(reading.watch.launch.id)?.live === true ? this.client : undefined;
210
+ const applied = reading.transcript.sync(client);
211
+ if (reading.following) {
212
+ reading.scroll.scrollToEnd();
213
+ reading.pending = false;
214
+ }
215
+ return applied;
216
+ }
217
+ update() {
218
+ if (!this.visible()) {
219
+ this.connection.pause();
220
+ this.reading?.transcript.pause();
221
+ }
222
+ this.connect();
223
+ this.flush();
224
+ }
225
+ back() {
226
+ if (this.details) {
227
+ this.details = undefined;
228
+ }
229
+ else if (this.reading) {
230
+ clearTimeout(this.timer);
231
+ this.timer = undefined;
232
+ clearImmediate(this.indexer);
233
+ this.indexer = undefined;
234
+ this.connection.clear();
235
+ this.reading.transcript.pause();
236
+ this.reading = undefined;
237
+ this.focus = "list";
238
+ }
239
+ else {
240
+ this.options.done();
241
+ return;
242
+ }
243
+ this.structure();
244
+ this.options.tui.requestRender();
245
+ }
246
+ move(direction) {
247
+ if (this.focus === "conversation") {
248
+ if (this.details) {
249
+ this.details.scrollBy(direction);
63
250
  }
64
251
  else {
65
- this.done();
252
+ this.visible()?.scrollBy(direction);
66
253
  }
254
+ return;
255
+ }
256
+ const rows = this.options.data.inventory();
257
+ const index = Math.max(0, rows.findIndex((row) => row.id === this.selected));
258
+ const next = Math.max(0, Math.min(rows.length - 1, index + direction));
259
+ this.selected = rows[next]?.id ?? "";
260
+ this.list.reveal(next);
261
+ }
262
+ page(direction) {
263
+ if (this.focus === "list") {
264
+ this.list.scrollBy(direction * this.list.viewportHeight);
265
+ return;
67
266
  }
68
- else if (!this.watching) {
69
- this.move(data);
267
+ if (this.details) {
268
+ this.details.scrollBy(direction * this.details.viewportHeight);
269
+ return;
70
270
  }
71
- this.update();
271
+ this.reading?.page(direction);
72
272
  }
73
- listLines(height) {
74
- const rows = this.data.inventory();
75
- const index = Math.max(0, rows.findIndex((row) => row.id === this.selected));
76
- const size = Math.max(1, height - 1);
77
- const start = Math.max(0, index - size + 1);
78
- const page = rows.slice(start, start + size);
79
- return rows.length === 0
80
- ? ["No child runs in this native parent session."]
81
- : [
82
- `Runs ${start + 1}–${start + page.length} of ${rows.length} · active first`,
83
- ...page.map((row, i) => `${start + i === index ? ">" : " "} ${row.id.slice(0, 8)} ${row.status} · ${row.name}`),
84
- ];
85
- }
86
- journalPath(width, file) {
87
- const text = plainText(file.slice(-2048), 2048);
88
- const size = visibleWidth(text);
89
- const room = Math.max(0, width - 1);
90
- return size > width ? `…${sliceByColumn(text, size - room, room)}` : text;
91
- }
92
- watchLines(height, watch, width) {
93
- const row = this.data.row(this.selected);
273
+ end() {
274
+ if (this.details && this.focus === "conversation") {
275
+ this.details.scrollToEnd();
276
+ return;
277
+ }
278
+ if (!this.reading) {
279
+ return;
280
+ }
281
+ this.focus = "conversation";
282
+ this.reading.transcript.resume();
283
+ this.connect();
284
+ this.reading.following = true;
285
+ this.reading.scroll.scrollToEnd();
286
+ this.reading.transcript.latest();
287
+ this.flush(true);
288
+ }
289
+ confirm() {
290
+ if (this.focus === "list") {
291
+ this.open(this.selected.length > 0 ? this.selected : (this.options.data.inventory()[0]?.id ?? ""));
292
+ }
293
+ else if (this.reading) {
294
+ this.reading.transcript.toggle();
295
+ }
296
+ }
297
+ switchFocus() {
298
+ this.focus = this.focus === "list" && this.reading ? "conversation" : "list";
299
+ if (!this.visible()) {
300
+ this.connection.pause();
301
+ this.reading?.transcript.pause();
302
+ }
303
+ else {
304
+ this.reading?.transcript.resume();
305
+ this.connect();
306
+ this.index();
307
+ }
308
+ }
309
+ toggleDetails() {
310
+ this.details =
311
+ !this.details && this.reading ? new Details(this.reading.watch.launch) : undefined;
312
+ }
313
+ actions() {
94
314
  return [
95
- `${this.selected.slice(0, 8)} ${row?.status ?? "checking"} · ${row?.name ?? this.selected}`,
96
- this.journalPath(width, watch.file),
97
- ...(watch.inherited ? ["Fork context: includes inherited parent activity."] : []),
98
- ...watch.tail.lines.slice(-Math.max(1, height - 3)),
99
- watch.tail.notice,
315
+ ["tui.select.cancel", this.reading ? "back" : "close", () => this.back()],
316
+ ["tui.select.confirm", this.focus === "list" ? "open" : "expand", () => this.confirm()],
317
+ ["tui.select.up", "", () => this.move(-1)],
318
+ ["tui.select.down", "", () => this.move(1)],
319
+ ["tui.select.pageUp", "read up", () => this.page(-1)],
320
+ ["tui.select.pageDown", "read down", () => this.page(1)],
321
+ ["tui.editor.cursorLineEnd", this.details ? "details end" : "live", () => this.end()],
322
+ ["app.session.togglePath", "details", () => this.toggleDetails()],
323
+ [
324
+ "tui.input.tab",
325
+ this.focus === "list" && this.reading ? "conversation" : "children",
326
+ () => this.switchFocus(),
327
+ ],
100
328
  ];
101
329
  }
330
+ perform(action) {
331
+ action();
332
+ this.paint();
333
+ }
334
+ paint() {
335
+ if (this.closed) {
336
+ return;
337
+ }
338
+ this.structure();
339
+ this.options.tui.requestRender();
340
+ }
341
+ handleInput(data) {
342
+ const action = this.actions().find(([key]) => this.options.keys.matches(data, key));
343
+ if (action) {
344
+ this.perform(action[2]);
345
+ }
346
+ }
347
+ conversation(reading) {
348
+ const configuration = this.client ?? reading.watch.launch;
349
+ appendHeader(this, {
350
+ launch: reading.watch.launch,
351
+ model: configuration.model,
352
+ thinking: configuration.thinking,
353
+ activity: activityLabel({
354
+ row: this.options.data.row(reading.watch.launch.id),
355
+ client: this.client,
356
+ connecting: this.connection.connecting,
357
+ }),
358
+ details: this.details,
359
+ toggle: () => this.perform(() => this.toggleDetails()),
360
+ });
361
+ appendConversation(this, {
362
+ list: this.list,
363
+ reading,
364
+ follow: () => this.end(),
365
+ });
366
+ }
367
+ structure() {
368
+ this.clear();
369
+ this.addChild(new TruncatedText(this.options.theme.fg("accent", "Agents · read-only"), 0, 0), {
370
+ basis: 1,
371
+ });
372
+ const rows = this.options.data.inventory();
373
+ if (this.selected.length === 0) {
374
+ this.selected = rows[0]?.id ?? "";
375
+ }
376
+ this.list.update(rows, this.selected, this.options.data.checking());
377
+ const reading = this.visible();
378
+ if (reading) {
379
+ this.conversation(reading);
380
+ }
381
+ else {
382
+ this.addChild(new TruncatedText(this.options.data.status(), 0, 0), { basis: 1 });
383
+ this.addChild(this.list, {
384
+ basis: 0,
385
+ grow: 1,
386
+ minSize: 1,
387
+ });
388
+ }
389
+ const error = this.failure.length > 0 ? this.failure : this.options.data.error();
390
+ if (error.length > 0) {
391
+ this.addChild(new TruncatedText(this.options.theme.fg("warning", error), 0, 0), { basis: 1 });
392
+ }
393
+ appendHints(this, {
394
+ actions: this.actions(),
395
+ keys: this.options.keys,
396
+ perform: (action) => this.perform(action),
397
+ });
398
+ }
102
399
  render(width) {
103
- const height = Math.max(1, Math.min(16, this.tui.terminal.rows - 9));
104
- const content = this.watching
105
- ? this.watchLines(height, this.watching, width)
106
- : this.listLines(height);
107
- const cancel = this.keys.getKeys("tui.select.cancel").join("/");
108
- const hint = this.watching
109
- ? `${rawKeyHint(cancel, "back")} · native published entries, not token deltas`
110
- : [
111
- rawKeyHint([...this.keys.getKeys("tui.select.up"), ...this.keys.getKeys("tui.select.down")].join("/"), "select"),
112
- rawKeyHint(this.keys.getKeys("tui.select.confirm").join("/"), "watch"),
113
- rawKeyHint(cancel, "close"),
114
- ].join(" · ");
115
- const error = this.failure.length > 0 ? this.failure : this.data.error();
116
- return [
117
- this.theme.fg("accent", this.watching?.inherited === true
118
- ? "Agents · read-only · fork context"
119
- : "Agents · read-only"),
120
- this.theme.fg("muted", this.data.status()),
121
- ...content,
122
- this.theme.fg("error", error),
123
- this.theme.fg("dim", hint),
124
- ].map((line) => truncateToWidth(line, width));
125
- }
126
- invalidate() {
127
- this.tui.requestRender();
400
+ const reading = this.reading;
401
+ const anchor = reading?.anchor();
402
+ if (reading?.resize(width) === true) {
403
+ this.structure();
404
+ }
405
+ const frame = () => renderLayoutFrame(this, width, Math.max(1, this.options.tui.terminal.rows), () => this.options.tui.requestRender());
406
+ this.frame = frame();
407
+ const visible = this.visible();
408
+ const selected = visible?.transcript.select(visible.scroll.scrollTop, visible.scroll.viewportHeight) === true;
409
+ if (visible && anchor) {
410
+ visible.reveal(anchor);
411
+ }
412
+ if (selected || (visible && anchor)) {
413
+ this.frame = frame();
414
+ }
415
+ return this.frame.lines;
416
+ }
417
+ handleMouse(event) {
418
+ const result = this.frame ? dispatchLayoutMouse(this, this.frame, event) : undefined;
419
+ const reading = this.visible();
420
+ if (reading?.following === true && !reading.scroll.isFollowingEnd) {
421
+ reading.following = false;
422
+ this.structure();
423
+ }
424
+ return result;
128
425
  }
129
426
  dispose() {
130
- clearInterval(this.timer);
427
+ this.closed = true;
428
+ clearTimeout(this.timer);
429
+ clearImmediate(this.indexer);
430
+ this.connection.clear();
431
+ for (const reading of this.saved.values()) {
432
+ reading.transcript.pause();
433
+ }
434
+ this.saved.clear();
131
435
  }
132
436
  }