@giladbarnea/pi-user-agents 0.0.6 → 0.0.8
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 +3 -3
- package/README.md +10 -9
- package/agent-viewer.ts +36 -9
- package/package.json +1 -1
- package/runner.ts +6 -13
- package/shared.ts +0 -10
- package/transcript.ts +9 -117
- package/widget.ts +193 -43
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
|
-
|
|
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
|
|
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 (
|
|
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
|
@@ -69,7 +69,7 @@ Every agent gets a row in a widget under the editor: status, task, model, turn c
|
|
|
69
69
|
|
|
70
70
|
<br>
|
|
71
71
|
|
|
72
|
-
Press `←` or `↓` from the editor to focus the widget
|
|
72
|
+
Press `←` or `↓` from the editor to focus the widget. The selected row shows a short session ID and offers the same agent actions as the overlay. Press `Enter` to open the full conversation, streaming live and following the tail as it grows. Tool calls render as proper views — `read`, `edit`, `write`, `grep`, `find`, `ls`, and `bash` each get a dedicated format, everything else a readable generic one.
|
|
73
73
|
|
|
74
74
|
<a href="screenshots/overlay.png">
|
|
75
75
|
<img src="screenshots/overlay.png" width="100%" alt="The agent overlay: a rendered bash call, its output, a todo update, and the final response, over the dimmed main conversation">
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -173,15 +173,16 @@ Everything goes through one command: `/agent [options] <task>`. Its one companio
|
|
|
173
173
|
| Widget / overlay | `d` `d` | Detach the agent, keeping its session (twice to confirm) |
|
|
174
174
|
| Widget / overlay | `Esc` | Back |
|
|
175
175
|
| Overlay | `Enter` | Steer mid-turn, or start another turn when idle |
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
176
|
+
| Widget / overlay | `s` | Squash the conversation into the main context |
|
|
177
|
+
| Widget / overlay | `r` | Rebase the raw conversation into the main context (fast-forward only) |
|
|
178
|
+
| Widget / overlay | `c` | Copy the latest response |
|
|
179
|
+
| Widget / overlay | `i` | Copy the agent's full session ID |
|
|
179
180
|
| Overlay | scroll · `End` | Pause tail-following · resume it |
|
|
180
181
|
|
|
181
182
|
## Good to know
|
|
182
183
|
|
|
183
184
|
- Some accepted `pi` options have no effect on a background run (the session, approval, offline, and API-key families). They parse; they just don't do anything yet.
|
|
184
|
-
- `c`
|
|
185
|
+
- `c` and `i` copy via `pbcopy`, so they are macOS-only for now.
|
|
185
186
|
- The overlay caps very large tool outputs and omits thinking entries.
|
|
186
187
|
|
|
187
188
|
## Roadmap
|
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
|
|
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
|
|
@@ -144,13 +151,17 @@ function truncateMiddle(
|
|
|
144
151
|
const VIEWER_CHROME_LINES = 6;
|
|
145
152
|
const VIEWER_MIN_VIEWPORT = 3;
|
|
146
153
|
const VIEWER_HEIGHT_PCT = 85;
|
|
154
|
+
const COPY_FEEDBACK_MS = 1500;
|
|
155
|
+
|
|
156
|
+
type CopyTarget = "response" | "session-id";
|
|
147
157
|
|
|
148
158
|
/** Focus-capturing overlay that shows an agent's live or finished response. */
|
|
149
159
|
export class AgentViewer implements Component {
|
|
150
160
|
private scrollOffset = 0;
|
|
151
161
|
private autoScroll = true;
|
|
152
162
|
private lastWidth = 0;
|
|
153
|
-
private
|
|
163
|
+
private copied: CopyTarget | undefined;
|
|
164
|
+
private copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
154
165
|
private rebaseWarning: string | undefined;
|
|
155
166
|
private rebaseWarningTimer: ReturnType<typeof setTimeout> | undefined;
|
|
156
167
|
/** One confirmation slot for both destructive overlay actions. */
|
|
@@ -158,7 +169,7 @@ export class AgentViewer implements Component {
|
|
|
158
169
|
this.tui.requestRender(),
|
|
159
170
|
);
|
|
160
171
|
private composer: Input | undefined;
|
|
161
|
-
/** Rich tool rendering is far too costly to repeat on every
|
|
172
|
+
/** Rich tool rendering is far too costly to repeat on every widget tick. */
|
|
162
173
|
private renderedContent: { key: string; lines: string[] } | undefined;
|
|
163
174
|
|
|
164
175
|
constructor(
|
|
@@ -205,6 +216,10 @@ export class AgentViewer implements Component {
|
|
|
205
216
|
this.copyResponse();
|
|
206
217
|
return;
|
|
207
218
|
}
|
|
219
|
+
if (matchesKey(data, "i")) {
|
|
220
|
+
this.copyToClipboard(this.agent.sessionId, "session-id");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
208
223
|
if (matchesKey(data, "s") && this.canSquashMainContext()) {
|
|
209
224
|
this.squashMainContext();
|
|
210
225
|
this.tui.requestRender();
|
|
@@ -344,7 +359,11 @@ export class AgentViewer implements Component {
|
|
|
344
359
|
th.fg("dim", `${content.length} lines`),
|
|
345
360
|
th.fg("dim", scrollPct),
|
|
346
361
|
this.canSteer() ? th.fg("dim", "Enter steer") : "",
|
|
347
|
-
this.
|
|
362
|
+
this.copied === "response"
|
|
363
|
+
? th.fg("success", "✓ copied")
|
|
364
|
+
: this.copied === "session-id"
|
|
365
|
+
? th.fg("success", "✓ copied ID")
|
|
366
|
+
: th.fg("dim", "c copy · i ID"),
|
|
348
367
|
th.fg("dim", "↑↓ scroll"),
|
|
349
368
|
th.fg("dim", "PgUp/PgDn"),
|
|
350
369
|
this.canSquashMainContext() ? th.fg("dim", "s squash") : "",
|
|
@@ -364,7 +383,9 @@ export class AgentViewer implements Component {
|
|
|
364
383
|
return lines;
|
|
365
384
|
}
|
|
366
385
|
|
|
367
|
-
invalidate(): void {
|
|
386
|
+
invalidate(): void {
|
|
387
|
+
this.renderedContent = undefined;
|
|
388
|
+
}
|
|
368
389
|
|
|
369
390
|
dispose(): void {}
|
|
370
391
|
|
|
@@ -392,14 +413,20 @@ export class AgentViewer implements Component {
|
|
|
392
413
|
isRunningAgent(this.agent) || this.agent.ok
|
|
393
414
|
? this.agent.responseText
|
|
394
415
|
: (this.agent.error ?? "");
|
|
416
|
+
this.copyToClipboard(text, "response");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private copyToClipboard(text: string, target: CopyTarget): void {
|
|
395
420
|
const child = childProcess.execFile("pbcopy", (error) => {
|
|
396
421
|
if (error) return;
|
|
397
|
-
this.
|
|
422
|
+
this.copied = target;
|
|
423
|
+
if (this.copiedTimer) clearTimeout(this.copiedTimer);
|
|
398
424
|
this.tui.requestRender();
|
|
399
|
-
setTimeout(() => {
|
|
400
|
-
this.
|
|
425
|
+
this.copiedTimer = setTimeout(() => {
|
|
426
|
+
this.copied = undefined;
|
|
427
|
+
this.copiedTimer = undefined;
|
|
401
428
|
this.tui.requestRender();
|
|
402
|
-
},
|
|
429
|
+
}, COPY_FEEDBACK_MS);
|
|
403
430
|
});
|
|
404
431
|
child.stdin?.end(text);
|
|
405
432
|
}
|
package/package.json
CHANGED
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
|
-
|
|
197
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
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, "&")
|
package/transcript.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
isKeyRelease,
|
|
3
4
|
matchesKey,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
import { renderAgentContextMeter } from "./context-meter.js";
|
|
18
19
|
import { selectRebaseMessages } from "./rebase.js";
|
|
19
20
|
import type {
|
|
21
|
+
AgentMessage,
|
|
20
22
|
AgentResultMessage,
|
|
21
23
|
CompletedAgent,
|
|
22
24
|
RebaseDelivery,
|
|
@@ -25,6 +27,7 @@ import type {
|
|
|
25
27
|
UIContext,
|
|
26
28
|
} from "./shared.js";
|
|
27
29
|
import {
|
|
30
|
+
CONFIRMATION_WINDOW_MS,
|
|
28
31
|
contextLabel,
|
|
29
32
|
formatMs,
|
|
30
33
|
TimedConfirmation,
|
|
@@ -42,6 +45,19 @@ type UserAgentWidgetEntry =
|
|
|
42
45
|
| { kind: "running"; startedAt: number; agent: RunningAgent }
|
|
43
46
|
| { kind: "completed"; startedAt: number; agent: CompletedAgent };
|
|
44
47
|
|
|
48
|
+
type CachedActivityPreview = {
|
|
49
|
+
source: AgentMessage | string | undefined;
|
|
50
|
+
status: RunningAgent["status"] | boolean;
|
|
51
|
+
error: string | undefined;
|
|
52
|
+
width: number;
|
|
53
|
+
theme: Theme;
|
|
54
|
+
text: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type CopyTarget = "response" | "session-id";
|
|
58
|
+
|
|
59
|
+
const COPY_FEEDBACK_MS = 1500;
|
|
60
|
+
|
|
45
61
|
export class UserAgentWidget {
|
|
46
62
|
private ui: UIContext | undefined;
|
|
47
63
|
private frame = 0;
|
|
@@ -52,8 +68,13 @@ export class UserAgentWidget {
|
|
|
52
68
|
private active = false;
|
|
53
69
|
private selectedIndex = 0;
|
|
54
70
|
private viewerOpen = false;
|
|
55
|
-
|
|
56
|
-
private
|
|
71
|
+
private copied: { agentId: string; target: CopyTarget } | undefined;
|
|
72
|
+
private copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
73
|
+
private rebaseWarning: { agentId: string; text: string } | undefined;
|
|
74
|
+
private rebaseWarningTimer: ReturnType<typeof setTimeout> | undefined;
|
|
75
|
+
private activityPreviews = new WeakMap<ViewableAgent, CachedActivityPreview>();
|
|
76
|
+
/** One confirmation slot for both destructive selected-row actions. */
|
|
77
|
+
private readonly confirmation = new TimedConfirmation<string>(() => this.update());
|
|
57
78
|
private readonly completedAgents: CompletedAgent[] = [];
|
|
58
79
|
|
|
59
80
|
constructor(
|
|
@@ -67,9 +88,10 @@ export class UserAgentWidget {
|
|
|
67
88
|
if (ui === this.ui) return;
|
|
68
89
|
this.inputUnsub?.();
|
|
69
90
|
this.ui = ui;
|
|
91
|
+
this.activityPreviews = new WeakMap();
|
|
70
92
|
this.widgetRegistered = false;
|
|
71
93
|
this.tui = undefined;
|
|
72
|
-
this.
|
|
94
|
+
this.confirmation.cancel();
|
|
73
95
|
this.inputUnsub = ui.onTerminalInput((data) => this.handleKey(data));
|
|
74
96
|
}
|
|
75
97
|
|
|
@@ -101,12 +123,18 @@ export class UserAgentWidget {
|
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
ensureTimer(): void {
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
const active = [...this.runningAgents].some(isLiveAgent);
|
|
127
|
+
if (!active && this.interval) {
|
|
128
|
+
clearInterval(this.interval);
|
|
129
|
+
this.interval = undefined;
|
|
130
|
+
}
|
|
131
|
+
if (!this.ui || !active || this.interval) return;
|
|
132
|
+
this.interval = setInterval(() => this.update(), 200);
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
update(): void {
|
|
109
136
|
if (!this.ui) return;
|
|
137
|
+
this.ensureTimer();
|
|
110
138
|
const entries = this.entries();
|
|
111
139
|
if (entries.length === 0) {
|
|
112
140
|
this.clear();
|
|
@@ -123,8 +151,7 @@ export class UserAgentWidget {
|
|
|
123
151
|
return {
|
|
124
152
|
render: (width: number) => this.renderWidget(width, theme),
|
|
125
153
|
invalidate: () => {
|
|
126
|
-
this.
|
|
127
|
-
this.tui = undefined;
|
|
154
|
+
this.activityPreviews = new WeakMap();
|
|
128
155
|
},
|
|
129
156
|
};
|
|
130
157
|
},
|
|
@@ -148,7 +175,8 @@ export class UserAgentWidget {
|
|
|
148
175
|
this.widgetRegistered = false;
|
|
149
176
|
this.tui = undefined;
|
|
150
177
|
this.active = false;
|
|
151
|
-
this.
|
|
178
|
+
this.activityPreviews = new WeakMap();
|
|
179
|
+
this.confirmation.cancel();
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
private clear(): void {
|
|
@@ -163,7 +191,7 @@ export class UserAgentWidget {
|
|
|
163
191
|
}
|
|
164
192
|
this.active = false;
|
|
165
193
|
this.selectedIndex = 0;
|
|
166
|
-
this.
|
|
194
|
+
this.confirmation.cancel();
|
|
167
195
|
}
|
|
168
196
|
|
|
169
197
|
private runningAgentsForWidget(): RunningAgent[] {
|
|
@@ -207,13 +235,13 @@ export class UserAgentWidget {
|
|
|
207
235
|
}
|
|
208
236
|
|
|
209
237
|
if (matchesKey(data, "down")) {
|
|
210
|
-
this.
|
|
238
|
+
this.confirmation.cancel();
|
|
211
239
|
this.selectedIndex = Math.min(entries.length - 1, this.selectedIndex + 1);
|
|
212
240
|
this.update();
|
|
213
241
|
return { consume: true };
|
|
214
242
|
}
|
|
215
243
|
if (matchesKey(data, "up")) {
|
|
216
|
-
this.
|
|
244
|
+
this.confirmation.cancel();
|
|
217
245
|
if (this.selectedIndex === 0) {
|
|
218
246
|
this.deactivate();
|
|
219
247
|
return { consume: true };
|
|
@@ -228,20 +256,40 @@ export class UserAgentWidget {
|
|
|
228
256
|
}
|
|
229
257
|
|
|
230
258
|
const selected = entries[this.selectedIndex];
|
|
259
|
+
if (!matchesKey(data, "d") && !matchesKey(data, "r")) this.confirmation.cancel();
|
|
231
260
|
if (
|
|
232
261
|
matchesKey(data, "ctrl+x") &&
|
|
233
262
|
selected?.kind === "running" &&
|
|
234
263
|
isLiveAgent(selected.agent)
|
|
235
264
|
) {
|
|
236
|
-
this.detachConfirmation.cancel();
|
|
237
265
|
this.interruptRunning(selected.agent);
|
|
238
266
|
return { consume: true };
|
|
239
267
|
}
|
|
240
268
|
if (matchesKey(data, "enter")) {
|
|
241
|
-
this.detachConfirmation.cancel();
|
|
242
269
|
if (selected) this.openSelected(selected.agent);
|
|
243
270
|
return { consume: true };
|
|
244
271
|
}
|
|
272
|
+
if (matchesKey(data, "c") && selected) {
|
|
273
|
+
const text =
|
|
274
|
+
isRunningAgent(selected.agent) || selected.agent.ok
|
|
275
|
+
? selected.agent.responseText
|
|
276
|
+
: (selected.agent.error ?? "");
|
|
277
|
+
this.copyToClipboard(selected.agent.id, text, "response");
|
|
278
|
+
return { consume: true };
|
|
279
|
+
}
|
|
280
|
+
if (matchesKey(data, "i") && selected) {
|
|
281
|
+
this.copyToClipboard(selected.agent.id, selected.agent.sessionId, "session-id");
|
|
282
|
+
return { consume: true };
|
|
283
|
+
}
|
|
284
|
+
if (matchesKey(data, "s")) {
|
|
285
|
+
if (selected && this.canSquashMainContext(selected.agent.id))
|
|
286
|
+
this.squashMainContext(selected.agent.id);
|
|
287
|
+
return { consume: true };
|
|
288
|
+
}
|
|
289
|
+
if (matchesKey(data, "r")) {
|
|
290
|
+
this.requestRebase(selected);
|
|
291
|
+
return { consume: true };
|
|
292
|
+
}
|
|
245
293
|
if (matchesKey(data, "d") && selected) {
|
|
246
294
|
if (!this.confirmDetach(selected.agent)) return { consume: true };
|
|
247
295
|
if (selected.kind === "running") this.closeRunning(selected.agent);
|
|
@@ -254,15 +302,64 @@ export class UserAgentWidget {
|
|
|
254
302
|
}
|
|
255
303
|
|
|
256
304
|
private confirmDetach(target: ViewableAgent): boolean {
|
|
257
|
-
const confirmed = this.
|
|
305
|
+
const confirmed = this.confirmation.press(`detach:${target.id}`);
|
|
258
306
|
if (!confirmed) this.update();
|
|
259
307
|
return confirmed;
|
|
260
308
|
}
|
|
261
309
|
|
|
310
|
+
private requestRebase(selected: UserAgentWidgetEntry | undefined): void {
|
|
311
|
+
if (!selected) return;
|
|
312
|
+
const agentId = selected.agent.id;
|
|
313
|
+
const reason = this.rebaseBlockReason(agentId);
|
|
314
|
+
if (reason) {
|
|
315
|
+
this.confirmation.cancel();
|
|
316
|
+
this.showRebaseWarning(agentId, `Can't rebase: ${reason}`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!this.deliverableAgent(agentId)) {
|
|
320
|
+
this.confirmation.cancel();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
this.rebaseDetachCount(agentId) > 0 &&
|
|
325
|
+
!this.confirmation.press(`rebase:${agentId}`)
|
|
326
|
+
) {
|
|
327
|
+
this.update();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.rebaseMainContext(agentId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private copyToClipboard(agentId: string, text: string, target: CopyTarget): void {
|
|
334
|
+
const child = childProcess.execFile("pbcopy", (error) => {
|
|
335
|
+
if (error) return;
|
|
336
|
+
this.copied = { agentId, target };
|
|
337
|
+
if (this.copiedTimer) clearTimeout(this.copiedTimer);
|
|
338
|
+
this.update();
|
|
339
|
+
this.copiedTimer = setTimeout(() => {
|
|
340
|
+
this.copied = undefined;
|
|
341
|
+
this.copiedTimer = undefined;
|
|
342
|
+
this.update();
|
|
343
|
+
}, COPY_FEEDBACK_MS);
|
|
344
|
+
});
|
|
345
|
+
child.stdin?.end(text);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private showRebaseWarning(agentId: string, text: string): void {
|
|
349
|
+
this.rebaseWarning = { agentId, text };
|
|
350
|
+
if (this.rebaseWarningTimer) clearTimeout(this.rebaseWarningTimer);
|
|
351
|
+
this.rebaseWarningTimer = setTimeout(() => {
|
|
352
|
+
this.rebaseWarning = undefined;
|
|
353
|
+
this.rebaseWarningTimer = undefined;
|
|
354
|
+
this.update();
|
|
355
|
+
}, CONFIRMATION_WINDOW_MS);
|
|
356
|
+
this.update();
|
|
357
|
+
}
|
|
358
|
+
|
|
262
359
|
private deactivate(): void {
|
|
263
360
|
this.active = false;
|
|
264
361
|
this.selectedIndex = 0;
|
|
265
|
-
this.
|
|
362
|
+
this.confirmation.cancel();
|
|
266
363
|
this.update();
|
|
267
364
|
}
|
|
268
365
|
|
|
@@ -493,24 +590,60 @@ export class UserAgentWidget {
|
|
|
493
590
|
} else {
|
|
494
591
|
const selected = entries[this.selectedIndex];
|
|
495
592
|
const confirmingDetach =
|
|
496
|
-
selected !== undefined &&
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
selected
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
593
|
+
selected !== undefined &&
|
|
594
|
+
this.confirmation.isArmedOn(`detach:${selected.agent.id}`);
|
|
595
|
+
const confirmingRebase =
|
|
596
|
+
selected !== undefined &&
|
|
597
|
+
this.confirmation.isArmedOn(`rebase:${selected.agent.id}`);
|
|
598
|
+
if (confirmingRebase && selected) {
|
|
599
|
+
const detachCount = this.rebaseDetachCount(selected.agent.id);
|
|
600
|
+
lines.push(
|
|
601
|
+
truncateToWidth(
|
|
602
|
+
` ${theme.fg(
|
|
603
|
+
"warning",
|
|
604
|
+
`⚠ Rebase will detach ${detachCount} other agent session${detachCount === 1 ? "" : "s"}. r again to confirm`,
|
|
605
|
+
)}`,
|
|
606
|
+
width,
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
} else if (selected && this.rebaseWarning?.agentId === selected.agent.id) {
|
|
610
|
+
lines.push(
|
|
611
|
+
truncateToWidth(` ${theme.fg("warning", `⚠ ${this.rebaseWarning.text}`)}`, width),
|
|
612
|
+
);
|
|
613
|
+
} else {
|
|
614
|
+
const segments = [theme.fg("accent", "↑↓ select"), theme.fg("accent", "Enter view")];
|
|
615
|
+
if (!confirmingDetach)
|
|
616
|
+
segments.push(
|
|
617
|
+
this.copied?.agentId === selected?.agent.id &&
|
|
618
|
+
this.copied.target === "response"
|
|
619
|
+
? theme.fg("success", "✓ copied")
|
|
620
|
+
: theme.fg("accent", "c copy"),
|
|
621
|
+
this.copied?.agentId === selected?.agent.id &&
|
|
622
|
+
this.copied.target === "session-id"
|
|
623
|
+
? theme.fg("success", "✓ copied ID")
|
|
624
|
+
: theme.fg("accent", "i ID"),
|
|
625
|
+
);
|
|
626
|
+
if (!confirmingDetach && selected && this.canSquashMainContext(selected.agent.id))
|
|
627
|
+
segments.push(theme.fg("accent", "s squash"));
|
|
628
|
+
if (!confirmingDetach && selected && this.canRebaseMainContext(selected.agent.id))
|
|
629
|
+
segments.push(theme.fg("accent", "r rebase"));
|
|
630
|
+
if (
|
|
631
|
+
!confirmingDetach &&
|
|
632
|
+
selected?.kind === "running" &&
|
|
633
|
+
isLiveAgent(selected.agent)
|
|
634
|
+
)
|
|
635
|
+
segments.push(theme.fg("accent", "Ctrl+x interrupt"));
|
|
636
|
+
segments.push(
|
|
637
|
+
theme.fg(
|
|
638
|
+
confirmingDetach ? "error" : "accent",
|
|
639
|
+
confirmingDetach ? "d again to confirm" : "d detach",
|
|
640
|
+
),
|
|
641
|
+
theme.fg("accent", "Esc back"),
|
|
642
|
+
);
|
|
643
|
+
lines.push(
|
|
644
|
+
truncateToWidth(` ${segments.join(theme.fg("accent", " · "))}`, width),
|
|
645
|
+
);
|
|
646
|
+
}
|
|
514
647
|
}
|
|
515
648
|
|
|
516
649
|
const maxEntries = Math.max(1, Math.floor((MAX_WIDGET_LINES - lines.length) / 2));
|
|
@@ -550,15 +683,17 @@ export class UserAgentWidget {
|
|
|
550
683
|
const running = isRunningAgent(agent);
|
|
551
684
|
const inFlight = isLiveAgent(agent);
|
|
552
685
|
const ok = running ? agent.error === undefined : agent.ok;
|
|
686
|
+
const status = inFlight ? "running" : running ? "idle" : ok ? "completed" : "failed";
|
|
553
687
|
const marker = selected ? theme.fg("accent", "⏺") : theme.fg("dim", "◯");
|
|
554
688
|
const icon = inFlight
|
|
555
689
|
? theme.fg("accent", SPINNER[this.frame % SPINNER.length])
|
|
556
|
-
:
|
|
557
|
-
? theme.fg("
|
|
558
|
-
:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
690
|
+
: running
|
|
691
|
+
? theme.fg("muted", "◉")
|
|
692
|
+
: ok
|
|
693
|
+
? theme.fg("success", "✓")
|
|
694
|
+
: theme.fg("error", "✗");
|
|
695
|
+
const parts = [agent.modelLabel, status];
|
|
696
|
+
if (inFlight) parts.push(contextLabel(agent.inheritedContext));
|
|
562
697
|
const mainContext = mainContextLabel(agent.mainContextState);
|
|
563
698
|
if (mainContext) parts.push(mainContext);
|
|
564
699
|
if (agent.turnCount > 0) parts.push(formatTurns(agent.turnCount));
|
|
@@ -574,6 +709,7 @@ export class UserAgentWidget {
|
|
|
574
709
|
marker,
|
|
575
710
|
icon,
|
|
576
711
|
theme.bold(`/${agent.command}`),
|
|
712
|
+
selected ? theme.fg("dim", `ID ${agent.sessionId.slice(0, 8)}`) : "",
|
|
577
713
|
renderAgentContextMeter(agent, theme),
|
|
578
714
|
theme.fg("muted", truncatePlain(agent.task, 52)),
|
|
579
715
|
theme.fg("dim", "·"),
|
|
@@ -583,16 +719,30 @@ export class UserAgentWidget {
|
|
|
583
719
|
.join(" ");
|
|
584
720
|
const activityPrefix = `${indent} ⎿ `;
|
|
585
721
|
const activityWidth = Math.max(0, width - visibleWidth(activityPrefix));
|
|
722
|
+
const preview = this.renderActivityPreview(agent, activityWidth, theme);
|
|
723
|
+
const activityText = ok ? preview : theme.fg("error", preview);
|
|
724
|
+
const activity = `${theme.fg("dim", indent)} ${theme.fg("dim", "⎿ ")}${activityText}`;
|
|
725
|
+
return [truncateToWidth(header, width), truncateToWidth(activity, width)];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private renderActivityPreview(agent: ViewableAgent, width: number, theme: Theme): string {
|
|
729
|
+
const running = isRunningAgent(agent);
|
|
730
|
+
const source = isLiveAgent(agent) ? agent.latestFinalizedMessage : agent.responseText;
|
|
731
|
+
const status = running ? agent.status : agent.ok;
|
|
732
|
+
const cached = this.activityPreviews.get(agent);
|
|
733
|
+
if (
|
|
734
|
+
cached && cached.source === source && cached.status === status &&
|
|
735
|
+
cached.error === agent.error && cached.width === width && cached.theme === theme
|
|
736
|
+
) return cached.text;
|
|
586
737
|
const description = running
|
|
587
738
|
? describeActivity(agent)
|
|
588
739
|
: {
|
|
589
740
|
text: agent.ok ? agent.responseText : `Error: ${agent.error ?? "unknown"}`,
|
|
590
741
|
truncation: "tail" as const,
|
|
591
742
|
};
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return [truncateToWidth(header, width), truncateToWidth(activity, width)];
|
|
743
|
+
const text = renderActivity(description, width, theme);
|
|
744
|
+
this.activityPreviews.set(agent, { source, status, error: agent.error, width, theme, text });
|
|
745
|
+
return text;
|
|
596
746
|
}
|
|
597
747
|
}
|
|
598
748
|
|