@jwilger/pi-development-system 0.51.1 → 0.52.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/package.json +1 -1
- package/src/core/test-runner.ts +1 -1
- package/src/subagents/index.ts +60 -144
- package/src/subagents/orch/inherited-tools.ts +3 -3
- package/src/subagents/orch/runtime.ts +1 -1
- package/src/subagents/prefs/agent-import.ts +2 -2
- package/src/subagents/prefs/config.ts +1 -1
- package/src/subagents/prefs/import-discovery.ts +6 -6
- package/src/subagents/prefs/settings.ts +2 -2
- package/src/subagents/ui/agent-navigation-editor.ts +13 -20
- package/src/subagents/ui/live-agent-view.ts +5 -17
- package/src/subagents/ui/thread-tree.ts +9 -3
package/package.json
CHANGED
package/src/core/test-runner.ts
CHANGED
|
@@ -65,7 +65,7 @@ export type ResultLike = {
|
|
|
65
65
|
const NO_TEST_SCRIPT = /Missing script: "?test/;
|
|
66
66
|
/** What npm prints around a missing script; any other line means something else ran or failed. */
|
|
67
67
|
const NPM_MISSING_SCRIPT_NOISE =
|
|
68
|
-
/^(?:npm (?:error|ERR!)\s*)?(?:$|.*Missing script.*|.*To see a list of scripts.*|.*A complete log of this run.*|.*Did you mean.*|.*npm run
|
|
68
|
+
/^(?:npm (?:error|ERR!)\s*)?(?:$|.*Missing script.*|.*To see a list of scripts.*|.*A complete log of this run.*|.*Did you mean.*|.*npm run.*|Command exited with code \d+)$/;
|
|
69
69
|
const onlyMissingScript = (text: string): boolean =>
|
|
70
70
|
NO_TEST_SCRIPT.test(text) &&
|
|
71
71
|
text.split("\n").every((l) => NPM_MISSING_SCRIPT_NOISE.test(l.trim()));
|
package/src/subagents/index.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
|
6
|
-
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
7
|
-
import { ThreadManager } from "./orch/manager.ts";
|
|
8
|
-
import { subagentPrompt } from "./orch/prompt.ts";
|
|
9
|
-
import { createDriverFactory } from "./orch/runtime.ts";
|
|
10
|
-
import { agentTools } from "./orch/tools.ts";
|
|
11
|
-
import {
|
|
12
|
-
IMPORT_REQUEST_PREFIX,
|
|
13
|
-
markImportOffered,
|
|
14
|
-
offerAgentImport,
|
|
15
|
-
} from "./prefs/agent-import.ts";
|
|
16
6
|
import { ConfigStore } from "./prefs/config.ts";
|
|
7
|
+
import { IMPORT_REQUEST_PREFIX, markImportOffered, offerAgentImport } from "./prefs/agent-import.ts";
|
|
8
|
+
import { ThreadManager } from "./orch/manager.ts";
|
|
17
9
|
import { loadManagerSettings } from "./prefs/settings.ts";
|
|
18
|
-
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
19
|
-
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
20
10
|
import { configureAgents } from "./ui/settings-ui.ts";
|
|
21
11
|
import { AgentNavigationController, showAgentStatus } from "./ui/status-ui.ts";
|
|
12
|
+
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
13
|
+
import { createDriverFactory } from "./orch/runtime.ts";
|
|
14
|
+
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
15
|
+
import { subagentPrompt } from "./orch/prompt.ts";
|
|
16
|
+
import { agentTools } from "./orch/tools.ts";
|
|
22
17
|
import { editAgentTypes, updateWidget } from "./ui/ui.ts";
|
|
18
|
+
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
23
19
|
|
|
24
20
|
const REGISTRY_ENTRY = "pi-subagent:registry:v1";
|
|
25
21
|
const ROOT_MAILBOX_ENTRY = "pi-subagent:root-mailbox:v1";
|
|
@@ -85,55 +81,32 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
85
81
|
if (existingFactory && existingFactory !== navigationEditorFactory) return;
|
|
86
82
|
// The host only transfers raw text, not cursor/undo/expanded-paste state.
|
|
87
83
|
if (ctx.ui.getEditorText?.()) return;
|
|
88
|
-
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
|
|
89
|
-
(message)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
? content
|
|
95
|
-
: content
|
|
96
|
-
.filter((block) => block.type === "text")
|
|
97
|
-
.map((block) => block.text)
|
|
98
|
-
.join(""),
|
|
99
|
-
];
|
|
100
|
-
},
|
|
101
|
-
);
|
|
84
|
+
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap((message) => {
|
|
85
|
+
if (message.role !== "user") return [];
|
|
86
|
+
const content = message.content;
|
|
87
|
+
return [typeof content === "string" ? content : content
|
|
88
|
+
.filter((block) => block.type === "text").map((block) => block.text).join("")];
|
|
89
|
+
});
|
|
102
90
|
navigationEditorFactory = (tui, theme, keys) => {
|
|
103
91
|
const editor = new AgentNavigationEditor(tui, theme, keys, {
|
|
104
92
|
// Initial startup hydrates after session_start; replacement editors do not.
|
|
105
93
|
initialHistory: seedHistory ? history : undefined,
|
|
106
94
|
generation: () => navigationGeneration,
|
|
107
|
-
canOpen: () =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
manager
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
thread.path !== "/root" &&
|
|
117
|
-
(thread.state === "starting" || thread.state === "running"),
|
|
118
|
-
),
|
|
119
|
-
openTree: () =>
|
|
120
|
-
manager && context
|
|
121
|
-
? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons)
|
|
122
|
-
: undefined,
|
|
123
|
-
canCollapse: () =>
|
|
124
|
-
manager !== undefined &&
|
|
125
|
-
context?.mode === "tui" &&
|
|
126
|
-
limits.subagentMode !== "off" &&
|
|
127
|
-
!navigation.isOpen &&
|
|
128
|
-
!widgetCollapsed &&
|
|
129
|
-
limits.widgetMode === "full" &&
|
|
130
|
-
widgetThreads().some((thread) => thread.path !== "/root"),
|
|
95
|
+
canOpen: () => manager !== undefined && context?.mode === "tui" &&
|
|
96
|
+
limits.subagentMode !== "off" && !navigation.isOpen &&
|
|
97
|
+
manager.list().some((thread) => thread.path !== "/root" &&
|
|
98
|
+
(thread.state === "starting" || thread.state === "running")),
|
|
99
|
+
openTree: () => manager && context
|
|
100
|
+
? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons) : undefined,
|
|
101
|
+
canCollapse: () => manager !== undefined && context?.mode === "tui" &&
|
|
102
|
+
limits.subagentMode !== "off" && !navigation.isOpen && !widgetCollapsed &&
|
|
103
|
+
limits.widgetMode === "full" && widgetThreads().some((thread) => thread.path !== "/root"),
|
|
131
104
|
collapseWidget: () => {
|
|
132
105
|
widgetCollapsed = true;
|
|
133
106
|
if (context) refreshWidget(context);
|
|
134
107
|
},
|
|
135
|
-
onError: (error) =>
|
|
136
|
-
|
|
108
|
+
onError: (error) => context?.ui.notify(
|
|
109
|
+
error instanceof Error ? error.message : String(error), "error"),
|
|
137
110
|
});
|
|
138
111
|
navigationEditor = editor;
|
|
139
112
|
return editor;
|
|
@@ -147,10 +120,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
147
120
|
// Migration requires the root's file tools and must not delegate source prompts.
|
|
148
121
|
if (limits.subagentMode === "orchestration") {
|
|
149
122
|
if (!firstRun)
|
|
150
|
-
ctx.ui.notify(
|
|
151
|
-
"Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.",
|
|
152
|
-
"info",
|
|
153
|
-
);
|
|
123
|
+
ctx.ui.notify("Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.", "info");
|
|
154
124
|
return;
|
|
155
125
|
}
|
|
156
126
|
if (importInProgress) return;
|
|
@@ -170,15 +140,12 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
170
140
|
if (!manager) throw new Error("Subagent threads are not initialized; start a Pi session first");
|
|
171
141
|
return manager;
|
|
172
142
|
};
|
|
173
|
-
const widgetThreads = () =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
hiddenWidgetThreads.delete(thread.path);
|
|
180
|
-
return !hiddenWidgetThreads.has(thread.path);
|
|
181
|
-
});
|
|
143
|
+
const widgetThreads = () => requireManager().list().filter((thread) => {
|
|
144
|
+
// Resuming a retained session makes it visible for this task, even after it settles again.
|
|
145
|
+
if (thread.state === "starting" || thread.state === "running")
|
|
146
|
+
hiddenWidgetThreads.delete(thread.path);
|
|
147
|
+
return !hiddenWidgetThreads.has(thread.path);
|
|
148
|
+
});
|
|
182
149
|
const requireContext = () => {
|
|
183
150
|
if (!context) throw new Error("No active Pi session");
|
|
184
151
|
return context;
|
|
@@ -220,72 +187,44 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
220
187
|
};
|
|
221
188
|
const pendingSummaries = (ctx: ExtensionContext) => {
|
|
222
189
|
const entries = ctx.sessionManager.getBranch();
|
|
223
|
-
const summarized = new Set(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
190
|
+
const summarized = new Set(entries.flatMap((entry) => {
|
|
191
|
+
if (entry.type !== "custom_message" || entry.customType !== ROOT_SUMMARY_MESSAGE) return [];
|
|
192
|
+
const ids = (entry.details as { mailboxIds?: unknown } | undefined)?.mailboxIds;
|
|
193
|
+
return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
|
|
194
|
+
}));
|
|
230
195
|
return entries.flatMap((entry) => {
|
|
231
196
|
if (entry.type !== "custom" || entry.customType !== ROOT_MAILBOX_ENTRY) return [];
|
|
232
197
|
const notification = entry.data as RootNotification | undefined;
|
|
233
198
|
return notification?.rootSessionId === ctx.sessionManager.getSessionId() &&
|
|
234
|
-
notification.finalRecap === true &&
|
|
235
|
-
typeof notification.content === "string" &&
|
|
199
|
+
notification.finalRecap === true && typeof notification.content === "string" &&
|
|
236
200
|
typeof notification.details?.mailboxId === "string" &&
|
|
237
|
-
!summarized.has(notification.details.mailboxId)
|
|
238
|
-
? [notification]
|
|
239
|
-
: [];
|
|
201
|
+
!summarized.has(notification.details.mailboxId) ? [notification] : [];
|
|
240
202
|
});
|
|
241
203
|
};
|
|
242
204
|
const scheduleSummary = (ctx: ExtensionContext) => {
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
limits.subagentMode === "off" ||
|
|
246
|
-
summaryRunning ||
|
|
247
|
-
suspendingSummaries ||
|
|
248
|
-
summaryTimer !== undefined ||
|
|
249
|
-
ctx.isIdle() === false ||
|
|
250
|
-
!pendingSummaries(ctx).length
|
|
251
|
-
)
|
|
252
|
-
return;
|
|
205
|
+
if (!limits.finalRecap || limits.subagentMode === "off" || summaryRunning || suspendingSummaries ||
|
|
206
|
+
summaryTimer !== undefined || ctx.isIdle() === false || !pendingSummaries(ctx).length) return;
|
|
253
207
|
const currentGeneration = generation;
|
|
254
208
|
// Coalesce siblings finishing together and leave the settlement boundary before
|
|
255
209
|
// starting a new turn. Recheck settings/session/idle state after the deferral.
|
|
256
210
|
summaryTimer = setTimeout(() => {
|
|
257
211
|
summaryTimer = undefined;
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
context?.sessionManager !== ctx.sessionManager ||
|
|
261
|
-
!limits.finalRecap ||
|
|
262
|
-
limits.subagentMode === "off" ||
|
|
263
|
-
suspendingSummaries ||
|
|
264
|
-
ctx.isIdle() === false
|
|
265
|
-
)
|
|
266
|
-
return;
|
|
212
|
+
if (currentGeneration !== generation || context?.sessionManager !== ctx.sessionManager ||
|
|
213
|
+
!limits.finalRecap || limits.subagentMode === "off" || suspendingSummaries || ctx.isIdle() === false) return;
|
|
267
214
|
const notifications = pendingSummaries(ctx);
|
|
268
215
|
if (!notifications.length) return;
|
|
269
216
|
summaryRunning = true;
|
|
270
217
|
try {
|
|
271
|
-
pi.sendMessage(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
),
|
|
282
|
-
display: false,
|
|
283
|
-
details: {
|
|
284
|
-
mailboxIds: notifications.map((notification) => notification.details.mailboxId),
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
{ triggerTurn: true },
|
|
288
|
-
);
|
|
218
|
+
pi.sendMessage({
|
|
219
|
+
customType: ROOT_SUMMARY_MESSAGE,
|
|
220
|
+
content: "Summarize the newly finished asynchronous subagent results for the user. " +
|
|
221
|
+
"Their results are in the preceding subagent notifications; use agent_output if more detail is needed. " +
|
|
222
|
+
"Give a concise main-thread answer covering results and any failures. " +
|
|
223
|
+
"Do not spawn or resume agents solely to summarize.\nAgents: " +
|
|
224
|
+
[...new Set(notifications.map((notification) => notification.details.path))].join(", "),
|
|
225
|
+
display: false,
|
|
226
|
+
details: { mailboxIds: notifications.map((notification) => notification.details.mailboxId) },
|
|
227
|
+
}, { triggerTurn: true });
|
|
289
228
|
} catch (error) {
|
|
290
229
|
summaryRunning = false;
|
|
291
230
|
warnDelivery(error);
|
|
@@ -339,10 +278,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
339
278
|
content: message,
|
|
340
279
|
// Foreground results already handled by a busy main thread do not need
|
|
341
280
|
// another turn. Completions during a summary do need a follow-up summary.
|
|
342
|
-
finalRecap:
|
|
343
|
-
limits.finalRecap &&
|
|
344
|
-
!suspendingSummaries &&
|
|
345
|
-
event.kind === "settled" &&
|
|
281
|
+
finalRecap: limits.finalRecap && !suspendingSummaries && event.kind === "settled" &&
|
|
346
282
|
(thread.state === "completed" || thread.state === "failed") &&
|
|
347
283
|
(requireContext().isIdle() || summaryRunning),
|
|
348
284
|
details: {
|
|
@@ -399,14 +335,8 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
399
335
|
const refreshWidget = (ctx: ExtensionContext) => {
|
|
400
336
|
if (limits.subagentMode === "off") {
|
|
401
337
|
if (ctx.hasUI) ctx.ui.setWidget("pi-subagent", undefined);
|
|
402
|
-
} else
|
|
403
|
-
|
|
404
|
-
ctx,
|
|
405
|
-
widgetThreads(),
|
|
406
|
-
widgetCollapsed ? "minimal" : limits.widgetMode,
|
|
407
|
-
limits.nerdFontIcons,
|
|
408
|
-
rootTurnEnded,
|
|
409
|
-
);
|
|
338
|
+
} else updateWidget(ctx, widgetThreads(), widgetCollapsed ? "minimal" : limits.widgetMode,
|
|
339
|
+
limits.nerdFontIcons, rootTurnEnded);
|
|
410
340
|
};
|
|
411
341
|
pi.on("agent_start", async (_event, ctx) => {
|
|
412
342
|
// A new task must not resurrect the previous task's settled agents. Automatic
|
|
@@ -444,11 +374,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
444
374
|
};
|
|
445
375
|
});
|
|
446
376
|
|
|
447
|
-
const attachSession = async (
|
|
448
|
-
ctx: ExtensionContext,
|
|
449
|
-
installEditor = false,
|
|
450
|
-
seedHistory = true,
|
|
451
|
-
) => {
|
|
377
|
+
const attachSession = async (ctx: ExtensionContext, installEditor = false, seedHistory = true) => {
|
|
452
378
|
cancelSummary();
|
|
453
379
|
const token = ++generation;
|
|
454
380
|
closeNavigation(installEditor);
|
|
@@ -605,19 +531,9 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
605
531
|
},
|
|
606
532
|
});
|
|
607
533
|
} else if (command === "status") {
|
|
608
|
-
await showAgentStatus(
|
|
609
|
-
ctx,
|
|
610
|
-
requireManager().scope("/root"),
|
|
611
|
-
undefined,
|
|
612
|
-
limits.nerdFontIcons,
|
|
613
|
-
);
|
|
534
|
+
await showAgentStatus(ctx, requireManager().scope("/root"), undefined, limits.nerdFontIcons);
|
|
614
535
|
} else if (command === "tree") {
|
|
615
|
-
await navigation.open(
|
|
616
|
-
ctx,
|
|
617
|
-
requireManager().scope("/root"),
|
|
618
|
-
rest.join(" ") || undefined,
|
|
619
|
-
limits.nerdFontIcons,
|
|
620
|
-
);
|
|
536
|
+
await navigation.open(ctx, requireManager().scope("/root"), rest.join(" ") || undefined, limits.nerdFontIcons);
|
|
621
537
|
} else
|
|
622
538
|
ctx.ui.notify(
|
|
623
539
|
"Usage: /agents [tree [path] | status | settings | types | import | reload]",
|
|
@@ -43,7 +43,7 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
|
|
|
43
43
|
for (const tool of loadout.registered) {
|
|
44
44
|
const exposure = loadout.getExposure(tool.name);
|
|
45
45
|
// Inactive direct tools and hidden tools are not available in the root either.
|
|
46
|
-
if (!
|
|
46
|
+
if (!callable.has(tool.name) && !active.has(tool.name)) continue;
|
|
47
47
|
if (exposure === "hidden") continue;
|
|
48
48
|
const metadata = info.get(tool.name);
|
|
49
49
|
tools.push({
|
|
@@ -64,14 +64,14 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
|
|
|
64
64
|
// SDK orchestrators are instead recreated locally by runtime.
|
|
65
65
|
throw new Error(
|
|
66
66
|
`Cannot safely inherit model-only tool ${tool.name}: the SDK does not ` +
|
|
67
|
-
|
|
67
|
+
"provide a hook-preserving execution bridge. Run it in the main session.",
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
const bridge = rootContext ?? context;
|
|
71
71
|
if (!bridge) {
|
|
72
72
|
throw new Error(
|
|
73
73
|
"Inherited tool execution needs a main-session agent_* tool call first. " +
|
|
74
|
-
|
|
74
|
+
"Use agent_steer to resume this child instead of the thread dialog.",
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
const outcome = await bridge.executeTool(tool.name, args, { signal, onUpdate });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity:
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: the flagged functions (driver construction, settings resolution) are long sequences over pi sessions; only the pin and resume paths are covered by test/subagents/spawn-pins.test.ts, so splitting them would move untested behaviour (see src/subagents/VENDORED.md)
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -159,8 +159,8 @@ export async function offerAgentImport(
|
|
|
159
159
|
cwd: ctx.cwd,
|
|
160
160
|
agentDir: options.agentDir,
|
|
161
161
|
includeProject: ctx.isProjectTrusted(),
|
|
162
|
-
homeDir: options.homeDir,
|
|
163
|
-
extraAgentDirs: options.extraAgentDirs,
|
|
162
|
+
...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
|
|
163
|
+
...(options.extraAgentDirs === undefined ? {} : { extraAgentDirs: options.extraAgentDirs }),
|
|
164
164
|
});
|
|
165
165
|
if (found.diagnostics.length > 0)
|
|
166
166
|
ctx.ui.notify(dialogText(found.diagnostics.join("\n")), "warning");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity:
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: agent-file parsing and layer merging are long validators kept close to upstream; only parseAgentType is covered (test/agents/agents.test.ts), so splitting the rest would move untested behaviour (see src/subagents/VENDORED.md)
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
lstatSync,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync, type Dirent, type Stats } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
@@ -17,8 +17,8 @@ export interface DiscoverImportOptions {
|
|
|
17
17
|
cwd: string;
|
|
18
18
|
agentDir: string;
|
|
19
19
|
includeProject: boolean;
|
|
20
|
-
homeDir?: string
|
|
21
|
-
extraAgentDirs?: string
|
|
20
|
+
homeDir?: string;
|
|
21
|
+
extraAgentDirs?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
@@ -227,7 +227,7 @@ function walk(
|
|
|
227
227
|
continue;
|
|
228
228
|
}
|
|
229
229
|
if (isSkillMarkdownName(entry.name)) continue;
|
|
230
|
-
if (!
|
|
230
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.endsWith(".chain.md")) {
|
|
231
231
|
continue;
|
|
232
232
|
}
|
|
233
233
|
if (isExcluded(child, excludes) || isManagerOwned(child, managerDirs)) continue;
|
|
@@ -270,7 +270,7 @@ function considerFile(
|
|
|
270
270
|
const rawName = fields.name?.trim() ?? "";
|
|
271
271
|
const name = rawName || (permissive ? filenameName(resolved) : "");
|
|
272
272
|
const description = fields.description ?? (permissive ? "" : undefined);
|
|
273
|
-
if (!name || description === undefined || !
|
|
273
|
+
if (!name || description === undefined || (!permissive && !description.trim())) return;
|
|
274
274
|
const packageName = fields.packageName?.trim() ?? "";
|
|
275
275
|
candidates.push({
|
|
276
276
|
id: resolved,
|
|
@@ -303,7 +303,7 @@ function looseDisplayFields(content: string): DisplayFields | undefined {
|
|
|
303
303
|
/* Ambiguous display metadata is not selectable. */
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
if (!
|
|
306
|
+
if (!fields.name || !fields.description) return undefined;
|
|
307
307
|
return { name: fields.name, description: fields.description, packageName: fields.package };
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream
|
|
2
|
-
// biome-ignore-all lint/suspicious/noShadow: vendored upstream
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream settings-file loader with no tests of its own; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/suspicious/noShadow: vendored upstream settings-file loader with no tests of its own; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import {
|
|
5
5
|
closeSync,
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
type KeybindingsManager,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import {
|
|
8
|
-
type EditorTheme,
|
|
9
8
|
getKeybindings,
|
|
10
9
|
Key,
|
|
11
|
-
type Keybinding,
|
|
12
10
|
matchesKey,
|
|
11
|
+
type EditorTheme,
|
|
12
|
+
type Keybinding,
|
|
13
13
|
type TUI,
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
@@ -87,11 +87,8 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
87
87
|
|
|
88
88
|
override handleInput(data: string): void {
|
|
89
89
|
const outer = this.dispatchDepth === 0;
|
|
90
|
-
const direction = this.ordinaryArrow(data, "left")
|
|
91
|
-
? "
|
|
92
|
-
: this.ordinaryArrow(data, "right")
|
|
93
|
-
? "right"
|
|
94
|
-
: undefined;
|
|
90
|
+
const direction = this.ordinaryArrow(data, "left") ? "left"
|
|
91
|
+
: this.ordinaryArrow(data, "right") ? "right" : undefined;
|
|
95
92
|
if (outer && (!direction || (this.pending && this.pending !== direction))) this.inputEpoch++;
|
|
96
93
|
const previousPaste = this.pastePending;
|
|
97
94
|
const wasPaste = previousPaste || data.includes("\x1b[200~");
|
|
@@ -103,12 +100,10 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
103
100
|
const cursor = this.getCursor();
|
|
104
101
|
const text = this.getText();
|
|
105
102
|
const lines = text.split("\n");
|
|
106
|
-
const atBoundary =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
cursor.line === lines.length - 1 &&
|
|
111
|
-
cursor.col === lines.at(-1)!.length;
|
|
103
|
+
const atBoundary = direction === "left"
|
|
104
|
+
? cursor.line === 0 && cursor.col === 0
|
|
105
|
+
: direction === "right" && cursor.line === lines.length - 1 &&
|
|
106
|
+
cursor.col === lines.at(-1)!.length;
|
|
112
107
|
const eligible =
|
|
113
108
|
outer &&
|
|
114
109
|
direction !== undefined &&
|
|
@@ -175,10 +170,9 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
175
170
|
generation: unknown,
|
|
176
171
|
direction: "left" | "right",
|
|
177
172
|
): void {
|
|
178
|
-
const allowed = () =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
: !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
|
|
173
|
+
const allowed = () => direction === "left"
|
|
174
|
+
? this.navigation.canOpen()
|
|
175
|
+
: !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
|
|
182
176
|
if (this.pending || !allowed()) return;
|
|
183
177
|
this.pending = direction;
|
|
184
178
|
const epoch = this.inputEpoch;
|
|
@@ -201,9 +195,8 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
201
195
|
try {
|
|
202
196
|
// Call within the checked microtask: an extra Promise.then would leave
|
|
203
197
|
// a gap in which root ownership could change after validation.
|
|
204
|
-
void Promise.resolve(
|
|
205
|
-
|
|
206
|
-
)
|
|
198
|
+
void Promise.resolve(direction === "left"
|
|
199
|
+
? this.navigation.openTree() : this.navigation.collapseWidget!())
|
|
207
200
|
.catch((error: unknown) => this.navigation.onError?.(error))
|
|
208
201
|
.finally(() => {
|
|
209
202
|
this.pending = undefined;
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
wrapTextWithAnsi,
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
import type { ThreadService, TranscriptSnapshot } from "../types.ts";
|
|
16
|
-
import { type DialogHost
|
|
16
|
+
import { dialogText, type DialogHost } from "./dialog.ts";
|
|
17
17
|
import { AGENT_PROGRESS_INTERVAL, agentProgressIcon, agentTypeLabel } from "./ui.ts";
|
|
18
18
|
|
|
19
19
|
/** Transcript detail level cycled by the t key: full → preview → compact. */
|
|
@@ -66,10 +66,7 @@ export class LiveAgentView {
|
|
|
66
66
|
private unsubscribe: (() => void) | undefined;
|
|
67
67
|
private renderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
68
|
private progressTimer: ReturnType<typeof setInterval> | undefined;
|
|
69
|
-
private cache = new WeakMap<
|
|
70
|
-
object,
|
|
71
|
-
{ width: number; detail: TranscriptDetail; lines: string[] }
|
|
72
|
-
>();
|
|
69
|
+
private cache = new WeakMap<object, { width: number; detail: TranscriptDetail; lines: string[] }>();
|
|
73
70
|
private scroll: ScrollView;
|
|
74
71
|
private content: string[] = [];
|
|
75
72
|
private restoreViewport = true;
|
|
@@ -191,10 +188,7 @@ export class LiveAgentView {
|
|
|
191
188
|
|
|
192
189
|
private syncProgress(): void {
|
|
193
190
|
const state = this.snapshot?.thread?.state;
|
|
194
|
-
const active =
|
|
195
|
-
this.nerdFontIcons &&
|
|
196
|
-
!this.disposed &&
|
|
197
|
-
!this.error &&
|
|
191
|
+
const active = this.nerdFontIcons && !this.disposed && !this.error &&
|
|
198
192
|
(state === "starting" || state === "running");
|
|
199
193
|
if (!active) {
|
|
200
194
|
if (this.progressTimer !== undefined) clearInterval(this.progressTimer);
|
|
@@ -339,11 +333,7 @@ export class LiveAgentView {
|
|
|
339
333
|
if (this.detail === "compact")
|
|
340
334
|
return [this.theme.fg("dim", "[Thinking hidden · t cycles view]")];
|
|
341
335
|
// Thinking is model-authored Markdown; preview truncates the rendered lines, not the source.
|
|
342
|
-
const rendered = this.markdown(
|
|
343
|
-
String(block.thinking ?? "[redacted thinking]"),
|
|
344
|
-
width,
|
|
345
|
-
true,
|
|
346
|
-
);
|
|
336
|
+
const rendered = this.markdown(String(block.thinking ?? "[redacted thinking]"), width, true);
|
|
347
337
|
if (this.detail === "full" || rendered.length <= 3) return rendered;
|
|
348
338
|
return [...rendered.slice(0, 3), this.theme.fg("dim", "...")];
|
|
349
339
|
}
|
|
@@ -459,9 +449,7 @@ export class LiveAgentView {
|
|
|
459
449
|
}
|
|
460
450
|
case "ls": {
|
|
461
451
|
const limit = num("limit");
|
|
462
|
-
lines = [
|
|
463
|
-
`${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`,
|
|
464
|
-
];
|
|
452
|
+
lines = [`${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`];
|
|
465
453
|
break;
|
|
466
454
|
}
|
|
467
455
|
default:
|
|
@@ -25,7 +25,11 @@ function comparePath(a: string, b: string): number {
|
|
|
25
25
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function chainHas(
|
|
28
|
+
function chainHas(
|
|
29
|
+
parentOf: Map<string, string | null>,
|
|
30
|
+
start: string,
|
|
31
|
+
target: string,
|
|
32
|
+
): boolean {
|
|
29
33
|
const seen = new Set<string>();
|
|
30
34
|
let current: string | null = start;
|
|
31
35
|
while (current) {
|
|
@@ -46,7 +50,8 @@ export function buildStatusTree(
|
|
|
46
50
|
): StatusRow[] {
|
|
47
51
|
const threadsByPath = new Map<string, ThreadView>();
|
|
48
52
|
for (const thread of threads) {
|
|
49
|
-
if (thread?.path && !threadsByPath.has(thread.path))
|
|
53
|
+
if (thread?.path && !threadsByPath.has(thread.path))
|
|
54
|
+
threadsByPath.set(thread.path, thread);
|
|
50
55
|
}
|
|
51
56
|
const nodes = new Map<string, StatusNode>();
|
|
52
57
|
const parentOf = new Map<string, string | null>();
|
|
@@ -61,7 +66,8 @@ export function buildStatusTree(
|
|
|
61
66
|
const declaredParent = (path: string): string | null => {
|
|
62
67
|
if (path === ROOT) return null;
|
|
63
68
|
const thread = threadsByPath.get(path);
|
|
64
|
-
if (thread)
|
|
69
|
+
if (thread)
|
|
70
|
+
return thread.parent && thread.parent !== path ? thread.parent : null;
|
|
65
71
|
return lexicalParent(path);
|
|
66
72
|
};
|
|
67
73
|
const ensure = (path: string, seen = new Set<string>()): void => {
|