@gavin-hu/my-pi-agent 0.17.0 → 0.18.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 +17 -9
- package/README.zh-CN.md +16 -9
- package/extensions/bytefly/README.md +25 -18
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/settings.ts +25 -18
- package/extensions/file-browse/README.md +60 -21
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +62 -0
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +19 -147
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +25 -4
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +2 -0
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +15 -7
- package/extensions/memory/README.md +3 -3
- package/extensions/plan/README.md +280 -86
- package/extensions/plan/commands.ts +56 -5
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +1 -1
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +165 -57
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +78 -98
- package/extensions/plan/runtime.ts +1 -1
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +80 -93
- package/extensions/plan/tools/enter.ts +8 -3
- package/extensions/plan/tools/exit.ts +101 -23
- package/extensions/plan/tools/write.ts +8 -3
- package/extensions/plan/tools.ts +1 -1
- package/extensions/plan/tui.ts +51 -8
- package/extensions/plan/types.ts +37 -3
- package/extensions/plan/web-chrome.ts +2 -1
- package/extensions/plan/web-client.ts +121 -177
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-page.ts +103 -17
- package/extensions/plan/web-router.ts +146 -30
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +1 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +15 -9
- package/extensions/wechat/README.md +20 -33
- package/extensions/wechat/bridge.ts +8 -8
- package/extensions/wechat/commands.ts +2 -2
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +31 -16
- package/extensions/wechat/notices.ts +7 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +39 -24
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/status.ts +1 -3
- package/lib/README.md +6 -2
- package/lib/claims/overlap.ts +43 -0
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +10 -0
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +52 -33
- package/lib/requirements.ts +1 -2
- package/lib/tui/README.md +16 -2
- package/lib/tui/dock-screen.ts +85 -17
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/web/components.ts +1 -1
- package/package.json +3 -2
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +78 -53
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/plan/config.ts +0 -27
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A page that stays open holds one event stream; this module builds the snapshot
|
|
5
5
|
* it is sent, the frame that carries it, and the channel that fans it out on a
|
|
6
|
-
* tick.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* tick. A tick costs one `git status` and one `git diff --stat`: the providers'
|
|
7
|
+
* 2 s memo only coalesces a tick with a page render in the same window, so the
|
|
8
|
+
* cadence is what bounds the reads. Every read failure degrades to `null`: a
|
|
9
|
+
* status feed must never be able to fail a page or the server that serves it.
|
|
9
10
|
*
|
|
10
11
|
* The channel opens no socket of its own — the caller hands it a `write`/`end`
|
|
11
12
|
* pair — so the whole feed is tested without one.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { defaultInterval, defaultTimer } from "../../lib/timer.ts";
|
|
16
|
+
import { LIVE_MAX_CLIENTS } from "./config.ts";
|
|
15
17
|
import { changeDirs } from "./diff.ts";
|
|
16
18
|
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
|
|
19
|
+
import { changeRevision } from "./rev.ts";
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* The changed-path index the pages render from: the totals the changes chip
|
|
@@ -50,12 +53,27 @@ export interface LiveChanges {
|
|
|
50
53
|
deletions: number;
|
|
51
54
|
files: string[];
|
|
52
55
|
dirs: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Identity of this change set, counts included. A page that painted the same
|
|
58
|
+
* revision repaints nothing when the next frame arrives.
|
|
59
|
+
*/
|
|
60
|
+
rev: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A change index as it goes over the wire, with its revision. */
|
|
64
|
+
function changesOf(index: ChangeIndex): LiveChanges {
|
|
65
|
+
const changes = {
|
|
66
|
+
count: index.count,
|
|
67
|
+
insertions: index.insertions,
|
|
68
|
+
deletions: index.deletions,
|
|
69
|
+
files: [...index.files].sort(),
|
|
70
|
+
dirs: [...index.dirs].sort(),
|
|
71
|
+
};
|
|
72
|
+
return { ...changes, rev: changeRevision(changes) };
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
/** One status frame's payload. */
|
|
56
76
|
export interface LiveSnapshot {
|
|
57
|
-
/** Server wall clock when the snapshot was taken, in ms. */
|
|
58
|
-
at: number;
|
|
59
77
|
/** How long the server has been listening, in ms. */
|
|
60
78
|
uptimeMs: number;
|
|
61
79
|
/** Requests the server has answered so far, this one included. */
|
|
@@ -112,15 +130,7 @@ export function createLiveStatus(options: LiveStatusOptions): LiveStatusSource {
|
|
|
112
130
|
uptimeMs: Math.max(0, at - options.startedAt),
|
|
113
131
|
requests: options.requests(),
|
|
114
132
|
git,
|
|
115
|
-
changes: index
|
|
116
|
-
? {
|
|
117
|
-
count: index.count,
|
|
118
|
-
insertions: index.insertions,
|
|
119
|
-
deletions: index.deletions,
|
|
120
|
-
files: [...index.files].sort(),
|
|
121
|
-
dirs: [...index.dirs].sort(),
|
|
122
|
-
}
|
|
123
|
-
: null,
|
|
133
|
+
changes: index ? changesOf(index) : null,
|
|
124
134
|
};
|
|
125
135
|
},
|
|
126
136
|
};
|
|
@@ -158,6 +168,8 @@ export interface LiveChannelOptions {
|
|
|
158
168
|
intervalMs: number;
|
|
159
169
|
/** How long the last page's departure is held before `onIdle` is called, in ms. */
|
|
160
170
|
idleMs: number;
|
|
171
|
+
/** How many pages may attach at once; defaults to `LIVE_MAX_CLIENTS`. */
|
|
172
|
+
maxClients?: number;
|
|
161
173
|
/** Called once the last page has been gone for `idleMs`; omitted, the channel only idles. */
|
|
162
174
|
onIdle?: () => void;
|
|
163
175
|
/** Timer seam; defaults to an unref'd interval. */
|
|
@@ -172,6 +184,11 @@ export interface LiveChannel {
|
|
|
172
184
|
attach(client: LiveClient): () => void;
|
|
173
185
|
/** End every connected page, stop the ticker, and drop a pending idle stop. */
|
|
174
186
|
close(): void;
|
|
187
|
+
/**
|
|
188
|
+
* Whether another page must be refused. The caller asks before it opens the
|
|
189
|
+
* stream, so a refusal never has to take back a head it already wrote.
|
|
190
|
+
*/
|
|
191
|
+
atCapacity(): boolean;
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
/**
|
|
@@ -183,6 +200,7 @@ export interface LiveChannel {
|
|
|
183
200
|
export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
184
201
|
const timer = options.timer ?? defaultTicker;
|
|
185
202
|
const timeout = options.timeout ?? defaultTimeout;
|
|
203
|
+
const maxClients = options.maxClients ?? LIVE_MAX_CLIENTS;
|
|
186
204
|
const clients = new Set<LiveClient>();
|
|
187
205
|
let cancel: (() => void) | undefined;
|
|
188
206
|
let cancelIdle: (() => void) | undefined;
|
|
@@ -252,5 +270,8 @@ export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
|
252
270
|
for (const client of clients) client.end();
|
|
253
271
|
clients.clear();
|
|
254
272
|
},
|
|
273
|
+
atCapacity() {
|
|
274
|
+
return clients.size >= maxClients;
|
|
275
|
+
},
|
|
255
276
|
};
|
|
256
277
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revisions for the browse live status.
|
|
3
|
+
*
|
|
4
|
+
* A revision is a short digest of something a page already drew — the change set
|
|
5
|
+
* a frame carries, or the names a listing holds — so a client can tell "nothing
|
|
6
|
+
* moved" from "paint again" without comparing the payload it was sent. A
|
|
7
|
+
* collision costs one skipped repaint and is corrected by the next frame or a
|
|
8
|
+
* reload, which is the trade a 12-character digest makes for a small frame.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A digest of the parts, cut to 12 hex characters. The parts are joined with NUL,
|
|
15
|
+
* which no filename can contain, so two different lists cannot run together into
|
|
16
|
+
* the same text.
|
|
17
|
+
*/
|
|
18
|
+
function digest(parts: readonly string[]): string {
|
|
19
|
+
return createHash("sha1").update(parts.join("\u0000")).digest("hex").slice(0, 12);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The change set a frame carries: its totals and paths, in a stable order. */
|
|
23
|
+
export interface RevisionInput {
|
|
24
|
+
count: number;
|
|
25
|
+
insertions: number;
|
|
26
|
+
deletions: number;
|
|
27
|
+
files: readonly string[];
|
|
28
|
+
dirs: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The revision of a change set. The totals are part of it, so an edit that moves
|
|
33
|
+
* no path still moves the revision and the chip's `+A −D` stays honest.
|
|
34
|
+
*/
|
|
35
|
+
export function changeRevision(changes: RevisionInput): string {
|
|
36
|
+
return digest([`${changes.count}:${changes.insertions}:${changes.deletions}`, ...changes.files, ...changes.dirs]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The revision of a listing's names, independent of the order they arrived in. */
|
|
40
|
+
export function listingRevision(names: readonly string[]): string {
|
|
41
|
+
return digest([...names].sort());
|
|
42
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../../lib/http/index.ts";
|
|
23
23
|
import { THEME_JS, THEME_JS_PATH } from "../../lib/web/index.ts";
|
|
24
24
|
import { CLIENT_JS } from "./client.ts";
|
|
25
|
+
import type { LivePage } from "./chrome.ts";
|
|
25
26
|
import type { ServeConfig } from "./config.ts";
|
|
26
27
|
import { isDiffMode, type DiffMode } from "./diff.ts";
|
|
27
28
|
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
|
|
@@ -43,6 +44,7 @@ import { fuzzySearch } from "./fuzzy.ts";
|
|
|
43
44
|
import { renderDirectoryPage, renderDiffPage, renderErrorPage, renderFileDiffPage, renderFilePage } from "./html.ts";
|
|
44
45
|
import type { ChangeSummary } from "./html.ts";
|
|
45
46
|
import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "./paths.ts";
|
|
47
|
+
import { listingRevision } from "./rev.ts";
|
|
46
48
|
|
|
47
49
|
export interface ServeContext {
|
|
48
50
|
root: string;
|
|
@@ -108,9 +110,13 @@ function summaryOf(list: DiffList): ChangeSummary {
|
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The live chrome a page renders, or `undefined` when live status is off. A
|
|
115
|
+
* listing page also passes the revision of what it drew, which is what its
|
|
116
|
+
* reload control compares against; every other page has nothing to compare.
|
|
117
|
+
*/
|
|
118
|
+
function liveOf(ctx: ServeContext, folder?: { dir: string; rev: string }): LivePage | undefined {
|
|
119
|
+
return ctx.config.live && ctx.live ? { intervalMs: ctx.config.liveIntervalMs, ...folder } : undefined;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/** The `head` change summary, for the header chip, tree markers, and file action. */
|
|
@@ -184,7 +190,10 @@ async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeR
|
|
|
184
190
|
maxThumbBytes: ctx.config.maxThumbBytes,
|
|
185
191
|
git,
|
|
186
192
|
summary: changes,
|
|
187
|
-
live: liveOf(
|
|
193
|
+
live: liveOf(
|
|
194
|
+
ctx,
|
|
195
|
+
listing.truncated ? undefined : { dir: rel, rev: listingRevision(listing.entries.map((entry) => entry.name)) },
|
|
196
|
+
),
|
|
188
197
|
}),
|
|
189
198
|
);
|
|
190
199
|
}
|
|
@@ -312,6 +321,17 @@ async function treeApi(ctx: ServeContext, decodedPath: string): Promise<ServeRes
|
|
|
312
321
|
return jsonResponse(200, { path: rel, entries: listing.entries.map(treeApiEntry) });
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
/** The revision of one folder's listing: what a page's reload control compares against. */
|
|
325
|
+
async function dirRevApi(ctx: ServeContext, decodedPath: string): Promise<ServeResponse> {
|
|
326
|
+
const abs = await resolveDecodedPath(ctx.root, decodedPath);
|
|
327
|
+
const rel = relativePath(ctx.root, abs);
|
|
328
|
+
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
329
|
+
// A truncated listing is a prefix whose boundary moves, so its revision is not
|
|
330
|
+
// comparable with what the page drew: `null` says "cannot tell".
|
|
331
|
+
const names = listing.entries.map((entry) => entry.name);
|
|
332
|
+
return jsonResponse(200, { path: rel, rev: listing.truncated ? null : listingRevision(names) });
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
/** Fuzzy file search for the quick-open palette, over the cached index. */
|
|
316
336
|
async function filesApi(ctx: ServeContext, query: string): Promise<ServeResponse> {
|
|
317
337
|
const index = ctx.index ? await ctx.index() : await buildFileIndex(ctx.root, ctx.config.maxIndexEntries);
|
|
@@ -355,6 +375,7 @@ export async function handleRequest(
|
|
|
355
375
|
if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
356
376
|
if (pathname === "/favicon.ico") return { status: 204, headers: pageHeaders(), body: "" };
|
|
357
377
|
if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
|
|
378
|
+
if (pathname === "/api/dir-rev") return await dirRevApi(ctx, search.get("path") ?? "");
|
|
358
379
|
if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
|
|
359
380
|
if (pathname === "/events") return liveEvents(ctx);
|
|
360
381
|
const rawMode = search.get("mode");
|
|
@@ -103,6 +103,13 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
103
103
|
respond(request, response, result);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
+
// Checked before the head is written: a refusal must not leave a stream the
|
|
107
|
+
// client would wait on. `204` is the one status an event source does not
|
|
108
|
+
// reconnect after, so a page beyond the cap goes quiet instead of retrying.
|
|
109
|
+
if (live.atCapacity()) {
|
|
110
|
+
respond(request, response, { status: 204, headers: { "Cache-Control": "no-store" }, body: "" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
106
113
|
openEventStream(response, result);
|
|
107
114
|
const detach = live.attach({
|
|
108
115
|
write: (frame) => {
|
|
@@ -47,6 +47,8 @@ body.palette-open{overflow:hidden}
|
|
|
47
47
|
swap only changes the colour, and the footer line reads as one muted clause. */
|
|
48
48
|
.browse-live{font-family:var(--font-mono);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
49
49
|
.browse-live__glyph{font-size:9px;line-height:1}
|
|
50
|
+
.browse-reload{border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px;cursor:pointer;font-family:var(--font-mono)}
|
|
51
|
+
.browse-reload:hover{text-decoration:underline}
|
|
50
52
|
.browse-live-line{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
51
53
|
.browse-live-line::after{content:" · "}
|
|
52
54
|
.browse-diff-totals{font-family:var(--font-mono)}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { isPursuedGoal } from "../../lib/goal-state.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
|
|
23
|
+
import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, requestPlanMode } from "../../lib/plan-entry.ts";
|
|
23
24
|
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
24
25
|
import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
|
|
25
26
|
import type { Goal, GoalStatus } from "./types.ts";
|
|
@@ -45,7 +46,12 @@ export async function goalPhase(
|
|
|
45
46
|
if (!options.ask || !ctx.hasUI) return "active";
|
|
46
47
|
// Do not ask about a phase this session cannot enter.
|
|
47
48
|
if (!pi.getAllTools().some((tool) => tool.name === PLAN_ENTER_TOOL)) return "active";
|
|
48
|
-
const
|
|
49
|
+
const consent = isRemoteTurn(ctx)
|
|
50
|
+
? remoteLanguage(ctx) === "zh"
|
|
51
|
+
? PLAN_CONSENT_SHORT_ZH
|
|
52
|
+
: PLAN_CONSENT_SHORT
|
|
53
|
+
: PLAN_CONSENT;
|
|
54
|
+
const approved = await ctx.ui.confirm(PLAN_ASK, consent);
|
|
49
55
|
if (!approved) return "active";
|
|
50
56
|
return (await requestPlanMode(pi, ctx)) ? "planning" : "active";
|
|
51
57
|
}
|
package/extensions/job/README.md
CHANGED
|
@@ -37,7 +37,8 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
37
37
|
chip for this session's jobs, plus a `/jobs` screen for selecting, tailing,
|
|
38
38
|
killing, and clearing.
|
|
39
39
|
- Reports finished jobs to the model at the start of the next turn, or — for
|
|
40
|
-
jobs started with `wake: true` — by triggering one turn
|
|
40
|
+
jobs started with `wake: true` — by triggering one turn as soon as the agent is
|
|
41
|
+
idle, including right after a running turn settles.
|
|
41
42
|
- Kills session-owned jobs on shutdown so no process is left behind; on POSIX,
|
|
42
43
|
start a job with `detached: true` to keep it running and reattach next session.
|
|
43
44
|
A detached job points its own output straight at its log file, so it keeps
|
|
@@ -338,13 +339,14 @@ rewritten.
|
|
|
338
339
|
extension keeps only what is job-shaped — the durable registry, rotation,
|
|
339
340
|
stdin, and the job vocabulary. The report coordinator is also why a wake can
|
|
340
341
|
never stack two turns: it owns the guard for the window between the triggered
|
|
341
|
-
send and `agent_start
|
|
342
|
+
send and `agent_start`, and it retries a wake the running turn suppressed.
|
|
342
343
|
- **Notify by default, wake opt-in.** A failed job raises one `ctx.ui.notify`
|
|
343
344
|
(a clean exit is left to the list and chips). Finished jobs are also posted
|
|
344
345
|
as a shared transcript notice — model-facing text plus the notice block — at the
|
|
345
|
-
next `before_agent_start
|
|
346
|
-
triggers one turn
|
|
347
|
-
|
|
346
|
+
next `before_agent_start`, or in the wake turn it raises. A `wake: true` job (or
|
|
347
|
+
`wakeOnFinish`) additionally triggers one turn as soon as the agent is idle,
|
|
348
|
+
including right after a running turn settles; auto-wake is not the default
|
|
349
|
+
because it easily loops and burns tokens.
|
|
348
350
|
- **Width-1 glyphs and cached latest line.** The running chip is `▸ N` and the
|
|
349
351
|
failure chip `✗ N`, each a two-token badge under its own status key, so the
|
|
350
352
|
status bar compacts each independently (`▸N` / `✗N`) and an unreported
|
package/extensions/job/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* processes with their output streamed to log files; the status chip shows
|
|
6
6
|
* running and unreported-failure counts, and finished jobs are reported to the
|
|
7
7
|
* model — and shown as a bytefly notice — at the next turn, or by triggering one
|
|
8
|
-
* turn for a `wake` job
|
|
8
|
+
* turn for a `wake` job as soon as the agent is idle, including right after a
|
|
9
|
+
* running turn settles.
|
|
9
10
|
*
|
|
10
11
|
* Load with: pi --extension ./extensions/job
|
|
11
12
|
*/
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
16
|
import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
|
|
16
|
-
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
17
|
+
import { burstText, noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
17
18
|
import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
|
|
18
19
|
import { registerCommands } from "./commands.ts";
|
|
19
20
|
import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
|
|
@@ -25,8 +26,13 @@ import type { JobRecord } from "./types.ts";
|
|
|
25
26
|
const JOB_CONTEXT_MARKER = "[BACKGROUND JOBS]";
|
|
26
27
|
|
|
27
28
|
function buildContext(jobs: JobRecord[]): string {
|
|
28
|
-
const
|
|
29
|
-
return
|
|
29
|
+
const count = jobs.length;
|
|
30
|
+
return burstText({
|
|
31
|
+
marker: JOB_CONTEXT_MARKER,
|
|
32
|
+
headline: `${count} background job${count === 1 ? "" : "s"} finished`,
|
|
33
|
+
lines: jobs.map((job) => formatCompletion(job)),
|
|
34
|
+
hint: "Run `job logs <id>` for output.",
|
|
35
|
+
});
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/** The shared notice for pending completions: model text plus renderer details. */
|
|
@@ -66,15 +72,17 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
|
66
72
|
if (!isExtensionEnabled("job")) return;
|
|
67
73
|
const runtime = deps.runtime ?? createJobsRuntime();
|
|
68
74
|
|
|
69
|
-
// Opt-in wake: a `wake` job finished
|
|
70
|
-
//
|
|
71
|
-
//
|
|
75
|
+
// Opt-in wake: a `wake` job finished. The reporter reports *all* pending
|
|
76
|
+
// completions, not just the waking one, so a job that finished earlier is not
|
|
77
|
+
// silently marked seen and dropped, and it retries a completion that arrived
|
|
78
|
+
// mid-turn once the run settles.
|
|
72
79
|
const reporter = createPendingReporter<JobRecord>({
|
|
73
80
|
wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
|
|
74
81
|
pending: () => runtime.pendingCompletions(),
|
|
75
82
|
markReported: (records) => runtime.markReported(records.map((job) => job.id)),
|
|
76
83
|
buildNotice: jobNotice,
|
|
77
84
|
source: "job",
|
|
85
|
+
wakeRequested: () => runtime.pendingCompletions().some((job) => job.wake),
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
registerTools(pi, runtime);
|
|
@@ -169,9 +169,9 @@ skill. Mitigations:
|
|
|
169
169
|
injection, and rendering.
|
|
170
170
|
- Reads are capped at 256 KiB and the injected body at `maxInjectBytes`.
|
|
171
171
|
- The path is a fixed constant; a symlink at the memory path is refused.
|
|
172
|
-
- The files are ordinary user-owned markdown
|
|
173
|
-
|
|
174
|
-
project
|
|
172
|
+
- The files are ordinary user-owned markdown, so committing or sharing them is
|
|
173
|
+
the user's choice; review project memory before trusting a repository. Notes
|
|
174
|
+
stay in the project, unlike plans, which live in the agent directory.
|
|
175
175
|
|
|
176
176
|
## Limitations
|
|
177
177
|
|