@ferris1225/pi-subagents 0.32.2 → 1.0.1
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/README.md +45 -22
- package/agents/reviewer.md +2 -0
- package/package.json +2 -2
- package/src/agents.ts +2 -7
- package/src/announcements.ts +62 -0
- package/src/completion.ts +7 -36
- package/src/config.ts +327 -364
- package/src/dispatch.ts +1682 -1878
- package/src/fixloop.ts +0 -16
- package/src/format.ts +4 -8
- package/src/index.ts +8 -9
- package/src/models.ts +17 -39
- package/src/monitor.ts +64 -175
- package/src/rpc-run.ts +2 -41
- package/src/runtime.ts +272 -285
- package/src/session-fork.ts +0 -4
- package/src/setup.ts +4 -4
- package/src/spawn.ts +542 -562
- package/src/tools.ts +706 -748
- package/src/ui.ts +3 -7
- package/src/widget.ts +90 -178
- package/src/worktree.ts +1 -1
- package/src/inspector-panel.ts +0 -363
- package/src/inspector.ts +0 -369
- package/src/trajectory.ts +0 -503
package/src/ui.ts
CHANGED
|
@@ -45,9 +45,7 @@ export interface PickerStyles {
|
|
|
45
45
|
filterEcho: (t: string) => string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export
|
|
49
|
-
disabled?: boolean;
|
|
50
|
-
}
|
|
48
|
+
export type PickerItem = SelectItem;
|
|
51
49
|
|
|
52
50
|
export function pickerItemSearchText(item: PickerItem): string {
|
|
53
51
|
return `${item.value} ${item.label} ${item.description ?? ""}`;
|
|
@@ -118,9 +116,7 @@ export class Picker implements Component, Focusable {
|
|
|
118
116
|
const item = visible[i];
|
|
119
117
|
const isCursor = start + i === this.cursor;
|
|
120
118
|
const mark = isCursor ? s.cursorMark("❯ ") : " ";
|
|
121
|
-
const label = item.
|
|
122
|
-
? s.dim(item.label)
|
|
123
|
-
: isCursor ? s.selectedLabel(item.label) : s.label(item.label);
|
|
119
|
+
const label = isCursor ? s.selectedLabel(item.label) : s.label(item.label);
|
|
124
120
|
const description = item.description ? s.dim(` — ${item.description}`) : "";
|
|
125
121
|
const line = this.multi
|
|
126
122
|
? mark + (this.selected.has(item.value) ? s.checked("[x] ") : s.unchecked("[ ] ")) + label + description
|
|
@@ -149,7 +145,7 @@ export class Picker implements Component, Focusable {
|
|
|
149
145
|
if (this.multi) this.cb.onConfirm?.([...this.selected]);
|
|
150
146
|
else {
|
|
151
147
|
const item = this.filtered[this.cursor];
|
|
152
|
-
if (item
|
|
148
|
+
if (item) this.cb.onSelect?.(item.value);
|
|
153
149
|
}
|
|
154
150
|
return;
|
|
155
151
|
} else if (kb.matches(data, "tui.select.cancel")) {
|
package/src/widget.ts
CHANGED
|
@@ -1,195 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session_start wiring: the persistent status widget above the editor, plus
|
|
3
|
-
* one-time feature announcements (a new configurable option is surfaced to the
|
|
4
|
-
* user once after an update; the marker persists in `announcedFeatures`).
|
|
5
|
-
*/
|
|
1
|
+
/** Lightweight active-run widget for interactive Pi sessions. */
|
|
6
2
|
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
|
-
import { loadConfig, saveConfig } from "./config.ts";
|
|
3
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
5
|
import {
|
|
12
|
-
activityStateLabel,
|
|
13
|
-
compactLine,
|
|
14
|
-
deriveActivityState,
|
|
15
6
|
formatElapsed,
|
|
16
|
-
formatUsageCompact,
|
|
17
7
|
isRunActiveStatus,
|
|
18
8
|
monitor,
|
|
19
9
|
statusIcon,
|
|
20
10
|
statusLabel,
|
|
11
|
+
type RunView,
|
|
21
12
|
} from "./monitor.ts";
|
|
22
|
-
import { announceRecoveryRecords } from "./recovery.ts";
|
|
23
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
* `announcedFeatures` entry). When the feature's precondition is unmet and the
|
|
27
|
-
* marker is absent, the user is told about it exactly once. */
|
|
28
|
-
const ANNOUNCEMENTS: Array<{
|
|
29
|
-
key: string;
|
|
30
|
-
condition: (config: Awaited<ReturnType<typeof loadConfig>>) => boolean;
|
|
31
|
-
message: string;
|
|
32
|
-
}> = [
|
|
33
|
-
{
|
|
34
|
-
key: "visionModel",
|
|
35
|
-
condition: (config) => config.visionModel === undefined,
|
|
36
|
-
message:
|
|
37
|
-
"pi-subagents: new — a vision-capable model can now handle image tasks (screenshots, mockups, designs). Run /subagents-setup to configure it; until set, vision tasks use the main session's current model.",
|
|
38
|
-
},
|
|
39
|
-
];
|
|
14
|
+
export const SUBAGENTS_WIDGET_ID = "pi-subagents";
|
|
40
15
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
16
|
+
/** Keep the elapsed tail visible while clipping the descriptive left side. */
|
|
17
|
+
function compactLine(left: string, right: string, width: number): string {
|
|
18
|
+
if (width <= 0) return "";
|
|
19
|
+
if (!right) return truncateToWidth(left, width, "");
|
|
20
|
+
const separator = " ";
|
|
21
|
+
const rightWidth = visibleWidth(right);
|
|
22
|
+
if (rightWidth >= width) return truncateToWidth(right, width, "");
|
|
23
|
+
const leftWidth = width - rightWidth - visibleWidth(separator);
|
|
24
|
+
if (leftWidth <= 0) return truncateToWidth(right, width, "");
|
|
25
|
+
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One compact line per genuinely active run. Settled and parked threads never
|
|
29
|
+
* appear, so elapsed time cannot keep ticking beside a terminal status. */
|
|
30
|
+
export function formatActiveRunLines(
|
|
31
|
+
runs: readonly RunView[],
|
|
32
|
+
theme: Theme,
|
|
33
|
+
width: number,
|
|
34
|
+
now: number = Date.now(),
|
|
35
|
+
): string[] {
|
|
36
|
+
const dim = (text: string): string => theme.fg("dim", text);
|
|
37
|
+
return runs
|
|
38
|
+
.filter((run) => isRunActiveStatus(run.status))
|
|
39
|
+
.map((run) => {
|
|
40
|
+
const icon = statusIcon(run.status, theme);
|
|
41
|
+
const name = theme.fg("accent", theme.bold(run.agent));
|
|
42
|
+
const context = run.relationLabel ?? run.label;
|
|
43
|
+
const activity = run.status === "running"
|
|
44
|
+
? (run.activity ?? statusLabel(run.status))
|
|
45
|
+
: statusLabel(run.status);
|
|
46
|
+
const parts = [
|
|
47
|
+
`${icon} ${dim(`#${run.id}`)} ${name}`,
|
|
48
|
+
context ? dim(`· ${context}`) : undefined,
|
|
49
|
+
activity ? dim(`· ${activity}`) : undefined,
|
|
50
|
+
].filter((part): part is string => Boolean(part));
|
|
51
|
+
const elapsed = formatElapsed(run, now);
|
|
52
|
+
return compactLine(parts.join(" "), elapsed ? dim(elapsed) : "", width);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
if (pending.length === 0) return;
|
|
68
|
-
await saveConfig(
|
|
69
|
-
{
|
|
70
|
-
...config,
|
|
71
|
-
announcedFeatures: [...config.announcedFeatures, ...pending.map((a) => a.key)],
|
|
72
|
-
},
|
|
73
|
-
runtime.configPath,
|
|
74
|
-
);
|
|
75
|
-
for (const announcement of pending) {
|
|
76
|
-
ctx.ui.notify(announcement.message, "info");
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
/* announcement failures are non-fatal */
|
|
80
|
-
}
|
|
56
|
+
function hasTickingRun(): boolean {
|
|
57
|
+
return monitor.getRuns().some(
|
|
58
|
+
(run) => isRunActiveStatus(run.status) && run.startedAt !== undefined,
|
|
59
|
+
);
|
|
81
60
|
}
|
|
82
61
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
/** Install the widget for one TUI session. Its timer exists only while at least
|
|
63
|
+
* one active run has started and is disposed with the widget. */
|
|
64
|
+
export function installActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
|
|
65
|
+
if (ctx.mode !== "tui") return;
|
|
66
|
+
ctx.ui.setWidget(
|
|
67
|
+
SUBAGENTS_WIDGET_ID,
|
|
68
|
+
(tui, theme) => {
|
|
69
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
70
|
+
let disposed = false;
|
|
90
71
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
render(width: number): string[] {
|
|
103
|
-
const runs = monitor.getRuns();
|
|
104
|
-
if (runs.length === 0) return [];
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
const lines: string[] = [];
|
|
107
|
-
// Tree layout: each top-level agent is a root whose title/activity hang
|
|
108
|
-
// off it as branches; auto-fix chain runs (groupId) become child nodes
|
|
109
|
-
// under their parent root, with a "│" continuation while more siblings
|
|
110
|
-
// follow. Blank lines separate agent blocks so parallel runs don't blur
|
|
111
|
-
// into one wall of text.
|
|
112
|
-
const dim = (t: string): string => theme.fg("dim", t);
|
|
113
|
-
for (let idx = 0; idx < runs.length; idx++) {
|
|
114
|
-
const r = runs[idx];
|
|
115
|
-
const isChain = Boolean(r.groupId);
|
|
116
|
-
const chainContinues = isChain && runs[idx + 1]?.groupId === r.groupId;
|
|
117
|
-
const activity =
|
|
118
|
-
r.activity && isRunActiveStatus(r.status) ? r.activity : undefined;
|
|
119
|
-
const hasActivity = activity !== undefined;
|
|
120
|
-
const icon = statusIcon(r.status, theme);
|
|
121
|
-
// Chain-internal runs (auto-fix worker/reviewer) are child nodes under
|
|
122
|
-
// their parent reviewer. Their relationLabel ("fix round 1") is more
|
|
123
|
-
// distinguishing than the repeated worker/reviewer name.
|
|
124
|
-
const name = isChain ? (r.relationLabel ?? r.agent) : r.agent;
|
|
125
|
-
// Two lines per run: the header row (icon, run id, agent name) and the
|
|
126
|
-
// live activity branch below. The task summary is deliberately not
|
|
127
|
-
// shown — the task lives in the tool result, and the agent name plus
|
|
128
|
-
// what it is doing right now is enough to tell runs apart. The header
|
|
129
|
-
// stays exactly as it was (accent name, dim stats), matching the
|
|
130
|
-
// referenced sub-agent widgets (tintinweb): the running indicator
|
|
131
|
-
// uses the accent color, everything else is quiet.
|
|
132
|
-
if (!isChain && lines.length > 0) lines.push("");
|
|
133
|
-
const nodeBranch = isChain ? (chainContinues ? "├─ " : "└─ ") : "";
|
|
134
|
-
// Content label (task-derived) trails the agent name so concurrent
|
|
135
|
-
// same-agent runs read as what they do, not just their run id. Chain
|
|
136
|
-
// nodes already carry a distinguishing relationLabel.
|
|
137
|
-
const labelPart = !isChain && r.label ? ` ${dim(`· ${r.label}`)}` : "";
|
|
138
|
-
const left = `${dim(nodeBranch)}${icon} ${dim(`#${r.id}`)} ${isChain ? name : theme.fg("accent", theme.bold(name))}${labelPart}`;
|
|
72
|
+
const syncTimer = (): void => {
|
|
73
|
+
if (disposed) return;
|
|
74
|
+
if (hasTickingRun()) {
|
|
75
|
+
if (timer) return;
|
|
76
|
+
timer = setInterval(() => tui.requestRender(), 1_000);
|
|
77
|
+
timer.unref?.();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (timer) clearInterval(timer);
|
|
81
|
+
timer = undefined;
|
|
82
|
+
};
|
|
139
83
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const metaParts = [r.summary, relation, isolation, model, usage, tools, elapsed].filter(Boolean);
|
|
161
|
-
// Running is conveyed by the icon + elapsed; spell out the label only for
|
|
162
|
-
// the other states (ready / done / stopped) so they are unambiguous.
|
|
163
|
-
if (r.status !== "running") metaParts.push(statusLabel(r.status));
|
|
164
|
-
const state = deriveActivityState(r, now);
|
|
165
|
-
if (state) metaParts.push(activityStateLabel(state));
|
|
166
|
-
if (r.annotation) metaParts.push(r.annotation);
|
|
167
|
-
// Metadata trails the header in dim — quiet, never competing with the
|
|
168
|
-
// accent agent name (the same restraint the referenced widgets use).
|
|
169
|
-
// Trailing with a single " · " chain keeps the row compact (no center
|
|
170
|
-
// gap); compactLine clips on overflow, never the right side on its own.
|
|
171
|
-
const right = metaParts.length ? dim(` · ${metaParts.join(" · ")}`) : "";
|
|
172
|
-
lines.push(compactLine(left, right, width));
|
|
84
|
+
const unsubscribe = monitor.subscribe(() => {
|
|
85
|
+
syncTimer();
|
|
86
|
+
tui.requestRender();
|
|
87
|
+
});
|
|
88
|
+
syncTimer();
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
render: (width: number) => formatActiveRunLines(monitor.getRuns(), theme, width),
|
|
92
|
+
invalidate() {},
|
|
93
|
+
dispose() {
|
|
94
|
+
disposed = true;
|
|
95
|
+
unsubscribe();
|
|
96
|
+
if (timer) clearInterval(timer);
|
|
97
|
+
timer = undefined;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
{ placement: "aboveEditor" },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
173
104
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// UI. Chain nodes that still have siblings carry a "│" continuation
|
|
177
|
-
// down to the last one.
|
|
178
|
-
if (hasActivity) {
|
|
179
|
-
const continuation = isChain ? (chainContinues ? "│ " : " ") : "";
|
|
180
|
-
lines.push(truncateToWidth(`${continuation}${dim("└─ ")}${dim(activity)}`, width));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return lines;
|
|
184
|
-
},
|
|
185
|
-
invalidate() {},
|
|
186
|
-
dispose() {
|
|
187
|
-
unsub();
|
|
188
|
-
clearInterval(timer);
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
{ placement: "aboveEditor" },
|
|
193
|
-
);
|
|
194
|
-
});
|
|
105
|
+
export function clearActiveRunsWidget(ctx: Pick<ExtensionContext, "mode" | "ui">): void {
|
|
106
|
+
if (ctx.mode === "tui") ctx.ui.setWidget(SUBAGENTS_WIDGET_ID, undefined);
|
|
195
107
|
}
|
package/src/worktree.ts
CHANGED
|
@@ -238,7 +238,7 @@ export interface WorktreeIsolation {
|
|
|
238
238
|
* attempting to apply the same patch twice. */
|
|
239
239
|
snapshotCheckpoint(): Promise<WorktreeCheckpoint>;
|
|
240
240
|
/** Remove a newly-created continuation that failed before it was dispatched. */
|
|
241
|
-
discard
|
|
241
|
+
discard(): Promise<void>;
|
|
242
242
|
/** Idempotent across stale generations and repeated stop/shutdown paths. */
|
|
243
243
|
finalize(): Promise<WorktreeFinalization>;
|
|
244
244
|
}
|