@gavin-hu/my-pi-agent 0.21.0 → 0.22.0
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 +46 -31
- package/README.zh-CN.md +40 -26
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +49 -39
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -7
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/view/todo-screen.ts +122 -0
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +27 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +1 -1
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -22,7 +22,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
22
22
|
import { claimPathMessage, resolveClaimPaths } from "./resolve.ts";
|
|
23
23
|
import { realPathOfNearest } from "../../lib/path.ts";
|
|
24
24
|
import { repoKeyFor } from "../../lib/repo-key.ts";
|
|
25
|
-
import { sessionNameOf } from "../../lib/session-label.ts";
|
|
25
|
+
import { sessionNameOf, SESSION_LABEL_CHANNEL } from "../../lib/session-label.ts";
|
|
26
26
|
import { defaultInterval, type RepeatTimer } from "../../lib/timer.ts";
|
|
27
27
|
import {
|
|
28
28
|
defaultLabel,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from "./protocol.ts";
|
|
40
40
|
import { createMailWatch, type WatchingState } from "./mail-watch.ts";
|
|
41
41
|
import { createBusStore, releaseClaimed, type BusStore } from "./store.ts";
|
|
42
|
-
import type { BusMessage, ClaimConflict, SessionRecord, SessionStatus } from "./types.ts";
|
|
42
|
+
import type { BusMessage, ClaimConflict, InboxPeek, SessionRecord, SessionStatus } from "./types.ts";
|
|
43
43
|
import {
|
|
44
44
|
BUS_DIR_NAME,
|
|
45
45
|
type RosterSeams,
|
|
@@ -131,9 +131,6 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
131
131
|
const interval = options.interval ?? defaultInterval;
|
|
132
132
|
const pid = options.pid ?? process.pid;
|
|
133
133
|
const seams: RosterSeams = { alive: options.alive ?? isPidAlive, token: options.startTokenFn ?? startTokenOf };
|
|
134
|
-
// Asked once, here: a reused pid is settled from the record afterwards, so no
|
|
135
|
-
// heartbeat tick spawns `ps`.
|
|
136
|
-
const processToken = (seams.token ?? startTokenOf)(pid);
|
|
137
134
|
|
|
138
135
|
/**
|
|
139
136
|
* What one record says about its session.
|
|
@@ -161,7 +158,8 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
161
158
|
|
|
162
159
|
/** The watch reads this session's own inbox; nothing is consumed or marked. */
|
|
163
160
|
const mailWatch = createMailWatch({
|
|
164
|
-
peek: async () =>
|
|
161
|
+
peek: async (known): Promise<InboxPeek> =>
|
|
162
|
+
store && record ? store.peek(record.id, { limit: MAX_LIMIT, known }) : { mail: [], unread: new Set() },
|
|
165
163
|
interval,
|
|
166
164
|
watchMs,
|
|
167
165
|
});
|
|
@@ -178,6 +176,10 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
178
176
|
await store.ensure();
|
|
179
177
|
const id = context.sessionManager.getSessionId();
|
|
180
178
|
const mode = sessionMode(context.mode);
|
|
179
|
+
// Asked once, when the bus starts: a reused pid is settled from the record
|
|
180
|
+
// afterwards, so no heartbeat tick spawns `ps`, and a session that never
|
|
181
|
+
// joins pays nothing in its factory.
|
|
182
|
+
const processToken = (seams.token ?? startTokenOf)(pid);
|
|
181
183
|
record = {
|
|
182
184
|
id,
|
|
183
185
|
label: defaultLabel(sessionNameOf(context), cwd, id),
|
|
@@ -208,6 +210,7 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
208
210
|
record.lastSeen = now();
|
|
209
211
|
await store.writeSession(record);
|
|
210
212
|
await settleLabel();
|
|
213
|
+
publishLabel();
|
|
211
214
|
joined = true;
|
|
212
215
|
armHeartbeat();
|
|
213
216
|
}
|
|
@@ -225,7 +228,7 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
225
228
|
await store.writeSession(record);
|
|
226
229
|
// A heartbeat that resumed after a long pause can find its label taken; the
|
|
227
230
|
// tick is where a revived session notices and yields, after its own write.
|
|
228
|
-
await settleLabel();
|
|
231
|
+
if (await settleLabel()) publishLabel();
|
|
229
232
|
})().catch(() => {});
|
|
230
233
|
});
|
|
231
234
|
}
|
|
@@ -248,18 +251,35 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
248
251
|
* that keeps the label does not look again, and two of them can each decide that.
|
|
249
252
|
* Every writer that observes a rival yields, and the record written last always
|
|
250
253
|
* observes the other, so a collision ends with two distinct labels.
|
|
254
|
+
*
|
|
255
|
+
* Answers whether this call yielded, which is the only thing that can change a label
|
|
256
|
+
* after it is settled — the heartbeat tick is where that happens.
|
|
251
257
|
*/
|
|
252
|
-
async function settleLabel(): Promise<
|
|
253
|
-
if (!store || !record) return;
|
|
258
|
+
async function settleLabel(): Promise<boolean> {
|
|
259
|
+
if (!store || !record) return false;
|
|
254
260
|
const me = record;
|
|
255
261
|
let yields = 0;
|
|
256
262
|
while (yields < 3 && (await takenLabels(store, me.id)).has(me.label.toLowerCase())) {
|
|
257
263
|
me.label = suffixLabel(me.label, me.id);
|
|
258
264
|
yields += 1;
|
|
259
265
|
}
|
|
260
|
-
if (yields === 0) return;
|
|
266
|
+
if (yields === 0) return false;
|
|
261
267
|
me.lastSeen = now();
|
|
262
268
|
await store.writeSession(me);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Tell the footer what peers address this session by.
|
|
274
|
+
*
|
|
275
|
+
* Nothing else can say what a label is: the bus settles it, yields it on a collision, and
|
|
276
|
+
* gives it to a caller that asked for a name. The footer draws it in place of Pi's own
|
|
277
|
+
* session name, which the terminal knows but a card, a handoff, and `/team` do not.
|
|
278
|
+
*/
|
|
279
|
+
function publishLabel(): void {
|
|
280
|
+
if (!record) return;
|
|
281
|
+
// A host with no event bus has no listener: the footer it would feed keeps Pi's own name.
|
|
282
|
+
pi.events?.emit(SESSION_LABEL_CHANNEL, { id: record.id, label: record.label });
|
|
263
283
|
}
|
|
264
284
|
|
|
265
285
|
/**
|
|
@@ -376,6 +396,7 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
376
396
|
// A rename that raced another one ends on the label the bus settled on, which
|
|
377
397
|
// is what the caller hands to Pi as this session's name.
|
|
378
398
|
await settleLabel();
|
|
399
|
+
publishLabel();
|
|
379
400
|
joined = true;
|
|
380
401
|
armHeartbeat();
|
|
381
402
|
return record.label;
|
|
@@ -458,7 +479,7 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
458
479
|
|
|
459
480
|
async peek() {
|
|
460
481
|
if (!store || !record) throw new Error("session bus is not started.");
|
|
461
|
-
return store.peek(record.id, MAX_LIMIT);
|
|
482
|
+
return (await store.peek(record.id, { limit: MAX_LIMIT })).mail;
|
|
462
483
|
},
|
|
463
484
|
|
|
464
485
|
/**
|
|
@@ -482,7 +503,14 @@ export function createSessionBus(pi: ExtensionAPI, options: SessionBusOptions =
|
|
|
482
503
|
if ("error" in text) throw new Error(text.error);
|
|
483
504
|
const kind = normalizeKind(input.kind);
|
|
484
505
|
if (input.to.trim() === "*" && kind !== "event") throw new Error('a broadcast needs kind: "event".');
|
|
485
|
-
|
|
506
|
+
// Mail reaches a session that can still answer, by the rule `status` and the prune
|
|
507
|
+
// already use: a record whose process is gone is not a recipient, so naming it is
|
|
508
|
+
// refused instead of confirmed, and a broadcast skips it. This session is always its
|
|
509
|
+
// own recipient, which is what a note to itself is.
|
|
510
|
+
const at = now();
|
|
511
|
+
const sessions = (await store.sessions()).filter(
|
|
512
|
+
(session) => session.id === me.id || stateOf(session, at) !== "gone",
|
|
513
|
+
);
|
|
486
514
|
const resolved = resolveRecipients(sessions, input.to, me.id);
|
|
487
515
|
if ("error" in resolved) throw new Error(resolved.error);
|
|
488
516
|
let paths: string[] | undefined;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
-
import { mkdir, link, readdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
11
|
+
import { mkdir, link, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { asRecord, readJson } from "../../lib/config.ts";
|
|
14
14
|
import { claimsConflict } from "./overlap.ts";
|
|
15
|
-
import type { BusMessage, ClaimConflict, ClaimRecord, SessionRecord } from "./types.ts";
|
|
15
|
+
import type { BusMessage, ClaimConflict, ClaimRecord, InboxPeek, SessionRecord } from "./types.ts";
|
|
16
16
|
import { INBOX_DIR_NAME, SESSIONS_DIR_NAME } from "../../lib/session-roster.ts";
|
|
17
17
|
|
|
18
18
|
const DIR_MODE = 0o700;
|
|
@@ -32,8 +32,11 @@ export interface BusStore {
|
|
|
32
32
|
session(id: string): Promise<SessionRecord | undefined>;
|
|
33
33
|
removeSession(id: string): Promise<void>;
|
|
34
34
|
writeMessage(target: string, message: BusMessage): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Read up to `limit` messages from the front of a recipient's inbox without consuming
|
|
37
|
+
* them, skipping the ids the caller already has, with every id still unread.
|
|
38
|
+
*/
|
|
39
|
+
peek(id: string, options: { limit: number; known?: ReadonlySet<string> }): Promise<InboxPeek>;
|
|
37
40
|
inbox(id: string, limit: number): Promise<BusMessage[]>;
|
|
38
41
|
unread(id: string): Promise<number>;
|
|
39
42
|
claims(): Promise<ClaimRecord[]>;
|
|
@@ -42,6 +45,8 @@ export interface BusStore {
|
|
|
42
45
|
owner: string,
|
|
43
46
|
ownerLabel: string,
|
|
44
47
|
at: number,
|
|
48
|
+
/** Test seam: runs after each path is published, so a suite can interleave a rival. */
|
|
49
|
+
afterClaim?: (path: string) => Promise<void>,
|
|
45
50
|
): Promise<{ claimed: string[]; conflicts: ClaimConflict[] }>;
|
|
46
51
|
release(path: string, owner: string): Promise<boolean>;
|
|
47
52
|
releaseAll(owner: string): Promise<string[]>;
|
|
@@ -148,14 +153,19 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
const MESSAGE_SUFFIX = ".json";
|
|
157
|
+
|
|
151
158
|
async function listFiles(dir: string): Promise<string[]> {
|
|
152
159
|
try {
|
|
153
|
-
return (await readdir(dir)).filter((name) => name.endsWith(
|
|
160
|
+
return (await readdir(dir)).filter((name) => name.endsWith(MESSAGE_SUFFIX)).sort();
|
|
154
161
|
} catch {
|
|
155
162
|
return [];
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
/** The message id a file name holds, which is the id a reader compares against. */
|
|
167
|
+
const idOfFile = (name: string): string => name.slice(0, -MESSAGE_SUFFIX.length);
|
|
168
|
+
|
|
159
169
|
/**
|
|
160
170
|
* Drop a recipient's directory once it holds no files, so a bus does not keep
|
|
161
171
|
* one empty directory for every session that ever joined it. An inbox with mail
|
|
@@ -168,6 +178,32 @@ async function dropIfEmpty(dir: string): Promise<void> {
|
|
|
168
178
|
await rmdir(dir).catch(() => {});
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
/**
|
|
182
|
+
* How long a writer's temp file may sit before it is debris.
|
|
183
|
+
*
|
|
184
|
+
* A record and a claim are published by renaming or linking a temp file, so the file
|
|
185
|
+
* exists for milliseconds; one left behind is a process that stopped between the
|
|
186
|
+
* write and the publish. The window is measured on the filesystem's own clock, not
|
|
187
|
+
* the bus's, because that is the clock the timestamps came from.
|
|
188
|
+
*/
|
|
189
|
+
const TEMP_TTL_MS = 60_000;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Remove the temp files a crashed writer left in one directory.
|
|
193
|
+
*
|
|
194
|
+
* Readers only ever look at `*.json`, so this debris is invisible and would live
|
|
195
|
+
* forever without a pass of its own.
|
|
196
|
+
*/
|
|
197
|
+
async function dropStaleTemps(dir: string): Promise<void> {
|
|
198
|
+
const cut = Date.now() - TEMP_TTL_MS;
|
|
199
|
+
for (const name of await readdir(dir).catch(() => [] as string[])) {
|
|
200
|
+
if (name.endsWith(".json")) continue;
|
|
201
|
+
const path = join(dir, name);
|
|
202
|
+
const info = await stat(path).catch(() => undefined);
|
|
203
|
+
if (info !== undefined && info.mtimeMs < cut) await rm(path, { force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
171
207
|
/** Create a store rooted at one repository's bus directory. */
|
|
172
208
|
export function createBusStore(options: BusStoreOptions): BusStore {
|
|
173
209
|
const { dir } = options;
|
|
@@ -188,17 +224,39 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
188
224
|
return sessions.sort((a, b) => b.lastSeen - a.lastSeen || a.label.localeCompare(b.label));
|
|
189
225
|
}
|
|
190
226
|
|
|
191
|
-
|
|
192
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Up to `limit` messages from the front of a recipient's inbox, oldest first.
|
|
229
|
+
*
|
|
230
|
+
* The front is the queue, not a detail: consuming starts there so a limit never starves,
|
|
231
|
+
* and so does a read, because the caller names the ids it already has. A file name holds
|
|
232
|
+
* the id, so a skip costs no read — which is what lets a watch page a backlog one poll at
|
|
233
|
+
* a time without ever reading the same file twice.
|
|
234
|
+
*
|
|
235
|
+
* Only the consuming read deletes, and it deletes a file it cannot validate too, so a
|
|
236
|
+
* truncated message cannot linger on the bus. A read that promises not to consume leaves
|
|
237
|
+
* such a file where it is: it may be mail this build cannot read and the next one can, and
|
|
238
|
+
* `inbox` or the prune TTL is what ends it.
|
|
239
|
+
*/
|
|
240
|
+
async function readMessages(
|
|
241
|
+
directory: string,
|
|
242
|
+
names: readonly string[],
|
|
243
|
+
options: { limit: number; known?: ReadonlySet<string>; remove: boolean },
|
|
244
|
+
): Promise<BusMessage[]> {
|
|
245
|
+
const { limit, known, remove } = options;
|
|
193
246
|
const messages: BusMessage[] = [];
|
|
194
|
-
for (const name of
|
|
247
|
+
for (const name of names) {
|
|
248
|
+
if (known?.has(idOfFile(name))) continue;
|
|
195
249
|
const path = join(directory, name);
|
|
196
250
|
const message = readTyped(path, isMessage);
|
|
197
|
-
if (message)
|
|
198
|
-
|
|
251
|
+
if (!message) {
|
|
252
|
+
if (remove) await rm(path, { force: true });
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
messages.push(message);
|
|
256
|
+
if (remove) await rm(path, { force: true });
|
|
199
257
|
if (messages.length >= limit) break;
|
|
200
258
|
}
|
|
201
|
-
return messages.sort((a, b) => a.at - b.at || a.id.localeCompare(b.id))
|
|
259
|
+
return messages.sort((a, b) => a.at - b.at || a.id.localeCompare(b.id));
|
|
202
260
|
}
|
|
203
261
|
|
|
204
262
|
async function readClaims(): Promise<ClaimRecord[]> {
|
|
@@ -215,7 +273,7 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
215
273
|
owner: string,
|
|
216
274
|
ownerLabel: string,
|
|
217
275
|
at: number,
|
|
218
|
-
): Promise<{ claim: ClaimRecord; conflict?: ClaimRecord }> {
|
|
276
|
+
): Promise<{ claim: ClaimRecord; conflict?: ClaimRecord; created: boolean }> {
|
|
219
277
|
await mkdir(claimsDir, { recursive: true, mode: DIR_MODE });
|
|
220
278
|
const file = claimFile(path);
|
|
221
279
|
const claim: ClaimRecord = { path, owner, ownerLabel, at };
|
|
@@ -227,19 +285,21 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
227
285
|
try {
|
|
228
286
|
try {
|
|
229
287
|
await link(temp, file);
|
|
230
|
-
return { claim };
|
|
288
|
+
return { claim, created: true };
|
|
231
289
|
} catch (error) {
|
|
232
290
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
233
291
|
}
|
|
234
292
|
const existing = readTyped(file, isClaim);
|
|
235
293
|
if (existing) {
|
|
236
|
-
return existing.owner === owner
|
|
294
|
+
return existing.owner === owner
|
|
295
|
+
? { claim: existing, created: false }
|
|
296
|
+
: { claim, conflict: existing, created: false };
|
|
237
297
|
}
|
|
238
298
|
// A claim file that does not parse is debris from before claims were
|
|
239
299
|
// published whole; clear it once so a stale path cannot block forever.
|
|
240
300
|
await rm(file, { force: true });
|
|
241
301
|
await link(temp, file);
|
|
242
|
-
return { claim };
|
|
302
|
+
return { claim, created: true };
|
|
243
303
|
} finally {
|
|
244
304
|
await rm(temp, { force: true });
|
|
245
305
|
}
|
|
@@ -258,13 +318,19 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
258
318
|
*
|
|
259
319
|
* The pre-check reads the claims once and reports every blocking owner, so a
|
|
260
320
|
* batch that cannot be satisfied touches no file. A create that loses a race
|
|
261
|
-
* after the pre-check
|
|
321
|
+
* after the pre-check gives back only what this batch published: a path the
|
|
322
|
+
* owner already held carries an older `at` and is not this batch's to release.
|
|
323
|
+
*
|
|
324
|
+
* `afterClaim` is a test seam: it runs after a path is published, which is the
|
|
325
|
+
* only way a suite can put a rival claim in the window between the pre-check and
|
|
326
|
+
* the next create.
|
|
262
327
|
*/
|
|
263
328
|
async function claimAll(
|
|
264
329
|
paths: readonly string[],
|
|
265
330
|
owner: string,
|
|
266
331
|
ownerLabel: string,
|
|
267
332
|
at: number,
|
|
333
|
+
afterClaim?: (path: string) => Promise<void>,
|
|
268
334
|
): Promise<{ claimed: string[]; conflicts: ClaimConflict[] }> {
|
|
269
335
|
const existing = await readClaims();
|
|
270
336
|
const conflicts: ClaimConflict[] = [];
|
|
@@ -277,14 +343,17 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
277
343
|
}
|
|
278
344
|
if (conflicts.length > 0) return { claimed: [], conflicts };
|
|
279
345
|
const claimed: string[] = [];
|
|
346
|
+
const created: string[] = [];
|
|
280
347
|
for (const path of paths) {
|
|
281
348
|
const result = await claimOne(path, owner, ownerLabel, at);
|
|
282
349
|
if (result.conflict) {
|
|
283
|
-
for (const done of
|
|
350
|
+
for (const done of created) await releaseOne(done, owner);
|
|
284
351
|
const { owner: holder, ownerLabel: holderLabel } = result.conflict;
|
|
285
352
|
return { claimed: [], conflicts: [{ path, owner: holder, ownerLabel: holderLabel }] };
|
|
286
353
|
}
|
|
287
354
|
claimed.push(path);
|
|
355
|
+
if (result.created) created.push(path);
|
|
356
|
+
if (afterClaim) await afterClaim(path);
|
|
288
357
|
}
|
|
289
358
|
return { claimed, conflicts: [] };
|
|
290
359
|
}
|
|
@@ -318,9 +387,19 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
318
387
|
await writeJson(join(directory, `${message.id}.json`), message);
|
|
319
388
|
},
|
|
320
389
|
|
|
321
|
-
peek
|
|
390
|
+
async peek(id, options) {
|
|
391
|
+
const directory = inboxDir(id);
|
|
392
|
+
const names = await listFiles(directory);
|
|
393
|
+
return {
|
|
394
|
+
mail: await readMessages(directory, names, { ...options, remove: false }),
|
|
395
|
+
unread: new Set(names.map(idOfFile)),
|
|
396
|
+
};
|
|
397
|
+
},
|
|
322
398
|
|
|
323
|
-
inbox
|
|
399
|
+
async inbox(id, limit) {
|
|
400
|
+
const directory = inboxDir(id);
|
|
401
|
+
return readMessages(directory, await listFiles(directory), { limit, remove: true });
|
|
402
|
+
},
|
|
324
403
|
|
|
325
404
|
async unread(id) {
|
|
326
405
|
return (await listFiles(inboxDir(id))).length;
|
|
@@ -364,12 +443,15 @@ export function createBusStore(options: BusStoreOptions): BusStore {
|
|
|
364
443
|
for (const claim of await readClaims()) {
|
|
365
444
|
if (!live.has(claim.owner)) await rm(claimFile(claim.path), { force: true });
|
|
366
445
|
}
|
|
446
|
+
await dropStaleTemps(sessionsDir);
|
|
447
|
+
await dropStaleTemps(claimsDir);
|
|
367
448
|
for (const name of await readdir(inboxRoot).catch(() => [] as string[])) {
|
|
368
449
|
const directory = join(inboxRoot, name);
|
|
369
450
|
for (const file of await listFiles(directory)) {
|
|
370
451
|
const message = readTyped(join(directory, file), isMessage);
|
|
371
452
|
if (!message || at - message.at > messageTtlMs) await rm(join(directory, file), { force: true });
|
|
372
453
|
}
|
|
454
|
+
await dropStaleTemps(directory);
|
|
373
455
|
await dropIfEmpty(directory);
|
|
374
456
|
}
|
|
375
457
|
},
|
|
@@ -53,6 +53,25 @@ export interface BusMessage {
|
|
|
53
53
|
at: number;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* One read of a recipient's inbox.
|
|
58
|
+
*
|
|
59
|
+
* The watch and `inbox` ask two questions of the same directory — which mail is new,
|
|
60
|
+
* and which mail is gone — and one readdir answers both, so a queue's health never
|
|
61
|
+
* depends on what a single page happened to cover.
|
|
62
|
+
*/
|
|
63
|
+
export interface InboxPeek {
|
|
64
|
+
/** Up to one page of messages the caller has not seen, oldest first. */
|
|
65
|
+
mail: BusMessage[];
|
|
66
|
+
/**
|
|
67
|
+
* Every id whose file is still in the inbox, whether or not this read returned it.
|
|
68
|
+
*
|
|
69
|
+
* A file this build cannot read counts: it stays where it is, and `inbox` or the prune
|
|
70
|
+
* TTL is what ends it, so its id is never in `mail` while it is still here.
|
|
71
|
+
*/
|
|
72
|
+
unread: ReadonlySet<string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
/** One repository-relative claim. */
|
|
57
76
|
export interface ClaimRecord {
|
|
58
77
|
path: string;
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { matchesKey, Key } from "@earendil-works/pi-tui";
|
|
13
|
-
import { messageOf } from "
|
|
14
|
-
import { formatDuration, sanitizeLine } from "
|
|
15
|
-
import type { ScreenHint, ViewportRowsSource } from "
|
|
16
|
-
import { type DetailContents, DockListScreen } from "
|
|
17
|
-
import { SEPARATORS } from "
|
|
18
|
-
import { keyValueRow } from "
|
|
19
|
-
import { paint } from "
|
|
20
|
-
import { idSuffix } from "
|
|
13
|
+
import { messageOf } from "../../../lib/errors.ts";
|
|
14
|
+
import { formatDuration, sanitizeLine } from "../../../lib/format.ts";
|
|
15
|
+
import type { ScreenHint, ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
16
|
+
import { type DetailContents, DockListScreen } from "../../../lib/tui/dock-screen.ts";
|
|
17
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
18
|
+
import { keyValueRow } from "../../../lib/tui/rows.ts";
|
|
19
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
+
import { idSuffix } from "../format.ts";
|
|
21
21
|
import {
|
|
22
22
|
type SessionRow,
|
|
23
23
|
claimRowLine,
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
sessionRowLine,
|
|
28
28
|
sessionRows,
|
|
29
29
|
summaryCells,
|
|
30
|
-
} from "
|
|
31
|
-
import type { BoardSnapshot, BusMessage, ClaimRecord } from "
|
|
30
|
+
} from "../rows.ts";
|
|
31
|
+
import type { BoardSnapshot, BusMessage, ClaimRecord } from "../types.ts";
|
|
32
32
|
|
|
33
33
|
/** The scopes `Tab` cycles through. */
|
|
34
34
|
export type BoardScope = "sessions" | "inbox" | "claims";
|
|
@@ -112,8 +112,8 @@ export interface SessionListCallbacks {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
interface SessionListOptions {
|
|
115
|
-
/** The
|
|
116
|
-
|
|
115
|
+
/** The board the screen opens on; the command's poll replaces it, and it is never read back. */
|
|
116
|
+
board: BoardSnapshot | undefined;
|
|
117
117
|
theme: Theme;
|
|
118
118
|
callbacks: SessionListCallbacks;
|
|
119
119
|
onClose(): void;
|
|
@@ -150,7 +150,7 @@ export class SessionListComponent extends DockListScreen<BoardRow> {
|
|
|
150
150
|
});
|
|
151
151
|
this.callbacks = options.callbacks;
|
|
152
152
|
this.now = options.now ?? Date.now;
|
|
153
|
-
this.board = options.
|
|
153
|
+
this.board = options.board;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/** Replace the board; a poll that changed nothing does not repaint. */
|
|
@@ -257,7 +257,7 @@ export class SessionListComponent extends DockListScreen<BoardRow> {
|
|
|
257
257
|
hints.push(enter("read"), this.watchHint(), "↑/↓", "PgUp/PgDn");
|
|
258
258
|
break;
|
|
259
259
|
case "claims":
|
|
260
|
-
hints.push(enter("open"), { text: "x release", color: "
|
|
260
|
+
hints.push(enter("open"), { text: "x release", color: "warn" }, "↑/↓", "PgUp/PgDn");
|
|
261
261
|
break;
|
|
262
262
|
}
|
|
263
263
|
return hints;
|
|
@@ -265,7 +265,7 @@ export class SessionListComponent extends DockListScreen<BoardRow> {
|
|
|
265
265
|
|
|
266
266
|
protected detailHints(row: BoardRow | undefined): ScreenHint[] {
|
|
267
267
|
if (row?.kind === "claim" && row.claim.owner === this.me()) {
|
|
268
|
-
return ["Esc back", { text: "x release", color: "
|
|
268
|
+
return ["Esc back", { text: "x release", color: "warn" }, "↑/↓ scroll", "g/G ends"];
|
|
269
269
|
}
|
|
270
270
|
return ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"];
|
|
271
271
|
}
|
|
@@ -366,7 +366,7 @@ export class SessionListComponent extends DockListScreen<BoardRow> {
|
|
|
366
366
|
|
|
367
367
|
private watchHint(): ScreenHint {
|
|
368
368
|
return this.board?.status.watching
|
|
369
|
-
? { text: "w watch off", color: "
|
|
369
|
+
? { text: "w watch off", color: "warn" }
|
|
370
370
|
: { text: "w watch on", color: "accent" };
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Agent definitions and discovery.
|
|
3
3
|
*
|
|
4
4
|
* Built-in agents ship with the extension. User agents are loaded from
|
|
5
|
-
* `<
|
|
5
|
+
* `<agent-dir>/agents` and project agents from the nearest
|
|
6
6
|
* `<CONFIG_DIR_NAME>/agents` (for example `.pi/agents`). Each external file is
|
|
7
7
|
* markdown with YAML frontmatter (`name`, `description`, `tools`, `model`) and
|
|
8
8
|
* the system prompt as the body.
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { planModeRefusal } from "../../lib/plan-entry.ts";
|
|
11
11
|
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
12
|
-
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
13
12
|
import type { BackgroundRuntime } from "./background.ts";
|
|
14
|
-
import { AgentsBoardComponent } from "./background-tui.ts";
|
|
15
13
|
import { formatAgentsList } from "./format.ts";
|
|
16
14
|
|
|
17
15
|
/** Open `/agents`; the TUI gets the board, other modes get the list. */
|
|
@@ -35,6 +33,10 @@ async function openAgents(
|
|
|
35
33
|
return;
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
// The board is the TUI's: the list above and registration load neither it nor
|
|
37
|
+
// its rails wrapper.
|
|
38
|
+
const { withRailsSuppressed } = await import("../../lib/rails.ts");
|
|
39
|
+
const { AgentsBoardComponent } = await import("./view/agents-board.ts");
|
|
38
40
|
let collectId: string | undefined;
|
|
39
41
|
await withRailsSuppressed(pi, async () => {
|
|
40
42
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { formatDuration, formatTokens, sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
12
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
12
13
|
import { USAGE_COUNTERS, zeroUsage } from "../../lib/usage.ts";
|
|
13
14
|
import type { BackgroundRun } from "./background.ts";
|
|
14
15
|
import type { UsageSource, UsageStats } from "./types.ts";
|
|
@@ -116,13 +117,13 @@ export function formatToolCall(
|
|
|
116
117
|
theme: Theme,
|
|
117
118
|
preview: boolean,
|
|
118
119
|
): string {
|
|
119
|
-
const fg =
|
|
120
|
+
const fg = (role: TuiRole, text: string) => paint(theme, role, text);
|
|
120
121
|
// Defensive: a malformed tool-call part can carry a null/undefined `arguments`.
|
|
121
122
|
const a = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
|
|
122
123
|
|
|
123
124
|
switch (toolName) {
|
|
124
125
|
case "bash":
|
|
125
|
-
return fg("
|
|
126
|
+
return fg("meta", "$ ") + fg("toolOutput", clip(sanitizeLine(String(a.command ?? "…")), preview ? 60 : 200));
|
|
126
127
|
case "read": {
|
|
127
128
|
const rawPath = clipPath(shortenPath(sanitizeLine(String(a.file_path ?? a.path ?? "…"))), preview ? 60 : 200);
|
|
128
129
|
const offset = a.offset as number | undefined;
|
|
@@ -131,44 +132,43 @@ export function formatToolCall(
|
|
|
131
132
|
if (offset !== undefined || limit !== undefined) {
|
|
132
133
|
const start = offset ?? 1;
|
|
133
134
|
const end = limit !== undefined ? start + limit - 1 : "";
|
|
134
|
-
text += fg("
|
|
135
|
+
text += fg("warn", `:${start}${end ? `-${end}` : ""}`);
|
|
135
136
|
}
|
|
136
|
-
return fg("
|
|
137
|
+
return fg("meta", "read ") + text;
|
|
137
138
|
}
|
|
138
139
|
case "write": {
|
|
139
140
|
const rawPath = clipPath(shortenPath(sanitizeLine(String(a.file_path ?? a.path ?? "…"))), preview ? 60 : 200);
|
|
140
141
|
const content = String(a.content ?? "");
|
|
141
142
|
const lines = content.split("\n").length;
|
|
142
|
-
const text = fg("
|
|
143
|
-
return lines > 1 ? text + fg("
|
|
143
|
+
const text = fg("meta", "write ") + fg("accent", rawPath);
|
|
144
|
+
return lines > 1 ? text + fg("faint", ` (${lines} lines)`) : text;
|
|
144
145
|
}
|
|
145
146
|
case "edit":
|
|
146
147
|
return (
|
|
147
|
-
fg("
|
|
148
|
+
fg("meta", "edit ") +
|
|
148
149
|
fg("accent", clipPath(shortenPath(sanitizeLine(String(a.file_path ?? a.path ?? "…"))), preview ? 60 : 200)) +
|
|
149
|
-
fg("
|
|
150
|
+
fg("faint", " (diff)")
|
|
150
151
|
);
|
|
151
152
|
case "ls":
|
|
152
153
|
return (
|
|
153
|
-
fg("
|
|
154
|
-
fg("accent", clipPath(shortenPath(sanitizeLine(String(a.path ?? "."))), preview ? 60 : 200))
|
|
154
|
+
fg("meta", "ls ") + fg("accent", clipPath(shortenPath(sanitizeLine(String(a.path ?? "."))), preview ? 60 : 200))
|
|
155
155
|
);
|
|
156
156
|
case "find":
|
|
157
157
|
return (
|
|
158
|
-
fg("
|
|
158
|
+
fg("meta", "find ") +
|
|
159
159
|
fg("accent", clip(sanitizeLine(String(a.pattern ?? "*")), preview ? 40 : 120)) +
|
|
160
|
-
fg("
|
|
160
|
+
fg("faint", ` in ${clipPath(shortenPath(sanitizeLine(String(a.path ?? "."))), preview ? 40 : 120)}`)
|
|
161
161
|
);
|
|
162
162
|
case "grep":
|
|
163
163
|
return (
|
|
164
|
-
fg("
|
|
164
|
+
fg("meta", "grep ") +
|
|
165
165
|
fg("accent", `/${clip(sanitizeLine(String(a.pattern ?? "")), preview ? 40 : 120)}/`) +
|
|
166
|
-
fg("
|
|
166
|
+
fg("faint", ` in ${clipPath(shortenPath(sanitizeLine(String(a.path ?? "."))), preview ? 40 : 120)}`)
|
|
167
167
|
);
|
|
168
168
|
default: {
|
|
169
169
|
const json = JSON.stringify(a);
|
|
170
170
|
const text = clip(json, preview ? 50 : 200);
|
|
171
|
-
return fg("accent", sanitizeLine(toolName)) + fg("
|
|
171
|
+
return fg("accent", sanitizeLine(toolName)) + fg("faint", ` ${text}`);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* output).
|
|
9
9
|
*
|
|
10
10
|
* Agents are the built-ins in `agents.ts` plus optional user agents loaded from
|
|
11
|
-
* `<
|
|
11
|
+
* `<agent-dir>/agents` and project agents from the nearest `.pi/agents`; the
|
|
12
12
|
* `agentScope` parameter decides which external directories are consulted.
|
|
13
13
|
* Subagents inherit the dispatching session's model and thinking level.
|
|
14
14
|
*
|
|
@@ -179,7 +179,7 @@ function indentBody(text: string, theme: Theme): string {
|
|
|
179
179
|
return stripControlChars(text)
|
|
180
180
|
.trim()
|
|
181
181
|
.split("\n")
|
|
182
|
-
.map((line) => indent + theme
|
|
182
|
+
.map((line) => indent + paint(theme, "toolOutput", line))
|
|
183
183
|
.join("\n");
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -192,7 +192,7 @@ function renderDisplayItems(items: DisplayItem[], theme: Theme, limit?: number,
|
|
|
192
192
|
if (item.type === "text") {
|
|
193
193
|
const safe = stripControlChars(item.text);
|
|
194
194
|
const text = preview ? safe.split("\n").slice(0, COLLAPSED_TEXT_LINES).join("\n") : safe;
|
|
195
|
-
lines.push(theme
|
|
195
|
+
lines.push(paint(theme, "toolOutput", text));
|
|
196
196
|
} else {
|
|
197
197
|
lines.push(paint(theme, "meta", "→ ") + formatToolCall(item.name, item.args, theme, preview));
|
|
198
198
|
}
|
|
@@ -419,7 +419,7 @@ function multiHeader(details: SubagentDetails, theme: Theme, now: number): strin
|
|
|
419
419
|
: bad > 0
|
|
420
420
|
? paint(theme, bad === total ? "bad" : "warn", bad === total ? GLYPHS.jobsFailure : GLYPHS.half)
|
|
421
421
|
: paint(theme, "ok", GLYPHS.check);
|
|
422
|
-
return `${icon} ${theme
|
|
422
|
+
return `${icon} ${paint(theme, "toolTitle", theme.bold(`${details.mode} · `))}${paint(theme, "accent", status)}`;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number): string {
|