@giladbarnea/pi-user-agents 0.0.8 → 0.0.9
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 +11 -1
- package/PARSER_SPEC.md +10 -2
- package/README.md +20 -2
- package/agent-viewer.ts +83 -32
- package/attach.ts +35 -4
- package/command-line.ts +47 -5
- package/context-meter.ts +5 -1
- package/herdr.ts +92 -0
- package/index.ts +2 -1
- package/package.json +3 -3
- package/runner.ts +109 -15
- package/screenshots/below-editor-widget-2.png +0 -0
- package/screenshots/editor-with-autocomplete.png +0 -0
- package/screenshots/overlay-2.png +0 -0
- package/shared.ts +26 -0
- package/widget.ts +162 -45
package/widget.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
AgentViewer,
|
|
10
10
|
type AgentViewerAction,
|
|
11
|
+
type AgentViewerActions,
|
|
11
12
|
describeActivity,
|
|
12
13
|
isLiveAgent,
|
|
13
14
|
isRunningAgent,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
AgentMessage,
|
|
22
23
|
AgentResultMessage,
|
|
23
24
|
CompletedAgent,
|
|
25
|
+
HerdrDelivery,
|
|
24
26
|
RebaseDelivery,
|
|
25
27
|
RunningAgent,
|
|
26
28
|
Theme,
|
|
@@ -29,10 +31,12 @@ import type {
|
|
|
29
31
|
import {
|
|
30
32
|
CONFIRMATION_WINDOW_MS,
|
|
31
33
|
contextLabel,
|
|
34
|
+
errorMessage,
|
|
32
35
|
formatMs,
|
|
33
36
|
TimedConfirmation,
|
|
34
37
|
formatToolUses,
|
|
35
38
|
formatTurns,
|
|
39
|
+
herdrPaneLabel,
|
|
36
40
|
logSteering,
|
|
37
41
|
mainContextLabel,
|
|
38
42
|
MAX_WIDGET_LINES,
|
|
@@ -48,6 +52,7 @@ type UserAgentWidgetEntry =
|
|
|
48
52
|
type CachedActivityPreview = {
|
|
49
53
|
source: AgentMessage | string | undefined;
|
|
50
54
|
status: RunningAgent["status"] | boolean;
|
|
55
|
+
herdrPane: string | undefined;
|
|
51
56
|
error: string | undefined;
|
|
52
57
|
width: number;
|
|
53
58
|
theme: Theme;
|
|
@@ -70,8 +75,11 @@ export class UserAgentWidget {
|
|
|
70
75
|
private viewerOpen = false;
|
|
71
76
|
private copied: { agentId: string; target: CopyTarget } | undefined;
|
|
72
77
|
private copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
73
|
-
|
|
74
|
-
private
|
|
78
|
+
/** A transient per-row notice explaining why an action did nothing. */
|
|
79
|
+
private warning: { agentId: string; text: string } | undefined;
|
|
80
|
+
private warningTimer: ReturnType<typeof setTimeout> | undefined;
|
|
81
|
+
/** The agent whose herdr pane is being opened right now. */
|
|
82
|
+
private herdrPending: string | undefined;
|
|
75
83
|
private activityPreviews = new WeakMap<ViewableAgent, CachedActivityPreview>();
|
|
76
84
|
/** One confirmation slot for both destructive selected-row actions. */
|
|
77
85
|
private readonly confirmation = new TimedConfirmation<string>(() => this.update());
|
|
@@ -82,6 +90,7 @@ export class UserAgentWidget {
|
|
|
82
90
|
private readonly sendSquashedResult: (message: AgentResultMessage) => void,
|
|
83
91
|
private readonly announceDetached: (sessionId: string) => void,
|
|
84
92
|
private readonly rebaseDelivery: RebaseDelivery,
|
|
93
|
+
private readonly herdrDelivery: HerdrDelivery,
|
|
85
94
|
) {}
|
|
86
95
|
|
|
87
96
|
setUI(ui: UIContext): void {
|
|
@@ -100,7 +109,22 @@ export class UserAgentWidget {
|
|
|
100
109
|
resultMessage: AgentResultMessage,
|
|
101
110
|
options: { squashable: boolean },
|
|
102
111
|
): void {
|
|
103
|
-
this.
|
|
112
|
+
this.snapshot(agent, {
|
|
113
|
+
ok: resultMessage.details.ok,
|
|
114
|
+
pendingSquashMessage: options.squashable ? resultMessage : undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The row for an agent dispatched straight into a herdr pane: nothing ran here, so nothing squashes. */
|
|
119
|
+
addHerdrDispatch(agent: RunningAgent, paneId: string): void {
|
|
120
|
+
this.snapshot(agent, { ok: true, herdrPane: paneId });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private snapshot(
|
|
124
|
+
agent: RunningAgent,
|
|
125
|
+
outcome: { ok: boolean; pendingSquashMessage?: AgentResultMessage; herdrPane?: string },
|
|
126
|
+
): CompletedAgent {
|
|
127
|
+
const completed: CompletedAgent = {
|
|
104
128
|
id: agent.id,
|
|
105
129
|
sessionId: agent.sessionId,
|
|
106
130
|
command: agent.command,
|
|
@@ -108,8 +132,8 @@ export class UserAgentWidget {
|
|
|
108
132
|
task: agent.task,
|
|
109
133
|
dispatchBaseFingerprint: agent.dispatchBaseFingerprint,
|
|
110
134
|
mainContextState: agent.mainContextState,
|
|
111
|
-
pendingSquashMessage:
|
|
112
|
-
ok:
|
|
135
|
+
pendingSquashMessage: outcome.pendingSquashMessage,
|
|
136
|
+
ok: outcome.ok,
|
|
113
137
|
responseText: agent.responseText,
|
|
114
138
|
messages: structuredClone(transcriptMessages(agent)),
|
|
115
139
|
error: agent.error,
|
|
@@ -118,8 +142,11 @@ export class UserAgentWidget {
|
|
|
118
142
|
toolUses: agent.toolUses,
|
|
119
143
|
turnCount: agent.turnCount,
|
|
120
144
|
contextPercent: agent.session?.getContextUsage()?.percent ?? undefined,
|
|
121
|
-
|
|
145
|
+
herdrPane: outcome.herdrPane,
|
|
146
|
+
};
|
|
147
|
+
this.completedAgents.push(completed);
|
|
122
148
|
this.update();
|
|
149
|
+
return completed;
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
ensureTimer(): void {
|
|
@@ -290,6 +317,10 @@ export class UserAgentWidget {
|
|
|
290
317
|
this.requestRebase(selected);
|
|
291
318
|
return { consume: true };
|
|
292
319
|
}
|
|
320
|
+
if (matchesKey(data, "h")) {
|
|
321
|
+
if (selected) this.requestHerdr(selected.agent.id);
|
|
322
|
+
return { consume: true };
|
|
323
|
+
}
|
|
293
324
|
if (matchesKey(data, "d") && selected) {
|
|
294
325
|
if (!this.confirmDetach(selected.agent)) return { consume: true };
|
|
295
326
|
if (selected.kind === "running") this.closeRunning(selected.agent);
|
|
@@ -313,7 +344,7 @@ export class UserAgentWidget {
|
|
|
313
344
|
const reason = this.rebaseBlockReason(agentId);
|
|
314
345
|
if (reason) {
|
|
315
346
|
this.confirmation.cancel();
|
|
316
|
-
this.
|
|
347
|
+
this.showWarning(agentId, `Can't rebase: ${reason}`);
|
|
317
348
|
return;
|
|
318
349
|
}
|
|
319
350
|
if (!this.deliverableAgent(agentId)) {
|
|
@@ -330,6 +361,58 @@ export class UserAgentWidget {
|
|
|
330
361
|
this.rebaseMainContext(agentId);
|
|
331
362
|
}
|
|
332
363
|
|
|
364
|
+
private requestHerdr(agentId: string): void {
|
|
365
|
+
this.openInHerdr(agentId).catch((error) =>
|
|
366
|
+
this.showWarning(agentId, `Can't open in herdr: ${errorMessage(error)}`),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Hand the agent's session to a Pi in a new herdr pane, then return focus to the editor. */
|
|
371
|
+
async openInHerdr(agentId: string): Promise<void> {
|
|
372
|
+
const reason = this.herdrBlockReason(agentId);
|
|
373
|
+
if (reason) throw new Error(reason);
|
|
374
|
+
this.herdrPending = agentId;
|
|
375
|
+
this.update();
|
|
376
|
+
try {
|
|
377
|
+
// The pane comes first: a herdr failure here leaves the agent untouched. Pi starts in it
|
|
378
|
+
// only after the session is free here, so the file has one writer at a time.
|
|
379
|
+
const target = this.idleAgent(agentId) ?? this.completedAgents.find((agent) => agent.id === agentId);
|
|
380
|
+
if (!target) throw new Error("the agent has no row anymore");
|
|
381
|
+
const pane = await this.herdrDelivery.split(target);
|
|
382
|
+
const snapshot = await this.releaseForHerdr(agentId);
|
|
383
|
+
await pane.start();
|
|
384
|
+
snapshot.herdrPane = pane.paneId;
|
|
385
|
+
logSteering(agentId, "opened-in-herdr", { paneId: pane.paneId });
|
|
386
|
+
} finally {
|
|
387
|
+
this.herdrPending = undefined;
|
|
388
|
+
this.update();
|
|
389
|
+
}
|
|
390
|
+
this.deactivate();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Why the agent cannot open in a herdr pane right now; undefined when it can. */
|
|
394
|
+
private herdrBlockReason(agentId: string): string | undefined {
|
|
395
|
+
if (!this.herdrDelivery.available()) return "Pi is not running inside a herdr pane";
|
|
396
|
+
if (this.herdrPending === agentId) return "a herdr pane is already opening";
|
|
397
|
+
const completed = this.completedAgents.find((agent) => agent.id === agentId);
|
|
398
|
+
if (completed?.herdrPane) return `already in ${herdrPaneLabel(completed.herdrPane)}`;
|
|
399
|
+
if (!completed && !this.idleAgent(agentId)) return "the agent is mid-turn (Ctrl+x interrupts it)";
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** End the agent's life here and keep its row as a snapshot; resolves once its session is disposed. */
|
|
404
|
+
private async releaseForHerdr(agentId: string): Promise<CompletedAgent> {
|
|
405
|
+
const idleAgent = this.idleAgent(agentId);
|
|
406
|
+
if (idleAgent) {
|
|
407
|
+
idleAgent.status = "posted";
|
|
408
|
+
this.retireRunning(idleAgent);
|
|
409
|
+
this.snapshot(idleAgent, { ok: true, pendingSquashMessage: idleAgent.pendingSquashMessage });
|
|
410
|
+
}
|
|
411
|
+
// The lifecycle disposes the child session asynchronously; another Pi may open the file only after that.
|
|
412
|
+
await [...this.runningAgents].find((agent) => agent.id === agentId)?.finished;
|
|
413
|
+
return this.completedAgents.find((agent) => agent.id === agentId)!;
|
|
414
|
+
}
|
|
415
|
+
|
|
333
416
|
private copyToClipboard(agentId: string, text: string, target: CopyTarget): void {
|
|
334
417
|
const child = childProcess.execFile("pbcopy", (error) => {
|
|
335
418
|
if (error) return;
|
|
@@ -345,12 +428,12 @@ export class UserAgentWidget {
|
|
|
345
428
|
child.stdin?.end(text);
|
|
346
429
|
}
|
|
347
430
|
|
|
348
|
-
private
|
|
349
|
-
this.
|
|
350
|
-
if (this.
|
|
351
|
-
this.
|
|
352
|
-
this.
|
|
353
|
-
this.
|
|
431
|
+
private showWarning(agentId: string, text: string): void {
|
|
432
|
+
this.warning = { agentId, text };
|
|
433
|
+
if (this.warningTimer) clearTimeout(this.warningTimer);
|
|
434
|
+
this.warningTimer = setTimeout(() => {
|
|
435
|
+
this.warning = undefined;
|
|
436
|
+
this.warningTimer = undefined;
|
|
354
437
|
this.update();
|
|
355
438
|
}, CONFIRMATION_WINDOW_MS);
|
|
356
439
|
this.update();
|
|
@@ -395,21 +478,7 @@ export class UserAgentWidget {
|
|
|
395
478
|
void ui
|
|
396
479
|
.custom<AgentViewerAction>(
|
|
397
480
|
(tui, theme, _keybindings, done) =>
|
|
398
|
-
new AgentViewer(
|
|
399
|
-
tui,
|
|
400
|
-
agent,
|
|
401
|
-
theme,
|
|
402
|
-
done,
|
|
403
|
-
() => this.canSquashMainContext(agent.id),
|
|
404
|
-
() => this.squashMainContext(agent.id),
|
|
405
|
-
() => this.canRebaseMainContext(agent.id),
|
|
406
|
-
() => this.rebaseMainContext(agent.id),
|
|
407
|
-
() => this.rebaseBlockReason(agent.id),
|
|
408
|
-
() => this.rebaseDetachCount(agent.id),
|
|
409
|
-
() => {
|
|
410
|
-
if (isRunningAgent(agent)) this.interruptRunning(agent);
|
|
411
|
-
},
|
|
412
|
-
),
|
|
481
|
+
new AgentViewer(tui, agent, theme, done, this.viewerActions(agent)),
|
|
413
482
|
{ overlay: true, overlayOptions: { anchor: "center", width: "90%", maxHeight: "85%" } },
|
|
414
483
|
)
|
|
415
484
|
.then(
|
|
@@ -418,13 +487,34 @@ export class UserAgentWidget {
|
|
|
418
487
|
);
|
|
419
488
|
}
|
|
420
489
|
|
|
490
|
+
private viewerActions(agent: ViewableAgent): AgentViewerActions {
|
|
491
|
+
return {
|
|
492
|
+
canSquashMainContext: () => this.canSquashMainContext(agent.id),
|
|
493
|
+
squashMainContext: () => this.squashMainContext(agent.id),
|
|
494
|
+
canRebaseMainContext: () => this.canRebaseMainContext(agent.id),
|
|
495
|
+
rebaseMainContext: () => this.rebaseMainContext(agent.id),
|
|
496
|
+
rebaseBlockReason: () => this.rebaseBlockReason(agent.id),
|
|
497
|
+
rebaseDetachCount: () => this.rebaseDetachCount(agent.id),
|
|
498
|
+
interrupt: () => {
|
|
499
|
+
if (isRunningAgent(agent)) this.interruptRunning(agent);
|
|
500
|
+
},
|
|
501
|
+
herdrBlockReason: () => this.herdrBlockReason(agent.id),
|
|
502
|
+
openInHerdr: () => this.openInHerdr(agent.id),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
421
506
|
/** Detaching ends the live session and leaves its session file on disk, untouched. */
|
|
422
507
|
private closeRunning(agent: RunningAgent): void {
|
|
508
|
+
this.retireRunning(agent);
|
|
509
|
+
this.announceDetached(agent.sessionId);
|
|
510
|
+
this.update();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** End a live agent's session here; its row and any breadcrumb are the caller's business. */
|
|
514
|
+
private retireRunning(agent: RunningAgent): void {
|
|
423
515
|
agent.aborted = true;
|
|
424
516
|
void agent.session?.abort();
|
|
425
517
|
agent.retire?.();
|
|
426
|
-
this.announceDetached(agent.sessionId);
|
|
427
|
-
this.update();
|
|
428
518
|
}
|
|
429
519
|
|
|
430
520
|
private interruptRunning(agent: RunningAgent): void {
|
|
@@ -606,10 +696,10 @@ export class UserAgentWidget {
|
|
|
606
696
|
width,
|
|
607
697
|
),
|
|
608
698
|
);
|
|
609
|
-
} else if (selected && this.
|
|
610
|
-
lines.push(
|
|
611
|
-
|
|
612
|
-
);
|
|
699
|
+
} else if (selected && this.herdrPending === selected.agent.id) {
|
|
700
|
+
lines.push(truncateToWidth(` ${theme.fg("accent", "⧉ Opening a herdr pane…")}`, width));
|
|
701
|
+
} else if (selected && this.warning?.agentId === selected.agent.id) {
|
|
702
|
+
lines.push(truncateToWidth(` ${theme.fg("warning", `⚠ ${this.warning.text}`)}`, width));
|
|
613
703
|
} else {
|
|
614
704
|
const segments = [theme.fg("accent", "↑↓ select"), theme.fg("accent", "Enter view")];
|
|
615
705
|
if (!confirmingDetach)
|
|
@@ -627,6 +717,8 @@ export class UserAgentWidget {
|
|
|
627
717
|
segments.push(theme.fg("accent", "s squash"));
|
|
628
718
|
if (!confirmingDetach && selected && this.canRebaseMainContext(selected.agent.id))
|
|
629
719
|
segments.push(theme.fg("accent", "r rebase"));
|
|
720
|
+
if (!confirmingDetach && selected && this.herdrBlockReason(selected.agent.id) === undefined)
|
|
721
|
+
segments.push(theme.fg("accent", "h herdr"));
|
|
630
722
|
if (
|
|
631
723
|
!confirmingDetach &&
|
|
632
724
|
selected?.kind === "running" &&
|
|
@@ -682,27 +774,40 @@ export class UserAgentWidget {
|
|
|
682
774
|
): string[] {
|
|
683
775
|
const running = isRunningAgent(agent);
|
|
684
776
|
const inFlight = isLiveAgent(agent);
|
|
777
|
+
const herdrPane = running ? undefined : agent.herdrPane;
|
|
685
778
|
const ok = running ? agent.error === undefined : agent.ok;
|
|
686
|
-
const status = inFlight
|
|
779
|
+
const status = inFlight
|
|
780
|
+
? "running"
|
|
781
|
+
: running
|
|
782
|
+
? "idle"
|
|
783
|
+
: herdrPane
|
|
784
|
+
? herdrPaneLabel(herdrPane)
|
|
785
|
+
: ok
|
|
786
|
+
? "completed"
|
|
787
|
+
: "failed";
|
|
687
788
|
const marker = selected ? theme.fg("accent", "⏺") : theme.fg("dim", "◯");
|
|
688
789
|
const icon = inFlight
|
|
689
790
|
? theme.fg("accent", SPINNER[this.frame % SPINNER.length])
|
|
690
791
|
: running
|
|
691
792
|
? theme.fg("muted", "◉")
|
|
692
|
-
:
|
|
693
|
-
? theme.fg("
|
|
694
|
-
:
|
|
793
|
+
: herdrPane
|
|
794
|
+
? theme.fg("muted", "⧉")
|
|
795
|
+
: ok
|
|
796
|
+
? theme.fg("success", "✓")
|
|
797
|
+
: theme.fg("error", "✗");
|
|
695
798
|
const parts = [agent.modelLabel, status];
|
|
696
799
|
if (inFlight) parts.push(contextLabel(agent.inheritedContext));
|
|
697
800
|
const mainContext = mainContextLabel(agent.mainContextState);
|
|
698
801
|
if (mainContext) parts.push(mainContext);
|
|
699
802
|
if (agent.turnCount > 0) parts.push(formatTurns(agent.turnCount));
|
|
700
803
|
if (agent.toolUses > 0) parts.push(formatToolUses(agent.toolUses));
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
804
|
+
// An agent born in a herdr pane never ran here; a duration would be noise.
|
|
805
|
+
if (!herdrPane || agent.turnCount > 0)
|
|
806
|
+
parts.push(
|
|
807
|
+
formatMs(
|
|
808
|
+
running ? (agent.completedAt ?? Date.now()) - agent.turnStartedAt : agent.durationMs,
|
|
809
|
+
),
|
|
810
|
+
);
|
|
706
811
|
|
|
707
812
|
const header = [
|
|
708
813
|
theme.fg("dim", connector),
|
|
@@ -729,19 +834,31 @@ export class UserAgentWidget {
|
|
|
729
834
|
const running = isRunningAgent(agent);
|
|
730
835
|
const source = isLiveAgent(agent) ? agent.latestFinalizedMessage : agent.responseText;
|
|
731
836
|
const status = running ? agent.status : agent.ok;
|
|
837
|
+
const herdrPane = running ? undefined : agent.herdrPane;
|
|
732
838
|
const cached = this.activityPreviews.get(agent);
|
|
733
839
|
if (
|
|
734
840
|
cached && cached.source === source && cached.status === status &&
|
|
841
|
+
cached.herdrPane === herdrPane &&
|
|
735
842
|
cached.error === agent.error && cached.width === width && cached.theme === theme
|
|
736
843
|
) return cached.text;
|
|
737
844
|
const description = running
|
|
738
845
|
? describeActivity(agent)
|
|
739
846
|
: {
|
|
740
|
-
text: agent.ok
|
|
847
|
+
text: !agent.ok
|
|
848
|
+
? `Error: ${agent.error ?? "unknown"}`
|
|
849
|
+
: agent.responseText || (herdrPane ? `runs in ${herdrPaneLabel(herdrPane)}` : ""),
|
|
741
850
|
truncation: "tail" as const,
|
|
742
851
|
};
|
|
743
852
|
const text = renderActivity(description, width, theme);
|
|
744
|
-
this.activityPreviews.set(agent, {
|
|
853
|
+
this.activityPreviews.set(agent, {
|
|
854
|
+
source,
|
|
855
|
+
status,
|
|
856
|
+
herdrPane,
|
|
857
|
+
error: agent.error,
|
|
858
|
+
width,
|
|
859
|
+
theme,
|
|
860
|
+
text,
|
|
861
|
+
});
|
|
745
862
|
return text;
|
|
746
863
|
}
|
|
747
864
|
}
|