@narumitw/pi-subagents 3.3.3 → 3.4.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 +74 -25
- package/dist/child-communication-bridge.ts +3 -2
- package/dist/child-communication-bridge.ts.map +1 -1
- package/dist/child-readiness-probe.ts +114 -17
- package/dist/child-readiness-probe.ts.map +2 -2
- package/dist/chunks/{chunk-FY5HIU6Y.ts → chunk-5OXDVXXO.ts} +1 -1
- package/dist/chunks/{chunk-FY5HIU6Y.ts.map → chunk-5OXDVXXO.ts.map} +1 -1
- package/dist/chunks/chunk-B3RWNGDX.ts +192 -0
- package/dist/chunks/chunk-B3RWNGDX.ts.map +7 -0
- package/dist/chunks/{chunk-ZOAFWLDJ.ts → chunk-PRIDTAJL.ts} +4 -16
- package/dist/chunks/chunk-PRIDTAJL.ts.map +7 -0
- package/dist/chunks/{chunk-YMVSJ5GE.ts → chunk-YV2P6XJX.ts} +46 -17
- package/dist/chunks/chunk-YV2P6XJX.ts.map +7 -0
- package/dist/chunks/{menu-KAIUEWL7.ts → menu-2UH4ICLL.ts} +74 -3
- package/dist/chunks/menu-2UH4ICLL.ts.map +7 -0
- package/dist/index.ts +554 -293
- package/dist/index.ts.map +3 -3
- package/docs/attachments.md +9 -3
- package/docs/design-principles.md +3 -1
- package/docs/messaging.md +3 -1
- package/docs/tools.md +15 -1
- package/package.json +3 -3
- package/src/broker-credentials.ts +44 -13
- package/src/child-communication-tools.ts +2 -19
- package/src/child-readiness-probe.ts +144 -18
- package/src/child-readiness.ts +118 -0
- package/src/menu.ts +82 -3
- package/src/message-broker.ts +23 -10
- package/src/process.ts +59 -167
- package/src/resource-attachments.ts +1 -3
- package/src/rpc-line-decoder.ts +61 -0
- package/src/runtime.ts +22 -10
- package/src/settings.ts +2 -2
- package/src/subagents.ts +110 -18
- package/src/tools.ts +83 -37
- package/src/types.ts +1 -0
- package/src/widget-renderer.ts +119 -0
- package/src/widget.ts +245 -121
- package/dist/chunks/chunk-3FCHXUKZ.ts +0 -98
- package/dist/chunks/chunk-3FCHXUKZ.ts.map +0 -7
- package/dist/chunks/chunk-YMVSJ5GE.ts.map +0 -7
- package/dist/chunks/chunk-ZOAFWLDJ.ts.map +0 -7
- package/dist/chunks/menu-KAIUEWL7.ts.map +0 -7
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { EditorStatusCollectionItem } from "@narumitw/pi-tui-kit/editor-status-collection";
|
|
4
|
+
import type { EditorStatusMode } from "@narumitw/pi-tui-kit/editor-status-view";
|
|
5
|
+
import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget";
|
|
6
|
+
import { sanitizeSubagentDisplayText as sanitizeLabel } from "./display-text.js";
|
|
7
|
+
import type { ActiveJobDisplay } from "./runtime.js";
|
|
8
|
+
import { DEFAULT_SUBAGENT_SETTINGS, type SubagentSettings } from "./settings.js";
|
|
9
|
+
|
|
10
|
+
const TASK_PREVIEW_WIDTH = 40;
|
|
11
|
+
// Reserve editor/footer space locally; this does not estimate other widgets' space.
|
|
12
|
+
export const SUBAGENT_WIDGET_RESERVED_ROWS = 8;
|
|
13
|
+
export const SUBAGENT_WIDGET_MAX_ROWS = 10;
|
|
14
|
+
|
|
15
|
+
export interface SubagentWidgetSnapshot {
|
|
16
|
+
jobs: readonly ActiveJobDisplay[];
|
|
17
|
+
items: readonly EditorStatusCollectionItem<ActiveJobDisplay>[];
|
|
18
|
+
settings: SubagentSettings;
|
|
19
|
+
mode: EditorStatusMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function subagentWidgetRowBudget(rows: number): number {
|
|
23
|
+
return Number.isFinite(rows)
|
|
24
|
+
? Math.min(SUBAGENT_WIDGET_MAX_ROWS, Math.max(0, Math.floor(rows) - SUBAGENT_WIDGET_RESERVED_ROWS))
|
|
25
|
+
: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderSubagentWidgetSnapshot(
|
|
29
|
+
snapshot: SubagentWidgetSnapshot,
|
|
30
|
+
theme: Theme,
|
|
31
|
+
width: number,
|
|
32
|
+
maxRows: number,
|
|
33
|
+
): string[] {
|
|
34
|
+
const { jobs, items, settings, mode } = snapshot;
|
|
35
|
+
const budget = Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows)) : 0;
|
|
36
|
+
if (!settings.showWidget || mode === "hidden" || jobs.length === 0 || items.length === 0 || budget < 2) return [];
|
|
37
|
+
const running = jobs.filter((job) => job.state === "running").length;
|
|
38
|
+
const counts = `Subagents · ${jobs.length} active · ${running} running · ${jobs.length - running} queued`;
|
|
39
|
+
const removed = jobs.length - items.length;
|
|
40
|
+
const summary = (shown: number) =>
|
|
41
|
+
`${counts}${jobs.length > shown ? ` · ${jobs.length - shown} not shown${removed ? ` (${removed} removed)` : ""}` : ""}`;
|
|
42
|
+
const bodyRows = budget - 1; // The standard separator consumes one row.
|
|
43
|
+
let body: string[];
|
|
44
|
+
if (mode === "compact" || bodyRows === 1) {
|
|
45
|
+
body = [theme.fg("muted", summary(0))];
|
|
46
|
+
} else {
|
|
47
|
+
const shown = items.slice(0, bodyRows - 1);
|
|
48
|
+
body = [
|
|
49
|
+
theme.fg("muted", shown.length === jobs.length ? `Subagents · ${jobs.length} active` : summary(shown.length)),
|
|
50
|
+
...shown.map(({ value, mode: itemMode }) => renderJob(value, theme, settings, itemMode === "compact")),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
return new EditorStatusWidget({ theme, renderBody: () => body }).render(width);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderSubagentWidget(
|
|
57
|
+
jobs: readonly ActiveJobDisplay[],
|
|
58
|
+
theme: Theme,
|
|
59
|
+
width: number,
|
|
60
|
+
settings: SubagentSettings = DEFAULT_SUBAGENT_SETTINGS,
|
|
61
|
+
maxRows = SUBAGENT_WIDGET_MAX_ROWS,
|
|
62
|
+
mode: EditorStatusMode = "default",
|
|
63
|
+
): string[] {
|
|
64
|
+
return renderSubagentWidgetSnapshot(
|
|
65
|
+
{ jobs, items: jobs.map((value) => ({ id: value.jobId, value, mode: "expanded" })), settings, mode },
|
|
66
|
+
theme,
|
|
67
|
+
width,
|
|
68
|
+
maxRows,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderJob(job: ActiveJobDisplay, theme: Theme, settings: SubagentSettings, compact: boolean): string {
|
|
73
|
+
const running = job.state === "running";
|
|
74
|
+
const symbol = theme.fg(running ? "accent" : "dim", running ? "▶ " : "○ ");
|
|
75
|
+
const state = theme.fg(running ? "accent" : "muted", job.state);
|
|
76
|
+
const jobId = sanitizeLabel(job.jobId);
|
|
77
|
+
const taskText = sanitizeLabel(job.task);
|
|
78
|
+
// Pi's truncation adds ANSI resets; remove them before applying the callback theme.
|
|
79
|
+
const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "…"));
|
|
80
|
+
const labels = [
|
|
81
|
+
...(settings.showJobId && visibleWidth(jobId) > 0 ? [theme.fg("text", jobId)] : []),
|
|
82
|
+
...(visibleWidth(task) > 0 ? [theme.fg("text", task)] : []),
|
|
83
|
+
];
|
|
84
|
+
const hasLabel = labels.length > 0;
|
|
85
|
+
// Keep a readable label when the task has no visible text and IDs are hidden.
|
|
86
|
+
if (!hasLabel) labels.push(state);
|
|
87
|
+
if (compact) return `${symbol}${labels.join(" · ")}${!running && hasLabel ? ` · ${state}` : ""}`;
|
|
88
|
+
const tools = job.tools.length > 0 ? job.tools.map(sanitizeLabel).join(", ") : "none";
|
|
89
|
+
const timeout = job.timeout === undefined ? undefined : formatSeconds(job.timeout);
|
|
90
|
+
const elapsed = formatSeconds(Math.floor(job.elapsedMs / 1_000));
|
|
91
|
+
const timing = running
|
|
92
|
+
? `${elapsed}${timeout === undefined ? "" : ` / ${timeout}`}`
|
|
93
|
+
: timeout === undefined
|
|
94
|
+
? undefined
|
|
95
|
+
: `timeout: ${timeout}`;
|
|
96
|
+
const details = [
|
|
97
|
+
...(job.provider !== undefined ? [`provider: ${sanitizeLabel(job.provider)}`] : []),
|
|
98
|
+
...(job.model !== undefined ? [`model: ${sanitizeLabel(job.model)}`] : []),
|
|
99
|
+
...(job.thinkingLevel !== job.defaultThinkingLevel ? [`thinking: ${sanitizeLabel(job.thinkingLevel)}`] : []),
|
|
100
|
+
...(!running && hasLabel ? ["queued"] : []),
|
|
101
|
+
...(settings.showTiming && timing !== undefined ? [timing] : []),
|
|
102
|
+
...(settings.showTools ? [`tools: ${tools}`] : []),
|
|
103
|
+
];
|
|
104
|
+
return `${symbol}${labels.join(" · ")}${details.length > 0 ? theme.fg("muted", ` · ${details.join(" · ")}`) : ""}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatSeconds(value: number): string {
|
|
108
|
+
if (value < 60) return `${formatNumber(value)}s`;
|
|
109
|
+
const wholeSeconds = Math.floor(value);
|
|
110
|
+
const hours = Math.floor(wholeSeconds / 3_600);
|
|
111
|
+
const minutes = Math.floor((wholeSeconds % 3_600) / 60);
|
|
112
|
+
const seconds = wholeSeconds % 60;
|
|
113
|
+
if (hours > 0) return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
|
|
114
|
+
return `${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatNumber(value: number): string {
|
|
118
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
119
|
+
}
|
package/src/widget.ts
CHANGED
|
@@ -1,161 +1,285 @@
|
|
|
1
|
-
import type { ExtensionContext
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
createEditorStatusCollection,
|
|
4
|
+
type EditorStatusCollection,
|
|
5
|
+
type EditorStatusItemHandle,
|
|
6
|
+
} from "@narumitw/pi-tui-kit/editor-status-collection";
|
|
7
|
+
import { createEditorStatusPublisher, type EditorStatusPublisher } from "@narumitw/pi-tui-kit/editor-status-publisher";
|
|
8
|
+
import {
|
|
9
|
+
createEditorStatusView,
|
|
10
|
+
type EditorStatusMode,
|
|
11
|
+
type EditorStatusTrigger,
|
|
12
|
+
type EditorStatusView,
|
|
13
|
+
} from "@narumitw/pi-tui-kit/editor-status-view";
|
|
5
14
|
import type { ActiveJobDisplay, SubagentRuntime } from "./runtime.js";
|
|
6
15
|
import { DEFAULT_SUBAGENT_SETTINGS, type SubagentSettings } from "./settings.js";
|
|
16
|
+
import {
|
|
17
|
+
renderSubagentWidgetSnapshot,
|
|
18
|
+
type SubagentWidgetSnapshot,
|
|
19
|
+
subagentWidgetRowBudget,
|
|
20
|
+
} from "./widget-renderer.js";
|
|
7
21
|
|
|
22
|
+
export { renderSubagentWidget } from "./widget-renderer.js";
|
|
8
23
|
export const SUBAGENT_WIDGET_KEY = "subagents";
|
|
9
24
|
export const SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
export interface SubagentWidgetControls {
|
|
27
|
+
state(): { mode: EditorStatusMode; pinned: boolean; trigger: EditorStatusTrigger };
|
|
28
|
+
setMode(mode: EditorStatusMode): void;
|
|
29
|
+
setPinned(pinned: boolean): void;
|
|
30
|
+
setTrigger(trigger: EditorStatusTrigger): void;
|
|
31
|
+
resetItems(): void;
|
|
32
|
+
item(jobId: string): EditorStatusItemHandle | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SubagentCyclePhase = "idle" | "maintenance" | "unconfirmed" | "auto-preparing" | "preparing" | "running";
|
|
13
36
|
|
|
14
37
|
export interface SubagentWidgetController {
|
|
15
|
-
start(ctx: ExtensionContext, settings?: SubagentSettings): void;
|
|
38
|
+
start(ctx: ExtensionContext, settings?: SubagentSettings, phase?: SubagentCyclePhase): void;
|
|
16
39
|
update(ctx: ExtensionContext, settings: SubagentSettings): void;
|
|
17
40
|
shutdown(ctx: ExtensionContext): void;
|
|
41
|
+
controls(ctx: ExtensionContext): SubagentWidgetControls | undefined;
|
|
42
|
+
preparing(ctx: ExtensionContext): void;
|
|
43
|
+
automaticCompaction(ctx: ExtensionContext): void;
|
|
44
|
+
clearCycle(ctx: ExtensionContext): void;
|
|
45
|
+
maintenance(ctx: ExtensionContext): void;
|
|
46
|
+
agentStarted(ctx: ExtensionContext): void;
|
|
47
|
+
userMessage(ctx: ExtensionContext): void;
|
|
48
|
+
turnEnded(ctx: ExtensionContext, stopReason: string): void;
|
|
49
|
+
settled(ctx: ExtensionContext, aborted: boolean): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createCollection(): EditorStatusCollection<ActiveJobDisplay> {
|
|
53
|
+
return createEditorStatusCollection({
|
|
54
|
+
maxItems: 8, // Runtime accepts at most eight active jobs.
|
|
55
|
+
// 50 KiB task + 64 tool names (128 UTF-16 units each) + 16 KiB model selection, plus metadata.
|
|
56
|
+
maxContentSize: 128 * 1024,
|
|
57
|
+
measure: (job) =>
|
|
58
|
+
Buffer.byteLength(
|
|
59
|
+
[job.jobId, job.task, job.provider ?? "", job.model ?? "", job.thinkingLevel, ...job.tools].join("\0"),
|
|
60
|
+
"utf8",
|
|
61
|
+
) + 128,
|
|
62
|
+
});
|
|
18
63
|
}
|
|
19
64
|
|
|
20
65
|
export function createSubagentWidgetController(runtime: SubagentRuntime): SubagentWidgetController {
|
|
21
66
|
let activeSession: ExtensionContext["sessionManager"] | undefined;
|
|
67
|
+
let generation = 0;
|
|
22
68
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
23
69
|
let unsubscribeJobs: (() => void) | undefined;
|
|
70
|
+
let publisher: EditorStatusPublisher<SubagentWidgetSnapshot> | undefined;
|
|
71
|
+
let collection: EditorStatusCollection<ActiveJobDisplay> | undefined;
|
|
72
|
+
let view: EditorStatusView | undefined;
|
|
73
|
+
let controls: SubagentWidgetControls | undefined;
|
|
74
|
+
let jobs: readonly ActiveJobDisplay[] = [];
|
|
24
75
|
let publishedValue: string | undefined;
|
|
76
|
+
let cyclePhase: SubagentCyclePhase = "idle";
|
|
77
|
+
let lastOutcome: string | undefined;
|
|
25
78
|
let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
|
|
26
|
-
let clearWidget: (() => void) | undefined;
|
|
27
|
-
|
|
28
|
-
const ownsSession = (ctx: ExtensionContext): boolean => ctx.sessionManager === activeSession;
|
|
29
79
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
const ownsSession = (ctx: ExtensionContext): boolean =>
|
|
81
|
+
activeSession !== undefined && ctx.sessionManager === activeSession;
|
|
82
|
+
const stop = (errors: unknown[] = []): void => {
|
|
83
|
+
try {
|
|
84
|
+
for (const release of [
|
|
85
|
+
() => {
|
|
86
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
87
|
+
},
|
|
88
|
+
() => unsubscribeJobs?.(),
|
|
89
|
+
() => view?.dispose(),
|
|
90
|
+
() => collection?.dispose(),
|
|
91
|
+
// The publisher must still own its key when it attempts to clear it.
|
|
92
|
+
() => publisher?.dispose(),
|
|
93
|
+
]) {
|
|
94
|
+
try {
|
|
95
|
+
release();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
errors.push(error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
generation++;
|
|
102
|
+
activeSession = undefined;
|
|
103
|
+
refreshTimer = undefined;
|
|
104
|
+
unsubscribeJobs = undefined;
|
|
105
|
+
view = undefined;
|
|
106
|
+
collection = undefined;
|
|
107
|
+
publisher = undefined;
|
|
108
|
+
controls = undefined;
|
|
109
|
+
jobs = [];
|
|
110
|
+
publishedValue = undefined;
|
|
111
|
+
cyclePhase = "idle";
|
|
112
|
+
lastOutcome = undefined;
|
|
113
|
+
}
|
|
114
|
+
if (errors.length === 1) throw errors[0];
|
|
115
|
+
if (errors.length > 1) throw new AggregateError(errors, "Subagent widget cleanup failed");
|
|
35
116
|
};
|
|
36
117
|
|
|
37
|
-
const publish = (
|
|
38
|
-
if (
|
|
39
|
-
const
|
|
40
|
-
const value =
|
|
118
|
+
const publish = (): void => {
|
|
119
|
+
if (!view || !collection || !publisher) return;
|
|
120
|
+
const items = collection.snapshot();
|
|
121
|
+
const value = JSON.stringify([settings, view.mode, jobs, items]);
|
|
41
122
|
if (value === publishedValue) return;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ctx.ui.setWidget(
|
|
48
|
-
SUBAGENT_WIDGET_KEY,
|
|
49
|
-
(_tui, theme) => createSubagentWidget(snapshot, theme, preferences),
|
|
50
|
-
WIDGET_OPTIONS,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
123
|
+
publisher.publish(
|
|
124
|
+
settings.showWidget && view.mode !== "hidden" && items.length > 0
|
|
125
|
+
? { jobs, items, settings: { ...settings }, mode: view.mode }
|
|
126
|
+
: undefined,
|
|
127
|
+
);
|
|
53
128
|
publishedValue = value;
|
|
54
129
|
};
|
|
130
|
+
const refresh = (): void => {
|
|
131
|
+
if (!view || !collection) return;
|
|
132
|
+
const next = runtime.activeJobsForDisplay().map((job) => ({ ...job, tools: [...job.tools] }));
|
|
133
|
+
const entries = next.map((job) => ({ id: job.jobId, value: job, revision: semanticRevision(job) }));
|
|
134
|
+
// Capacity validation is atomic; do not change the published owner snapshot on failure.
|
|
135
|
+
collection.replace(entries);
|
|
136
|
+
jobs = next;
|
|
137
|
+
view.revision(JSON.stringify(entries.map(({ id, revision }) => [id, revision])));
|
|
138
|
+
publish();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const finishCycle = (eligible: boolean): void => {
|
|
142
|
+
try {
|
|
143
|
+
view?.settled(eligible);
|
|
144
|
+
} finally {
|
|
145
|
+
cyclePhase = "idle";
|
|
146
|
+
lastOutcome = undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const startCycle = (phase: Exclude<SubagentCyclePhase, "idle" | "maintenance">): void => {
|
|
150
|
+
if (cyclePhase === "idle" || cyclePhase === "maintenance") lastOutcome = undefined;
|
|
151
|
+
cyclePhase = phase;
|
|
152
|
+
view?.agentStarted();
|
|
153
|
+
};
|
|
154
|
+
const syncActiveCycle = (ctx: ExtensionContext): void => {
|
|
155
|
+
if (ctx.isIdle()) {
|
|
156
|
+
if (cyclePhase !== "idle" && cyclePhase !== "running") finishCycle(false);
|
|
157
|
+
} else if (cyclePhase === "idle" || cyclePhase === "maintenance") {
|
|
158
|
+
// Earlier extension handlers can hold preparation before this owner sees
|
|
159
|
+
// it. Public maintenance hooks or positive idle discard this provisional cycle.
|
|
160
|
+
startCycle(ctx.signal === undefined ? "unconfirmed" : "running");
|
|
161
|
+
}
|
|
162
|
+
};
|
|
55
163
|
|
|
56
164
|
return {
|
|
57
|
-
start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS) {
|
|
165
|
+
start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS, phase = "idle") {
|
|
166
|
+
// Dispose while the old publisher still owns its key, then transfer ownership.
|
|
58
167
|
stop();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
168
|
+
const token = generation;
|
|
169
|
+
const isCurrent = () => token === generation && ownsSession(ctx);
|
|
170
|
+
try {
|
|
171
|
+
activeSession = ctx.sessionManager;
|
|
172
|
+
settings = { ...preferences };
|
|
173
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) return;
|
|
174
|
+
collection = createCollection();
|
|
175
|
+
view = createEditorStatusView({ onChange: publish, canCompact: () => settings.showWidget && jobs.length > 0 });
|
|
176
|
+
publisher = createEditorStatusPublisher({
|
|
177
|
+
key: SUBAGENT_WIDGET_KEY,
|
|
178
|
+
context: ctx,
|
|
179
|
+
isCurrent,
|
|
180
|
+
render: (snapshot, theme, width, rows) =>
|
|
181
|
+
renderSubagentWidgetSnapshot(snapshot, theme, width, subagentWidgetRowBudget(rows)),
|
|
182
|
+
});
|
|
183
|
+
// The owner records public preparation/run hooks while settings load.
|
|
184
|
+
// General busy state also includes navigation and manual compaction.
|
|
185
|
+
if (phase === "maintenance") cyclePhase = phase;
|
|
186
|
+
else if (phase !== "idle") startCycle(phase);
|
|
187
|
+
syncActiveCycle(ctx);
|
|
188
|
+
const ownedView = view;
|
|
189
|
+
controls = {
|
|
190
|
+
state: () => ({ mode: ownedView.mode, pinned: ownedView.pinned, trigger: ownedView.trigger }),
|
|
191
|
+
setMode: (mode) => {
|
|
192
|
+
if (isCurrent()) ownedView.setMode(mode);
|
|
193
|
+
},
|
|
194
|
+
setPinned: (pinned) => {
|
|
195
|
+
if (isCurrent()) ownedView.setPinned(pinned);
|
|
196
|
+
},
|
|
197
|
+
setTrigger: (trigger) => {
|
|
198
|
+
if (!isCurrent()) return;
|
|
199
|
+
syncActiveCycle(ctx);
|
|
200
|
+
ownedView.setTrigger(trigger);
|
|
201
|
+
},
|
|
202
|
+
resetItems() {
|
|
203
|
+
if (!isCurrent()) return;
|
|
204
|
+
collection?.dispose();
|
|
205
|
+
collection = createCollection();
|
|
206
|
+
refresh();
|
|
207
|
+
},
|
|
208
|
+
item(jobId) {
|
|
209
|
+
if (!isCurrent()) return undefined;
|
|
210
|
+
const handle = collection?.item(jobId);
|
|
211
|
+
if (!handle) return undefined;
|
|
212
|
+
const apply = (action: "compact" | "expand" | "remove") => {
|
|
213
|
+
if (!isCurrent() || !handle[action]()) return false;
|
|
214
|
+
publish();
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
return { compact: () => apply("compact"), expand: () => apply("expand"), remove: () => apply("remove") };
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
unsubscribeJobs = runtime.subscribeJobs(() => {
|
|
221
|
+
if (isCurrent()) refresh();
|
|
222
|
+
});
|
|
223
|
+
refreshTimer = setInterval(() => {
|
|
224
|
+
if (isCurrent()) refresh();
|
|
225
|
+
}, SUBAGENT_WIDGET_REFRESH_INTERVAL_MS);
|
|
226
|
+
refreshTimer.unref();
|
|
227
|
+
refresh();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
stop([error]);
|
|
230
|
+
}
|
|
71
231
|
},
|
|
72
232
|
update(ctx, preferences) {
|
|
73
233
|
if (!ownsSession(ctx)) return;
|
|
74
234
|
settings = { ...preferences };
|
|
75
|
-
|
|
235
|
+
if (ctx.mode === "tui") refresh();
|
|
76
236
|
},
|
|
77
237
|
shutdown(ctx) {
|
|
78
238
|
if (!ownsSession(ctx)) return;
|
|
79
239
|
stop();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
240
|
+
},
|
|
241
|
+
controls(ctx) {
|
|
242
|
+
return ownsSession(ctx) && ctx.mode === "tui" ? controls : undefined;
|
|
243
|
+
},
|
|
244
|
+
preparing(ctx) {
|
|
245
|
+
if (!ownsSession(ctx) || !view) return;
|
|
246
|
+
// A new preparation closes a failed prior preparation without consuming
|
|
247
|
+
// the rule. Earlier automatic compaction is part of this same run.
|
|
248
|
+
if (cyclePhase !== "auto-preparing" && cyclePhase !== "unconfirmed") finishCycle(false);
|
|
249
|
+
startCycle("preparing");
|
|
250
|
+
},
|
|
251
|
+
automaticCompaction(ctx) {
|
|
252
|
+
if (!ownsSession(ctx) || !view) return;
|
|
253
|
+
if (cyclePhase === "preparing" || cyclePhase === "maintenance") finishCycle(false);
|
|
254
|
+
if (cyclePhase === "idle" || cyclePhase === "unconfirmed") startCycle("auto-preparing");
|
|
255
|
+
},
|
|
256
|
+
clearCycle(ctx) {
|
|
257
|
+
if (ownsSession(ctx)) finishCycle(false);
|
|
258
|
+
},
|
|
259
|
+
maintenance(ctx) {
|
|
260
|
+
if (!ownsSession(ctx)) return;
|
|
261
|
+
finishCycle(false);
|
|
262
|
+
cyclePhase = "maintenance";
|
|
263
|
+
},
|
|
264
|
+
agentStarted(ctx) {
|
|
265
|
+
if (ownsSession(ctx) && view) startCycle("running");
|
|
266
|
+
},
|
|
267
|
+
userMessage(ctx) {
|
|
268
|
+
if (ownsSession(ctx)) view?.userMessage();
|
|
269
|
+
},
|
|
270
|
+
turnEnded(ctx, stopReason) {
|
|
271
|
+
if (ownsSession(ctx) && cyclePhase !== "idle" && cyclePhase !== "maintenance") lastOutcome = stopReason;
|
|
272
|
+
},
|
|
273
|
+
settled(ctx, aborted) {
|
|
274
|
+
if (!ownsSession(ctx) || !view) return;
|
|
275
|
+
// A non-aborted idle boundary alone does not establish a successful response.
|
|
276
|
+
const eligible = !aborted && lastOutcome !== undefined && lastOutcome !== "error" && lastOutcome !== "aborted";
|
|
277
|
+
finishCycle(eligible);
|
|
84
278
|
},
|
|
85
279
|
};
|
|
86
280
|
}
|
|
87
281
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
width: number,
|
|
92
|
-
settings: SubagentSettings = DEFAULT_SUBAGENT_SETTINGS,
|
|
93
|
-
): string[] {
|
|
94
|
-
if (!settings.showWidget) return [];
|
|
95
|
-
return createSubagentWidget(jobs, theme, settings).render(width);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function createSubagentWidget(
|
|
99
|
-
jobs: readonly ActiveJobDisplay[],
|
|
100
|
-
theme: Theme,
|
|
101
|
-
settings: SubagentSettings,
|
|
102
|
-
): EditorStatusWidget {
|
|
103
|
-
return new EditorStatusWidget({
|
|
104
|
-
theme,
|
|
105
|
-
renderBody: () => [
|
|
106
|
-
theme.fg("muted", `Subagents · ${jobs.length} active`),
|
|
107
|
-
...jobs.map((job) => renderJob(job, theme, settings)),
|
|
108
|
-
],
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function renderJob(job: ActiveJobDisplay, theme: Theme, settings: SubagentSettings): string {
|
|
113
|
-
const running = job.state === "running";
|
|
114
|
-
const symbol = theme.fg(running ? "accent" : "dim", running ? "▶ " : "○ ");
|
|
115
|
-
const state = theme.fg(running ? "accent" : "muted", job.state);
|
|
116
|
-
const jobId = sanitizeLabel(job.jobId);
|
|
117
|
-
const taskText = sanitizeLabel(job.task);
|
|
118
|
-
// Pi's truncation adds ANSI resets; remove them before applying the callback theme.
|
|
119
|
-
const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "…"));
|
|
120
|
-
const labels = [
|
|
121
|
-
...(settings.showJobId ? [theme.fg("text", jobId)] : []),
|
|
122
|
-
...(visibleWidth(task) > 0 ? [theme.fg("text", task)] : []),
|
|
123
|
-
state,
|
|
124
|
-
];
|
|
125
|
-
const tools = job.tools.length > 0 ? job.tools.map(sanitizeLabel).join(", ") : "none";
|
|
126
|
-
const timeout = job.timeout === undefined ? "no timeout" : formatSeconds(job.timeout);
|
|
127
|
-
const details = [
|
|
128
|
-
`thinking: ${sanitizeLabel(job.thinkingLevel)}`,
|
|
129
|
-
...(settings.showTiming ? [`${formatSeconds(Math.floor(job.elapsedMs / 1_000))} / ${timeout}`] : []),
|
|
130
|
-
...(settings.showTools ? [`tools: ${tools}`] : []),
|
|
131
|
-
];
|
|
132
|
-
const detail = details.length > 0 ? theme.fg("muted", ` · ${details.join(" · ")}`) : "";
|
|
133
|
-
return `${symbol}${labels.join(" · ")}${detail}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function widgetValue(jobs: readonly ActiveJobDisplay[]): string {
|
|
137
|
-
return jobs
|
|
138
|
-
.map(
|
|
139
|
-
(job) =>
|
|
140
|
-
`${job.jobId}\0${job.task}\0${job.state}\0${job.thinkingLevel}\0${Math.floor(job.elapsedMs / 1_000)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`,
|
|
141
|
-
)
|
|
142
|
-
.join("\n");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function cloneDisplayJob(job: ActiveJobDisplay): ActiveJobDisplay {
|
|
146
|
-
return { ...job, tools: [...job.tools] };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function formatSeconds(value: number): string {
|
|
150
|
-
if (value < 60) return `${formatNumber(value)}s`;
|
|
151
|
-
const wholeSeconds = Math.floor(value);
|
|
152
|
-
const hours = Math.floor(wholeSeconds / 3_600);
|
|
153
|
-
const minutes = Math.floor((wholeSeconds % 3_600) / 60);
|
|
154
|
-
const seconds = wholeSeconds % 60;
|
|
155
|
-
if (hours > 0) return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
|
|
156
|
-
return `${minutes}m${seconds > 0 ? ` ${seconds}s` : ""}`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function formatNumber(value: number): string {
|
|
160
|
-
return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
282
|
+
function semanticRevision(job: ActiveJobDisplay): string {
|
|
283
|
+
const { elapsedMs: _elapsedMs, ...semantic } = job;
|
|
284
|
+
return JSON.stringify(semantic);
|
|
161
285
|
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
-
// @ts-nocheck -- generated entries use .ts extensions for Pi's Jiti loader.
|
|
3
|
-
import {
|
|
4
|
-
sanitizeTerminalText
|
|
5
|
-
} from "./chunk-YMVSJ5GE.ts";
|
|
6
|
-
|
|
7
|
-
// src/attachment-utils.ts
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import { realpathSync } from "node:fs";
|
|
10
|
-
import { readFile, stat } from "node:fs/promises";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
var ATTACHMENT_IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
|
|
13
|
-
function throwInvalidAttachmentIgnoreFile() {
|
|
14
|
-
throw new Error("Subagent attachment ignore files must be regular files.");
|
|
15
|
-
}
|
|
16
|
-
function throwIfAttachmentAborted(signal) {
|
|
17
|
-
if (!signal?.aborted) return;
|
|
18
|
-
const error = new Error("Subagent attachment validation was cancelled.");
|
|
19
|
-
error.name = "AbortError";
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
function isAbortError(error) {
|
|
23
|
-
return error instanceof Error && error.name === "AbortError";
|
|
24
|
-
}
|
|
25
|
-
function prefixIgnorePattern(line, prefix) {
|
|
26
|
-
const trimmed = line.trim();
|
|
27
|
-
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null;
|
|
28
|
-
let pattern = line;
|
|
29
|
-
let negated = false;
|
|
30
|
-
if (pattern.startsWith("!")) {
|
|
31
|
-
negated = true;
|
|
32
|
-
pattern = pattern.slice(1);
|
|
33
|
-
} else if (pattern.startsWith("\\!")) {
|
|
34
|
-
pattern = pattern.slice(1);
|
|
35
|
-
}
|
|
36
|
-
if (pattern.startsWith("/")) pattern = pattern.slice(1);
|
|
37
|
-
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
|
|
38
|
-
return negated ? `!${prefixed}` : prefixed;
|
|
39
|
-
}
|
|
40
|
-
async function addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, signal, accountForFile) {
|
|
41
|
-
const relativeDirectory = path.relative(rootDirectory, directory);
|
|
42
|
-
const prefix = relativeDirectory ? `${toPosixPath(relativeDirectory)}/` : "";
|
|
43
|
-
for (const filename of ATTACHMENT_IGNORE_FILE_NAMES) {
|
|
44
|
-
throwIfAttachmentAborted(signal);
|
|
45
|
-
const ignorePath = path.join(directory, filename);
|
|
46
|
-
let ignoreStats;
|
|
47
|
-
try {
|
|
48
|
-
ignoreStats = await stat(ignorePath);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (isAbortError(error)) throw error;
|
|
51
|
-
throwIfAttachmentAborted(signal);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (!ignoreStats.isFile()) throwInvalidAttachmentIgnoreFile();
|
|
55
|
-
accountForFile(ignorePath, ignoreStats.size);
|
|
56
|
-
try {
|
|
57
|
-
const content = await readFile(ignorePath, { encoding: "utf8", signal });
|
|
58
|
-
throwIfAttachmentAborted(signal);
|
|
59
|
-
const patterns = content.split(/\r?\n/u).map((line) => prefixIgnorePattern(line, prefix)).filter((line) => line !== null);
|
|
60
|
-
if (patterns.length > 0) ignoreMatcher.add(patterns);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (isAbortError(error)) throw error;
|
|
63
|
-
throwIfAttachmentAborted(signal);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function toPosixPath(value) {
|
|
68
|
-
return value.split(path.sep).join("/");
|
|
69
|
-
}
|
|
70
|
-
function toolSourceId(value) {
|
|
71
|
-
return createHash("sha256").update(value).digest("hex");
|
|
72
|
-
}
|
|
73
|
-
function realpath(value, label) {
|
|
74
|
-
try {
|
|
75
|
-
return realpathSync(value);
|
|
76
|
-
} catch {
|
|
77
|
-
throw new Error(`${label} path does not exist: ${sanitizeTerminalText(value).slice(0, 512)}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function isWithin(root, candidate) {
|
|
81
|
-
const relative2 = path.relative(root, candidate);
|
|
82
|
-
return relative2 === "" || !relative2.startsWith(`..${path.sep}`) && relative2 !== ".." && !path.isAbsolute(relative2);
|
|
83
|
-
}
|
|
84
|
-
function isRecord(value) {
|
|
85
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export {
|
|
89
|
-
throwIfAttachmentAborted,
|
|
90
|
-
isAbortError,
|
|
91
|
-
addAttachmentIgnoreRules,
|
|
92
|
-
toPosixPath,
|
|
93
|
-
toolSourceId,
|
|
94
|
-
realpath,
|
|
95
|
-
isWithin,
|
|
96
|
-
isRecord
|
|
97
|
-
};
|
|
98
|
-
//# sourceMappingURL=chunk-3FCHXUKZ.ts.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/attachment-utils.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport { readFile, stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type ignore from \"ignore\";\nimport { sanitizeTerminalText } from \"./message-broker.js\";\n\nconst ATTACHMENT_IGNORE_FILE_NAMES = [\".gitignore\", \".ignore\", \".fdignore\"];\n\nexport type IgnoreMatcher = ReturnType<typeof ignore>;\n\nfunction throwInvalidAttachmentIgnoreFile(): never {\n throw new Error(\"Subagent attachment ignore files must be regular files.\");\n}\n\nexport function throwIfAttachmentAborted(signal: AbortSignal | undefined): void {\n if (!signal?.aborted) return;\n const error = new Error(\"Subagent attachment validation was cancelled.\");\n error.name = \"AbortError\";\n throw error;\n}\n\nexport function isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === \"AbortError\";\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n const trimmed = line.trim();\n if (!trimmed || (trimmed.startsWith(\"#\") && !trimmed.startsWith(\"\\\\#\"))) return null;\n let pattern = line;\n let negated = false;\n if (pattern.startsWith(\"!\")) {\n negated = true;\n pattern = pattern.slice(1);\n } else if (pattern.startsWith(\"\\\\!\")) {\n pattern = pattern.slice(1);\n }\n if (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n const prefixed = prefix ? `${prefix}${pattern}` : pattern;\n return negated ? `!${prefixed}` : prefixed;\n}\n\nexport async function addAttachmentIgnoreRules(\n ignoreMatcher: IgnoreMatcher,\n directory: string,\n rootDirectory: string,\n signal: AbortSignal | undefined,\n accountForFile: (ignorePath: string, bytes: number) => void,\n): Promise<void> {\n const relativeDirectory = path.relative(rootDirectory, directory);\n const prefix = relativeDirectory ? `${toPosixPath(relativeDirectory)}/` : \"\";\n for (const filename of ATTACHMENT_IGNORE_FILE_NAMES) {\n throwIfAttachmentAborted(signal);\n const ignorePath = path.join(directory, filename);\n let ignoreStats: Awaited<ReturnType<typeof stat>>;\n try {\n ignoreStats = await stat(ignorePath);\n } catch (error) {\n if (isAbortError(error)) throw error;\n throwIfAttachmentAborted(signal);\n continue;\n }\n if (!ignoreStats.isFile()) throwInvalidAttachmentIgnoreFile();\n accountForFile(ignorePath, ignoreStats.size);\n try {\n const content = await readFile(ignorePath, { encoding: \"utf8\", signal });\n throwIfAttachmentAborted(signal);\n const patterns = content\n .split(/\\r?\\n/u)\n .map((line) => prefixIgnorePattern(line, prefix))\n .filter((line): line is string => line !== null);\n if (patterns.length > 0) ignoreMatcher.add(patterns);\n } catch (error) {\n if (isAbortError(error)) throw error;\n throwIfAttachmentAborted(signal);\n // Pi ignores unreadable ignore files.\n }\n }\n}\n\nexport function toPosixPath(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nexport function toolSourceId(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nexport function realpath(value: string, label: string): string {\n try {\n return realpathSync(value);\n } catch {\n throw new Error(`${label} path does not exist: ${sanitizeTerminalText(value).slice(0, 512)}`);\n }\n}\n\nexport function isWithin(root: string, candidate: string): boolean {\n const relative = path.relative(root, candidate);\n return relative === \"\" || (!relative.startsWith(`..${path.sep}`) && relative !== \"..\" && !path.isAbsolute(relative));\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["relative"]
|
|
7
|
-
}
|