@giladbarnea/pi-user-agents 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INTERNALS.md CHANGED
@@ -16,9 +16,9 @@ How a finished run reaches (or hides from) the main agent rests on two Pi SDK ch
16
16
 
17
17
  - **`pi.appendEntry(customType, data)` + `pi.registerEntryRenderer`** — the only channel that renders in the transcript while staying out of LLM context *forever*. Entries are persisted in the session file and re-rendered on reload. The entry's `data` is optional in the type — renderers must guard it.
18
18
 
19
- This extension maps the two extremes onto the `-s` flag: the default posts the result card via `appendEntry` (main agent structurally oblivious), and `-s` posts a visible custom message with `{ triggerTurn: true }` (steered mid-turn or answered immediately). Both channels share one card renderer in `transcript.ts`.
19
+ Default completion writes no entry or message to the parent session. The widget and agent overlay show the response. `-s` sends a hidden custom message with `{ triggerTurn: true }`, steered mid-turn or answered immediately.
20
20
 
21
- Immediate `-s` delivery and late `s` squash delivery share one canonical message builder. Its `display` option prevents a second visible result card. Both paths use the same selected conversation and `{ triggerTurn: true }` delivery. For a run launched without `-s`, the context message stays hidden at squash time because the TUI-only result card is already visible.
21
+ Immediate `-s` delivery and late `s` squash delivery share one canonical message builder and use `display: false`. Both paths preserve the selected conversation and `{ triggerTurn: true }` delivery. `transcript.ts` renders old completion entries and messages as zero lines, preventing Pi's fallback renderer from showing them. Command errors and detach, attach, and rebase breadcrumbs remain visible.
22
22
 
23
23
  ## Squashed message format
24
24
 
@@ -46,7 +46,7 @@ A child compaction passes through deliberately and lands as a real `compaction`
46
46
 
47
47
  Two mechanics make that exact. First, a live compaction emits no message event — only `compaction_end` — so the child subscription captures it there as a synthetic `compactionSummary` message, or the append-only conversation would never contain it. Second, the kept-tail boundary (`firstKeptEntryId`) is a child-session entry id that means nothing in the main session's file; but at `compaction_end` the child's live context is exactly `[summary, kept tail]`, so the capture records `keptTailCount`, and the rebase replay counts back that many context messages to pass the main-session id of the message that starts the tail. The tail may reach past the replayed messages into main's pre-existing context — a child that inherits a nearly full context compacts early — so the delivery prepends main's own per-message entry ids to the count.
48
48
 
49
- The fast-forward precondition is fingerprint equality: at dispatch the agent stores `JSON.stringify` of the inherited context messages (`[]` for `-i`), and `r` is offered only while the main session's current `buildSessionContext().messages` fingerprint still matches. The live fingerprint is cached per session manager and recomputed only when the branch leaf moves, so TUI-only entries (result cards, detach lines) move the leaf, trigger one recompute, and correctly do not block the rebase. One rule covers isolated agents too: their base is empty, so they rebase only onto a still-empty session.
49
+ The fast-forward precondition is fingerprint equality: at dispatch the agent stores `JSON.stringify` of the inherited context messages (`[]` for `-i`), and `r` is offered only while the main session's current `buildSessionContext().messages` fingerprint still matches. The live fingerprint is cached per session manager and recomputed only when the branch leaf moves, so TUI-only entries (detach and attach lines) move the leaf, trigger one recompute, and correctly do not block the rebase. One rule covers isolated agents too: their base is empty, so they rebase only onto a still-empty session.
50
50
 
51
51
  Because the session switch tears the extension down, live widget rows cannot survive it: `r` is withheld while any agent is mid-turn, and the remaining parked and completed agents are detached first through the ordinary detach paths, so every one of them leaves its `Detached session …` breadcrumb before the switch. The switch re-opens the same file, so the session id does not change.
52
52
 
package/README.md CHANGED
@@ -81,7 +81,7 @@ Press `←` or `↓` from the editor to focus the widget, pick an agent, press `
81
81
 
82
82
  A background agent is not fire-and-forget — it's a session you can talk to.
83
83
 
84
- Press `Enter` in the overlay to open the steer composer. Mid-turn, your message queues in after the current tool batch, before the next model call — exactly like steering the main agent. After the turn completes, the same composer starts **another turn** on the same live session. Follow up as many times as you need; every completed turn posts its own result card.
84
+ Press `Enter` in the overlay to open the steer composer. Mid-turn, your message queues in after the current tool batch, before the next model call — exactly like steering the main agent. After the turn completes, the same composer starts **another turn** on the same live session. Follow up as many times as you need. Read each response in the agent overlay.
85
85
 
86
86
  `Ctrl+x` interrupts only the current turn — the agent goes idle and stays available for steering, squashing, or detaching. In fact, nothing ends an agent except you: pressing `d` twice, squashing or rebasing it, or ending the Pi session.
87
87
 
@@ -101,14 +101,14 @@ Or take it back as a background agent instead: `/agent-attach 0199c4f2` puts the
101
101
 
102
102
  This is what makes user agents different from subagents: **by default, the main agent never learns any of this happened.**
103
103
 
104
- Each completed turn posts a result card into your transcript — metadata, a collapsed preview, expandable to the full Markdown response. The card is TUI-only: you read it, expand it, copy it, and the main agent's context is untouched. Ask an agent ten questions and your main agent's token budget doesn't move.
104
+ Completed responses stay in the agent overlay, accessible through the widget below the editor. Completion adds no transcript card. Ask an agent ten questions and your main agent's token budget does not move.
105
105
 
106
106
  When a result does belong in the main conversation, squash it in:
107
107
 
108
108
  - Dispatch with `-s`/`--squash` and the result is delivered automatically on completion.
109
109
  - Or press `s` in the overlay of any completed agent, whenever you decide it earned its place.
110
110
 
111
- A squash delivers a compact record, not a transcript dump: every message you sent the agent and the final answer to each — no thinking, no tool traffic. The record is rebuilt from the agent's full history at squash time, so early turns survive even after the agent compacts its own context. If the main agent is mid-turn, the record is steered in; if idle, it triggers a turn. A squashed agent retires; its overlay stays readable.
111
+ A squash delivers a compact record, not a transcript dump: every message you sent the agent and the final answer to each — no thinking, no tool traffic. The record is rebuilt from the agent's full history at squash time, so early turns survive even after the agent compacts its own context. If the main agent is mid-turn, the record is steered in; if idle, it triggers a turn. Squash adds no transcript card. A squashed agent retires; its overlay stays readable.
112
112
 
113
113
  ### 6. Or rebase — rewrite history
114
114
 
@@ -148,7 +148,7 @@ And what you do type by hand is checked live, as you type. Valid options and val
148
148
 
149
149
  Every dispatch writes a real session file, exactly like the one you're sitting in, so dispatched agents show up in `/resume` and `pi -r` alongside your own sessions.
150
150
 
151
- Result cards persist across session reloads; the widget doesn't. After a reload you keep every card, and every agent's session is still on disk — but the steerable rows are gone. Reach them with `/resume`.
151
+ The widget does not survive a session reload. Each agent's session remains on disk, accessible through `/resume` or `/agent-attach`. Completion cards from older versions stay hidden.
152
152
 
153
153
  ### 🌡️ A context meter in one cell
154
154
 
package/agent-viewer.ts CHANGED
@@ -102,7 +102,14 @@ function describeAssistantPart(part: AssistantContentPart): ActivityDescription
102
102
 
103
103
  export function renderActivity(description: ActivityDescription, width: number, theme: Theme): string {
104
104
  if (width <= 0) return "";
105
- const normalized = description.text.replace(/\s+/g, " ").trim();
105
+ const limit = Math.min(4096, Math.max(128, width * 4));
106
+ const text = description.text;
107
+ const bounded = text.length <= limit
108
+ ? text
109
+ : description.truncation === "tail"
110
+ ? `${text.slice(0, limit - 1)}…`
111
+ : `${text.slice(0, Math.ceil((limit - 1) / 2))}…${text.slice(-Math.floor((limit - 1) / 2))}`;
112
+ const normalized = bounded.replace(/\s+/g, " ").trim();
106
113
  const dim = (text: string) => theme.fg("dim", text);
107
114
  const markdown = new Markdown(normalized, 0, 0, dimMarkdownThemeFromTheme(theme), { color: dim });
108
115
  const rendered = markdown
@@ -158,7 +165,7 @@ export class AgentViewer implements Component {
158
165
  this.tui.requestRender(),
159
166
  );
160
167
  private composer: Input | undefined;
161
- /** Rich tool rendering is far too costly to repeat on every 100 ms widget tick. */
168
+ /** Rich tool rendering is far too costly to repeat on every widget tick. */
162
169
  private renderedContent: { key: string; lines: string[] } | undefined;
163
170
 
164
171
  constructor(
@@ -364,7 +371,9 @@ export class AgentViewer implements Component {
364
371
  return lines;
365
372
  }
366
373
 
367
- invalidate(): void {}
374
+ invalidate(): void {
375
+ this.renderedContent = undefined;
376
+ }
368
377
 
369
378
  dispose(): void {}
370
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giladbarnea/pi-user-agents",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Run, view, and control background Pi agents.",
5
5
  "keywords": [
6
6
  "pi-package",
package/runner.ts CHANGED
@@ -192,14 +192,9 @@ function postUserAgentResult(
192
192
  notifyMainAgent: boolean,
193
193
  message: AgentResultMessage,
194
194
  ): boolean {
195
- if (isShuttingDown()) return false;
196
- if (notifyMainAgent) {
197
- // Steers into a streaming turn, or triggers an immediate response when idle.
198
- pi.sendMessage(message, { triggerTurn: true });
199
- } else {
200
- // TUI-only session entry: renders in the transcript but never enters the main agent's context.
201
- pi.appendEntry(message.customType, { content: message.content, details: message.details });
202
- }
195
+ if (isShuttingDown() || !notifyMainAgent) return false;
196
+ // Steers into a streaming turn, or triggers an immediate response when idle.
197
+ pi.sendMessage(message, { triggerTurn: true });
203
198
  return true;
204
199
  }
205
200
 
@@ -490,7 +485,7 @@ async function runAgentLifecycle(
490
485
  }
491
486
  }
492
487
 
493
- /** Settle a failed lifecycle: record the error and post the error card, unless the user aborted. */
488
+ /** Record a failed lifecycle in the widget and deliver it only when squash was requested. */
494
489
  export function reportAgentFailure(
495
490
  pi: ExtensionAPI,
496
491
  isShuttingDown: () => boolean,
@@ -514,7 +509,7 @@ export function reportAgentFailure(
514
509
  const resultMessage = buildAgentResultMessage(
515
510
  runningAgent,
516
511
  { ok: false, error: message },
517
- { display: runningAgent.notifyMainAgent },
512
+ { display: false },
518
513
  );
519
514
  widget.addCompleted(runningAgent, resultMessage, { squashable: !runningAgent.notifyMainAgent });
520
515
  if (postUserAgentResult(pi, isShuttingDown, runningAgent.notifyMainAgent, resultMessage))
@@ -556,7 +551,6 @@ export async function runChildTurns(
556
551
  { ok: true, response },
557
552
  { display: false },
558
553
  );
559
- postUserAgentResult(pi, isShuttingDown, false, resultMessage);
560
554
  runningAgent.pendingSquashMessage = runningAgent.notifyMainAgent
561
555
  ? undefined
562
556
  : resultMessage;
@@ -581,7 +575,7 @@ export async function runChildTurns(
581
575
  const resultMessage = buildAgentResultMessage(
582
576
  runningAgent,
583
577
  { ok: true, response },
584
- { display: runningAgent.notifyMainAgent },
578
+ { display: false },
585
579
  );
586
580
  if (runningAgent.notifyMainAgent) {
587
581
  if (isShuttingDown()) {
@@ -594,7 +588,6 @@ export async function runChildTurns(
594
588
  runningAgent.status = posted ? "posted" : runningAgent.status;
595
589
  return;
596
590
  }
597
- postUserAgentResult(pi, isShuttingDown, false, resultMessage);
598
591
  runningAgent.pendingSquashMessage = resultMessage;
599
592
  if (isShuttingDown()) return;
600
593
  runningAgent.status = "idle";
package/shared.ts CHANGED
@@ -98,8 +98,6 @@ export type AgentCommandDetails = {
98
98
  toolUses?: number;
99
99
  turnCount?: number;
100
100
  responseText?: string;
101
- /** Retained for completed entries written before responseText was persisted. */
102
- responsePreview?: string;
103
101
  error?: string;
104
102
  };
105
103
 
@@ -294,14 +292,6 @@ export function truncatePlain(text: string, length: number): string {
294
292
  return `${line.slice(0, Math.max(0, length - 1))}…`;
295
293
  }
296
294
 
297
- export function extractTag(
298
- content: string,
299
- tag: "task" | "response" | "error" | "duration_ms",
300
- ): string | undefined {
301
- const match = content.match(new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?</${tag}>`));
302
- return match?.[1]?.trim();
303
- }
304
-
305
295
  export function escapeAttribute(value: string): string {
306
296
  return value
307
297
  .replace(/&/g, "&amp;")
package/transcript.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, Container, Markdown, Text } from "@earendil-works/pi-tui";
1
+ import { type Component, Text } from "@earendil-works/pi-tui";
3
2
  import type {
4
3
  AgentCommandDetails,
5
4
  AgentCommandName,
@@ -12,21 +11,15 @@ import type {
12
11
  ExtensionCommandContext,
13
12
  RebasedEntryData,
14
13
  RunningAgent,
15
- Theme,
16
14
  } from "./shared.js";
17
15
  import {
18
16
  ATTACHED_ENTRY_TYPE,
19
17
  contextLabel,
20
18
  DETACHED_ENTRY_TYPE,
21
19
  escapeAttribute,
22
- extractTag,
23
- formatMs,
24
- formatToolUses,
25
- formatTurns,
26
20
  mainContextLabel,
27
21
  MESSAGE_TYPE,
28
22
  REBASED_ENTRY_TYPE,
29
- truncatePlain,
30
23
  } from "./shared.js";
31
24
 
32
25
  export function reportCommandError(
@@ -195,28 +188,14 @@ export function formatStartNotification(agent: RunningAgent): string {
195
188
  }
196
189
 
197
190
  export function registerUserAgentRenderer(pi: ExtensionAPI): void {
198
- pi.registerMessageRenderer<AgentCommandDetails>(
199
- MESSAGE_TYPE,
200
- (message, { expanded }, theme) =>
201
- liveAgentCard(
202
- () => customMessageContentText(message.content),
203
- () => message.details,
204
- expanded,
205
- theme,
206
- ),
207
- );
208
- pi.registerEntryRenderer<AgentEntryData>(
209
- MESSAGE_TYPE,
210
- (entry, { expanded }, theme) =>
211
- entry.data
212
- ? liveAgentCard(
213
- () => entry.data!.content,
214
- () => entry.data!.details,
215
- expanded,
216
- theme,
217
- )
218
- : undefined,
219
- );
191
+ const hiddenResult: Component = { render: () => [], invalidate: () => undefined };
192
+ pi.registerMessageRenderer<AgentCommandDetails>(MESSAGE_TYPE, (message, _state, theme) => {
193
+ const details = message.details;
194
+ if (!details || details.agentId || details.inheritedContext !== undefined || details.ok)
195
+ return hiddenResult;
196
+ return new Text(theme.fg("error", `/${details.command}: ${details.error}`), 1, 0);
197
+ });
198
+ pi.registerEntryRenderer<AgentEntryData>(MESSAGE_TYPE, () => hiddenResult);
220
199
  // Pi's chat container does not pad renderer components; its own chat lines self-pad with paddingX 1.
221
200
  pi.registerEntryRenderer<DetachedEntryData>(DETACHED_ENTRY_TYPE, (entry, _state, theme) =>
222
201
  entry.data
@@ -255,90 +234,3 @@ function formatTokenCount(tokens: number): string {
255
234
  return tokens < 1000 ? `${tokens}` : `${Math.round(tokens / 1000)}K`;
256
235
  }
257
236
 
258
- function liveAgentCard(
259
- content: () => string,
260
- details: () => AgentCommandDetails | undefined,
261
- expanded: boolean,
262
- theme: Theme,
263
- ): Component {
264
- const body = renderAgentCardBody(content(), details(), expanded, theme);
265
- return {
266
- render: (width) => renderAgentCard(content(), details(), body, theme).render(width),
267
- invalidate: () => body.invalidate(),
268
- };
269
- }
270
-
271
- function renderAgentCard(
272
- content: string,
273
- details: AgentCommandDetails | undefined,
274
- body: Component,
275
- theme: Theme,
276
- ): Container {
277
- const ok = details?.ok ?? !content.includes("<user_agent_error");
278
- const command = details ? `/${details.command}` : "/agent";
279
- const icon = ok ? theme.fg("success", "✓") : theme.fg("error", "✗");
280
- const statusText = ok ? theme.fg("dim", "completed") : theme.fg("error", "failed");
281
- const parts = buildRendererParts(content, details);
282
- const task = details?.task ?? extractTag(content, "task");
283
-
284
- let text = `${icon} ${theme.bold(command)} ${statusText}`;
285
- if (parts.length > 0) text += ` ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}`;
286
- if (task) text += `\n ${theme.fg("dim", `task: ${truncatePlain(task, 88)}`)}`;
287
- const card = new Container();
288
- card.addChild(new Text(text, 1, 0));
289
- card.addChild(body);
290
- return card;
291
- }
292
-
293
- function renderAgentCardBody(
294
- content: string,
295
- details: AgentCommandDetails | undefined,
296
- expanded: boolean,
297
- theme: Theme,
298
- ): Component {
299
- const ok = details?.ok ?? !content.includes("<user_agent_error");
300
- const output =
301
- details?.responseText ??
302
- details?.error ??
303
- extractTag(content, ok ? "response" : "error") ??
304
- "No output.";
305
- if (expanded)
306
- return ok
307
- ? new Markdown(output, 3, 0, getMarkdownTheme())
308
- : new Text(theme.fg("error", output), 3, 0);
309
-
310
- const previewText = `⎿ ${truncatePlain(details?.responsePreview ?? output, 110)}`;
311
- if (!ok) return new Text(` ${theme.fg("error", previewText)}`, 1, 0);
312
- return new Markdown(previewText, 3, 0, getMarkdownTheme(), {
313
- color: (value) => theme.fg("dim", value),
314
- });
315
- }
316
-
317
- function customMessageContentText(content: unknown): string {
318
- if (typeof content === "string") return content;
319
- if (!Array.isArray(content)) return String(content ?? "");
320
- return content
321
- .filter(
322
- (part): part is { type: "text"; text: string } =>
323
- part?.type === "text" && typeof part.text === "string",
324
- )
325
- .map((part) => part.text)
326
- .join("\n");
327
- }
328
-
329
- function buildRendererParts(content: string, details: AgentCommandDetails | undefined): string[] {
330
- const parts: string[] = [];
331
- if (details?.modelLabel) parts.push(details.modelLabel);
332
- else if (details?.model) parts.push(details.model);
333
- if (details?.inheritedContext !== undefined) parts.push(contextLabel(details.inheritedContext));
334
- const mainContext = mainContextLabel(details?.mainContextState);
335
- if (mainContext) parts.push(mainContext);
336
- if (details?.turnCount) parts.push(formatTurns(details.turnCount));
337
- if (details?.toolUses) parts.push(formatToolUses(details.toolUses));
338
- if (details?.durationMs !== undefined) parts.push(formatMs(details.durationMs));
339
- else {
340
- const duration = extractTag(content, "duration_ms");
341
- if (duration) parts.push(formatMs(Number(duration)));
342
- }
343
- return parts;
344
- }
package/widget.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import { renderAgentContextMeter } from "./context-meter.js";
18
18
  import { selectRebaseMessages } from "./rebase.js";
19
19
  import type {
20
+ AgentMessage,
20
21
  AgentResultMessage,
21
22
  CompletedAgent,
22
23
  RebaseDelivery,
@@ -42,6 +43,15 @@ type UserAgentWidgetEntry =
42
43
  | { kind: "running"; startedAt: number; agent: RunningAgent }
43
44
  | { kind: "completed"; startedAt: number; agent: CompletedAgent };
44
45
 
46
+ type CachedActivityPreview = {
47
+ source: AgentMessage | string | undefined;
48
+ status: RunningAgent["status"] | boolean;
49
+ error: string | undefined;
50
+ width: number;
51
+ theme: Theme;
52
+ text: string;
53
+ };
54
+
45
55
  export class UserAgentWidget {
46
56
  private ui: UIContext | undefined;
47
57
  private frame = 0;
@@ -52,6 +62,7 @@ export class UserAgentWidget {
52
62
  private active = false;
53
63
  private selectedIndex = 0;
54
64
  private viewerOpen = false;
65
+ private activityPreviews = new WeakMap<ViewableAgent, CachedActivityPreview>();
55
66
  /** One confirmation slot for detaching the selected agent. */
56
67
  private readonly detachConfirmation = new TimedConfirmation<ViewableAgent>(() => this.update());
57
68
  private readonly completedAgents: CompletedAgent[] = [];
@@ -67,6 +78,7 @@ export class UserAgentWidget {
67
78
  if (ui === this.ui) return;
68
79
  this.inputUnsub?.();
69
80
  this.ui = ui;
81
+ this.activityPreviews = new WeakMap();
70
82
  this.widgetRegistered = false;
71
83
  this.tui = undefined;
72
84
  this.detachConfirmation.cancel();
@@ -101,12 +113,18 @@ export class UserAgentWidget {
101
113
  }
102
114
 
103
115
  ensureTimer(): void {
104
- if (!this.ui || this.interval) return;
105
- this.interval = setInterval(() => this.update(), 100);
116
+ const active = [...this.runningAgents].some(isLiveAgent);
117
+ if (!active && this.interval) {
118
+ clearInterval(this.interval);
119
+ this.interval = undefined;
120
+ }
121
+ if (!this.ui || !active || this.interval) return;
122
+ this.interval = setInterval(() => this.update(), 200);
106
123
  }
107
124
 
108
125
  update(): void {
109
126
  if (!this.ui) return;
127
+ this.ensureTimer();
110
128
  const entries = this.entries();
111
129
  if (entries.length === 0) {
112
130
  this.clear();
@@ -123,8 +141,7 @@ export class UserAgentWidget {
123
141
  return {
124
142
  render: (width: number) => this.renderWidget(width, theme),
125
143
  invalidate: () => {
126
- this.widgetRegistered = false;
127
- this.tui = undefined;
144
+ this.activityPreviews = new WeakMap();
128
145
  },
129
146
  };
130
147
  },
@@ -148,6 +165,7 @@ export class UserAgentWidget {
148
165
  this.widgetRegistered = false;
149
166
  this.tui = undefined;
150
167
  this.active = false;
168
+ this.activityPreviews = new WeakMap();
151
169
  this.detachConfirmation.cancel();
152
170
  }
153
171
 
@@ -583,16 +601,30 @@ export class UserAgentWidget {
583
601
  .join(" ");
584
602
  const activityPrefix = `${indent} ⎿ `;
585
603
  const activityWidth = Math.max(0, width - visibleWidth(activityPrefix));
604
+ const preview = this.renderActivityPreview(agent, activityWidth, theme);
605
+ const activityText = ok ? preview : theme.fg("error", preview);
606
+ const activity = `${theme.fg("dim", indent)} ${theme.fg("dim", "⎿ ")}${activityText}`;
607
+ return [truncateToWidth(header, width), truncateToWidth(activity, width)];
608
+ }
609
+
610
+ private renderActivityPreview(agent: ViewableAgent, width: number, theme: Theme): string {
611
+ const running = isRunningAgent(agent);
612
+ const source = isLiveAgent(agent) ? agent.latestFinalizedMessage : agent.responseText;
613
+ const status = running ? agent.status : agent.ok;
614
+ const cached = this.activityPreviews.get(agent);
615
+ if (
616
+ cached && cached.source === source && cached.status === status &&
617
+ cached.error === agent.error && cached.width === width && cached.theme === theme
618
+ ) return cached.text;
586
619
  const description = running
587
620
  ? describeActivity(agent)
588
621
  : {
589
622
  text: agent.ok ? agent.responseText : `Error: ${agent.error ?? "unknown"}`,
590
623
  truncation: "tail" as const,
591
624
  };
592
- const preview = renderActivity(description, activityWidth, theme);
593
- const activityText = ok ? preview : theme.fg("error", preview);
594
- const activity = `${theme.fg("dim", indent)} ${theme.fg("dim", "⎿ ")}${activityText}`;
595
- return [truncateToWidth(header, width), truncateToWidth(activity, width)];
625
+ const text = renderActivity(description, width, theme);
626
+ this.activityPreviews.set(agent, { source, status, error: agent.error, width, theme, text });
627
+ return text;
596
628
  }
597
629
  }
598
630