@gotgenes/pi-subagents 1.0.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 (86) hide show
  1. package/.markdownlint-cli2.yaml +19 -0
  2. package/.prettierignore +5 -0
  3. package/.release-please-manifest.json +3 -0
  4. package/AGENTS.md +85 -0
  5. package/CHANGELOG.md +495 -0
  6. package/LICENSE +21 -0
  7. package/README.md +528 -0
  8. package/dist/agent-manager.d.ts +108 -0
  9. package/dist/agent-manager.js +390 -0
  10. package/dist/agent-runner.d.ts +93 -0
  11. package/dist/agent-runner.js +428 -0
  12. package/dist/agent-types.d.ts +48 -0
  13. package/dist/agent-types.js +136 -0
  14. package/dist/context.d.ts +12 -0
  15. package/dist/context.js +56 -0
  16. package/dist/cross-extension-rpc.d.ts +46 -0
  17. package/dist/cross-extension-rpc.js +54 -0
  18. package/dist/custom-agents.d.ts +14 -0
  19. package/dist/custom-agents.js +127 -0
  20. package/dist/default-agents.d.ts +7 -0
  21. package/dist/default-agents.js +119 -0
  22. package/dist/env.d.ts +6 -0
  23. package/dist/env.js +28 -0
  24. package/dist/group-join.d.ts +32 -0
  25. package/dist/group-join.js +116 -0
  26. package/dist/index.d.ts +13 -0
  27. package/dist/index.js +1731 -0
  28. package/dist/invocation-config.d.ts +22 -0
  29. package/dist/invocation-config.js +15 -0
  30. package/dist/memory.d.ts +49 -0
  31. package/dist/memory.js +151 -0
  32. package/dist/model-resolver.d.ts +19 -0
  33. package/dist/model-resolver.js +62 -0
  34. package/dist/output-file.d.ts +24 -0
  35. package/dist/output-file.js +86 -0
  36. package/dist/prompts.d.ts +29 -0
  37. package/dist/prompts.js +72 -0
  38. package/dist/schedule-store.d.ts +36 -0
  39. package/dist/schedule-store.js +144 -0
  40. package/dist/schedule.d.ts +109 -0
  41. package/dist/schedule.js +338 -0
  42. package/dist/settings.d.ts +66 -0
  43. package/dist/settings.js +130 -0
  44. package/dist/skill-loader.d.ts +24 -0
  45. package/dist/skill-loader.js +93 -0
  46. package/dist/types.d.ts +164 -0
  47. package/dist/types.js +5 -0
  48. package/dist/ui/agent-widget.d.ts +134 -0
  49. package/dist/ui/agent-widget.js +451 -0
  50. package/dist/ui/conversation-viewer.d.ts +35 -0
  51. package/dist/ui/conversation-viewer.js +252 -0
  52. package/dist/ui/schedule-menu.d.ts +16 -0
  53. package/dist/ui/schedule-menu.js +95 -0
  54. package/dist/usage.d.ts +50 -0
  55. package/dist/usage.js +49 -0
  56. package/dist/worktree.d.ts +36 -0
  57. package/dist/worktree.js +139 -0
  58. package/docs/decisions/0001-deferred-patches.md +75 -0
  59. package/package.json +68 -0
  60. package/prek.toml +24 -0
  61. package/release-please-config.json +22 -0
  62. package/src/agent-manager.ts +482 -0
  63. package/src/agent-runner.ts +625 -0
  64. package/src/agent-types.ts +164 -0
  65. package/src/context.ts +58 -0
  66. package/src/cross-extension-rpc.ts +95 -0
  67. package/src/custom-agents.ts +136 -0
  68. package/src/default-agents.ts +123 -0
  69. package/src/env.ts +33 -0
  70. package/src/group-join.ts +141 -0
  71. package/src/index.ts +1894 -0
  72. package/src/invocation-config.ts +40 -0
  73. package/src/memory.ts +165 -0
  74. package/src/model-resolver.ts +81 -0
  75. package/src/output-file.ts +96 -0
  76. package/src/prompts.ts +105 -0
  77. package/src/schedule-store.ts +143 -0
  78. package/src/schedule.ts +365 -0
  79. package/src/settings.ts +186 -0
  80. package/src/skill-loader.ts +102 -0
  81. package/src/types.ts +176 -0
  82. package/src/ui/agent-widget.ts +533 -0
  83. package/src/ui/conversation-viewer.ts +261 -0
  84. package/src/ui/schedule-menu.ts +104 -0
  85. package/src/usage.ts +60 -0
  86. package/src/worktree.ts +162 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * agent-widget.ts — Persistent widget showing running/completed agents above the editor.
3
+ *
4
+ * Displays a tree of agents with animated spinners, live stats, and activity descriptions.
5
+ * Uses the callback form of setWidget for themed rendering.
6
+ */
7
+ import { truncateToWidth } from "@earendil-works/pi-tui";
8
+ import { getConfig } from "../agent-types.js";
9
+ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
+ // ---- Constants ----
11
+ /** Maximum number of rendered lines before overflow collapse kicks in. */
12
+ const MAX_WIDGET_LINES = 12;
13
+ /** Braille spinner frames for animated running indicator. */
14
+ export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
15
+ /** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
16
+ export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
17
+ /** Tool name → human-readable action for activity descriptions. */
18
+ const TOOL_DISPLAY = {
19
+ read: "reading",
20
+ bash: "running command",
21
+ edit: "editing",
22
+ write: "writing",
23
+ grep: "searching",
24
+ find: "finding files",
25
+ ls: "listing",
26
+ };
27
+ // ---- Formatting helpers ----
28
+ /** Format a token count compactly: "33.8k token", "1.2M token". */
29
+ export function formatTokens(count) {
30
+ if (count >= 1_000_000)
31
+ return `${(count / 1_000_000).toFixed(1)}M token`;
32
+ if (count >= 1_000)
33
+ return `${(count / 1_000).toFixed(1)}k token`;
34
+ return `${count} token`;
35
+ }
36
+ /**
37
+ * Token count with optional context-fill % and compaction-count annotations.
38
+ * Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
39
+ * Compaction count rendered as `↻N` in dim.
40
+ *
41
+ * "12.3k token" — no annotations
42
+ * "12.3k token (45%)" — percent only
43
+ * "12.3k token (↻2)" — compactions only (e.g. right after compact)
44
+ * "12.3k token (45% · ↻2)" — both
45
+ */
46
+ export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
47
+ const tokenStr = formatTokens(tokens);
48
+ const annot = [];
49
+ if (percent !== null) {
50
+ const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
51
+ annot.push(theme.fg(color, `${Math.round(percent)}%`));
52
+ }
53
+ if (compactions > 0) {
54
+ annot.push(theme.fg("dim", `↻${compactions}`));
55
+ }
56
+ if (annot.length === 0)
57
+ return tokenStr;
58
+ return `${tokenStr} (${annot.join(" · ")})`;
59
+ }
60
+ /** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
61
+ export function formatTurns(turnCount, maxTurns) {
62
+ return maxTurns != null ? `⟳${turnCount}≤${maxTurns}` : `⟳${turnCount}`;
63
+ }
64
+ /** Format milliseconds as human-readable duration. */
65
+ export function formatMs(ms) {
66
+ return `${(ms / 1000).toFixed(1)}s`;
67
+ }
68
+ /** Format duration from start/completed timestamps. */
69
+ export function formatDuration(startedAt, completedAt) {
70
+ if (completedAt)
71
+ return formatMs(completedAt - startedAt);
72
+ return `${formatMs(Date.now() - startedAt)} (running)`;
73
+ }
74
+ /** Get display name for any agent type (built-in or custom). */
75
+ export function getDisplayName(type) {
76
+ return getConfig(type).displayName;
77
+ }
78
+ /** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
79
+ export function getPromptModeLabel(type) {
80
+ const config = getConfig(type);
81
+ return config.promptMode === "append" ? "twin" : undefined;
82
+ }
83
+ /** Mode label is not included — callers add it where they want it. */
84
+ export function buildInvocationTags(invocation) {
85
+ const tags = [];
86
+ if (!invocation)
87
+ return { tags };
88
+ if (invocation.thinking)
89
+ tags.push(`thinking: ${invocation.thinking}`);
90
+ if (invocation.isolated)
91
+ tags.push("isolated");
92
+ if (invocation.isolation === "worktree")
93
+ tags.push("worktree");
94
+ if (invocation.inheritContext)
95
+ tags.push("inherit context");
96
+ if (invocation.runInBackground)
97
+ tags.push("background");
98
+ if (invocation.maxTurns != null)
99
+ tags.push(`max turns: ${invocation.maxTurns}`);
100
+ return { modelName: invocation.modelName, tags };
101
+ }
102
+ /** Truncate text to a single line, max `len` chars. */
103
+ function truncateLine(text, len = 60) {
104
+ const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
105
+ if (line.length <= len)
106
+ return line;
107
+ return line.slice(0, len) + "…";
108
+ }
109
+ /** Build a human-readable activity string from currently-running tools or response text. */
110
+ export function describeActivity(activeTools, responseText) {
111
+ if (activeTools.size > 0) {
112
+ const groups = new Map();
113
+ for (const toolName of activeTools.values()) {
114
+ const action = TOOL_DISPLAY[toolName] ?? toolName;
115
+ groups.set(action, (groups.get(action) ?? 0) + 1);
116
+ }
117
+ const parts = [];
118
+ for (const [action, count] of groups) {
119
+ if (count > 1) {
120
+ parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
121
+ }
122
+ else {
123
+ parts.push(action);
124
+ }
125
+ }
126
+ return parts.join(", ") + "…";
127
+ }
128
+ // No tools active — show truncated response text if available
129
+ if (responseText && responseText.trim().length > 0) {
130
+ return truncateLine(responseText);
131
+ }
132
+ return "thinking…";
133
+ }
134
+ // ---- Widget manager ----
135
+ export class AgentWidget {
136
+ manager;
137
+ agentActivity;
138
+ uiCtx;
139
+ widgetFrame = 0;
140
+ widgetInterval;
141
+ /** Tracks how many turns each finished agent has survived. Key: agent ID, Value: turns since finished. */
142
+ finishedTurnAge = new Map();
143
+ /** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
144
+ static ERROR_LINGER_TURNS = 2;
145
+ /** Whether the widget callback is currently registered with the TUI. */
146
+ widgetRegistered = false;
147
+ /** Cached TUI reference from widget factory callback, used for requestRender(). */
148
+ tui;
149
+ /** Last status bar text, used to avoid redundant setStatus calls. */
150
+ lastStatusText;
151
+ constructor(manager, agentActivity) {
152
+ this.manager = manager;
153
+ this.agentActivity = agentActivity;
154
+ }
155
+ /** Set the UI context (grabbed from first tool execution). */
156
+ setUICtx(ctx) {
157
+ if (ctx !== this.uiCtx) {
158
+ // UICtx changed — the widget registered on the old context is gone.
159
+ // Force re-registration on next update().
160
+ this.uiCtx = ctx;
161
+ this.widgetRegistered = false;
162
+ this.tui = undefined;
163
+ this.lastStatusText = undefined;
164
+ }
165
+ }
166
+ /**
167
+ * Called on each new turn (tool_execution_start).
168
+ * Ages finished agents and clears those that have lingered long enough.
169
+ */
170
+ onTurnStart() {
171
+ // Age all finished agents
172
+ for (const [id, age] of this.finishedTurnAge) {
173
+ this.finishedTurnAge.set(id, age + 1);
174
+ }
175
+ // Trigger a widget refresh (will filter out expired agents)
176
+ this.update();
177
+ }
178
+ /** Ensure the widget update timer is running. */
179
+ ensureTimer() {
180
+ if (!this.widgetInterval) {
181
+ this.widgetInterval = setInterval(() => this.update(), 80);
182
+ }
183
+ }
184
+ /** Check if a finished agent should still be shown in the widget. */
185
+ shouldShowFinished(agentId, status) {
186
+ const age = this.finishedTurnAge.get(agentId) ?? 0;
187
+ const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
188
+ return age < maxAge;
189
+ }
190
+ /** Record an agent as finished (call when agent completes). */
191
+ markFinished(agentId) {
192
+ if (!this.finishedTurnAge.has(agentId)) {
193
+ this.finishedTurnAge.set(agentId, 0);
194
+ }
195
+ }
196
+ /** Render a finished agent line. */
197
+ renderFinishedLine(a, theme) {
198
+ const name = getDisplayName(a.type);
199
+ const modeLabel = getPromptModeLabel(a.type);
200
+ const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
201
+ let icon;
202
+ let statusText;
203
+ if (a.status === "completed") {
204
+ icon = theme.fg("success", "✓");
205
+ statusText = "";
206
+ }
207
+ else if (a.status === "steered") {
208
+ icon = theme.fg("warning", "✓");
209
+ statusText = theme.fg("warning", " (turn limit)");
210
+ }
211
+ else if (a.status === "stopped") {
212
+ icon = theme.fg("dim", "■");
213
+ statusText = theme.fg("dim", " stopped");
214
+ }
215
+ else if (a.status === "error") {
216
+ icon = theme.fg("error", "✗");
217
+ const errMsg = a.error ? `: ${a.error.slice(0, 60)}` : "";
218
+ statusText = theme.fg("error", ` error${errMsg}`);
219
+ }
220
+ else {
221
+ // aborted
222
+ icon = theme.fg("error", "✗");
223
+ statusText = theme.fg("warning", " aborted");
224
+ }
225
+ const parts = [];
226
+ const activity = this.agentActivity.get(a.id);
227
+ if (activity)
228
+ parts.push(formatTurns(activity.turnCount, activity.maxTurns));
229
+ if (a.toolUses > 0)
230
+ parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
231
+ parts.push(duration);
232
+ const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
233
+ return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
234
+ }
235
+ /**
236
+ * Render the widget content. Called from the registered widget's render() callback,
237
+ * reading live state each time instead of capturing it in a closure.
238
+ */
239
+ renderWidget(tui, theme) {
240
+ const allAgents = this.manager.listAgents();
241
+ const running = allAgents.filter(a => a.status === "running");
242
+ const queued = allAgents.filter(a => a.status === "queued");
243
+ const finished = allAgents.filter(a => a.status !== "running" && a.status !== "queued" && a.completedAt
244
+ && this.shouldShowFinished(a.id, a.status));
245
+ const hasActive = running.length > 0 || queued.length > 0;
246
+ const hasFinished = finished.length > 0;
247
+ // Nothing to show — return empty (widget will be unregistered by update())
248
+ if (!hasActive && !hasFinished)
249
+ return [];
250
+ const w = tui.terminal.columns;
251
+ const truncate = (line) => truncateToWidth(line, w);
252
+ const headingColor = hasActive ? "accent" : "dim";
253
+ const headingIcon = hasActive ? "●" : "○";
254
+ const frame = SPINNER[this.widgetFrame % SPINNER.length];
255
+ // Build sections separately for overflow-aware assembly.
256
+ // Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
257
+ const finishedLines = [];
258
+ for (const a of finished) {
259
+ finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
260
+ }
261
+ const runningLines = []; // each entry is [header, activity]
262
+ for (const a of running) {
263
+ const name = getDisplayName(a.type);
264
+ const modeLabel = getPromptModeLabel(a.type);
265
+ const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
266
+ const elapsed = formatMs(Date.now() - a.startedAt);
267
+ const bg = this.agentActivity.get(a.id);
268
+ const toolUses = bg?.toolUses ?? a.toolUses;
269
+ const tokens = getLifetimeTotal(bg?.lifetimeUsage);
270
+ const contextPercent = getSessionContextPercent(bg?.session);
271
+ const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
272
+ const parts = [];
273
+ if (bg)
274
+ parts.push(formatTurns(bg.turnCount, bg.maxTurns));
275
+ if (toolUses > 0)
276
+ parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
277
+ if (tokenText)
278
+ parts.push(tokenText);
279
+ parts.push(elapsed);
280
+ const statsText = parts.join(" · ");
281
+ const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
282
+ runningLines.push([
283
+ truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`),
284
+ truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
285
+ ]);
286
+ }
287
+ const queuedLine = queued.length > 0
288
+ ? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
289
+ : undefined;
290
+ // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
291
+ const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
292
+ const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
293
+ const lines = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
294
+ if (totalBody <= maxBody) {
295
+ // Everything fits — add all lines and fix up connectors for the last item.
296
+ lines.push(...finishedLines);
297
+ for (const pair of runningLines)
298
+ lines.push(...pair);
299
+ if (queuedLine)
300
+ lines.push(queuedLine);
301
+ // Fix last connector: swap ├─ → └─ and │ → space for activity lines.
302
+ if (lines.length > 1) {
303
+ const last = lines.length - 1;
304
+ lines[last] = lines[last].replace("├─", "└─");
305
+ // If last item is a running agent activity line, fix indent of that line
306
+ // and fix the header line above it.
307
+ if (runningLines.length > 0 && !queuedLine) {
308
+ // The last two lines are the last running agent's header + activity.
309
+ if (last >= 2) {
310
+ lines[last - 1] = lines[last - 1].replace("├─", "└─");
311
+ lines[last] = lines[last].replace("│ ", " ");
312
+ }
313
+ }
314
+ }
315
+ }
316
+ else {
317
+ // Overflow — prioritize: running > queued > finished.
318
+ // Reserve 1 line for overflow indicator.
319
+ let budget = maxBody - 1;
320
+ let hiddenRunning = 0;
321
+ let hiddenFinished = 0;
322
+ // 1. Running agents (2 lines each)
323
+ for (const pair of runningLines) {
324
+ if (budget >= 2) {
325
+ lines.push(...pair);
326
+ budget -= 2;
327
+ }
328
+ else {
329
+ hiddenRunning++;
330
+ }
331
+ }
332
+ // 2. Queued line
333
+ if (queuedLine && budget >= 1) {
334
+ lines.push(queuedLine);
335
+ budget--;
336
+ }
337
+ // 3. Finished agents
338
+ for (const fl of finishedLines) {
339
+ if (budget >= 1) {
340
+ lines.push(fl);
341
+ budget--;
342
+ }
343
+ else {
344
+ hiddenFinished++;
345
+ }
346
+ }
347
+ // Overflow summary
348
+ const overflowParts = [];
349
+ if (hiddenRunning > 0)
350
+ overflowParts.push(`${hiddenRunning} running`);
351
+ if (hiddenFinished > 0)
352
+ overflowParts.push(`${hiddenFinished} finished`);
353
+ const overflowText = overflowParts.join(", ");
354
+ lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`));
355
+ }
356
+ return lines;
357
+ }
358
+ /** Force an immediate widget update. */
359
+ update() {
360
+ if (!this.uiCtx)
361
+ return;
362
+ const allAgents = this.manager.listAgents();
363
+ // Lightweight existence checks — full categorization happens in renderWidget()
364
+ let runningCount = 0;
365
+ let queuedCount = 0;
366
+ let hasFinished = false;
367
+ for (const a of allAgents) {
368
+ if (a.status === "running") {
369
+ runningCount++;
370
+ }
371
+ else if (a.status === "queued") {
372
+ queuedCount++;
373
+ }
374
+ else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
375
+ hasFinished = true;
376
+ }
377
+ }
378
+ const hasActive = runningCount > 0 || queuedCount > 0;
379
+ // Nothing to show — clear widget
380
+ if (!hasActive && !hasFinished) {
381
+ if (this.widgetRegistered) {
382
+ this.uiCtx.setWidget("agents", undefined);
383
+ this.widgetRegistered = false;
384
+ this.tui = undefined;
385
+ }
386
+ if (this.lastStatusText !== undefined) {
387
+ this.uiCtx.setStatus("subagents", undefined);
388
+ this.lastStatusText = undefined;
389
+ }
390
+ if (this.widgetInterval) {
391
+ clearInterval(this.widgetInterval);
392
+ this.widgetInterval = undefined;
393
+ }
394
+ // Clean up stale entries
395
+ for (const [id] of this.finishedTurnAge) {
396
+ if (!allAgents.some(a => a.id === id))
397
+ this.finishedTurnAge.delete(id);
398
+ }
399
+ return;
400
+ }
401
+ // Status bar — only call setStatus when the text actually changes
402
+ let newStatusText;
403
+ if (hasActive) {
404
+ const statusParts = [];
405
+ if (runningCount > 0)
406
+ statusParts.push(`${runningCount} running`);
407
+ if (queuedCount > 0)
408
+ statusParts.push(`${queuedCount} queued`);
409
+ const total = runningCount + queuedCount;
410
+ newStatusText = `${statusParts.join(", ")} agent${total === 1 ? "" : "s"}`;
411
+ }
412
+ if (newStatusText !== this.lastStatusText) {
413
+ this.uiCtx.setStatus("subagents", newStatusText);
414
+ this.lastStatusText = newStatusText;
415
+ }
416
+ this.widgetFrame++;
417
+ // Register widget callback once; subsequent updates use requestRender()
418
+ // which re-invokes render() without replacing the component (avoids layout thrashing).
419
+ if (!this.widgetRegistered) {
420
+ this.uiCtx.setWidget("agents", (tui, theme) => {
421
+ this.tui = tui;
422
+ return {
423
+ render: () => this.renderWidget(tui, theme),
424
+ invalidate: () => {
425
+ // Theme changed — force re-registration so factory captures fresh theme.
426
+ this.widgetRegistered = false;
427
+ this.tui = undefined;
428
+ },
429
+ };
430
+ }, { placement: "aboveEditor" });
431
+ this.widgetRegistered = true;
432
+ }
433
+ else {
434
+ // Widget already registered — just request a re-render of existing components.
435
+ this.tui?.requestRender();
436
+ }
437
+ }
438
+ dispose() {
439
+ if (this.widgetInterval) {
440
+ clearInterval(this.widgetInterval);
441
+ this.widgetInterval = undefined;
442
+ }
443
+ if (this.uiCtx) {
444
+ this.uiCtx.setWidget("agents", undefined);
445
+ this.uiCtx.setStatus("subagents", undefined);
446
+ }
447
+ this.widgetRegistered = false;
448
+ this.tui = undefined;
449
+ this.lastStatusText = undefined;
450
+ }
451
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
3
+ *
4
+ * Displays a scrollable, live-updating view of an agent's conversation.
5
+ * Subscribes to session events for real-time streaming updates.
6
+ */
7
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
8
+ import { type Component, type TUI } from "@earendil-works/pi-tui";
9
+ import type { AgentRecord } from "../types.js";
10
+ import type { Theme } from "./agent-widget.js";
11
+ import { type AgentActivity } from "./agent-widget.js";
12
+ /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
13
+ export declare const VIEWPORT_HEIGHT_PCT = 70;
14
+ export declare class ConversationViewer implements Component {
15
+ private tui;
16
+ private session;
17
+ private record;
18
+ private activity;
19
+ private theme;
20
+ private done;
21
+ private scrollOffset;
22
+ private autoScroll;
23
+ private unsubscribe;
24
+ private lastInnerW;
25
+ private closed;
26
+ constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void);
27
+ handleInput(data: string): void;
28
+ render(width: number): string[];
29
+ invalidate(): void;
30
+ dispose(): void;
31
+ private viewportHeight;
32
+ private chromeLines;
33
+ private invocationLine;
34
+ private buildContentLines;
35
+ }