@gavin-hu/my-pi-agent 0.8.1 → 0.10.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 +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -31,6 +31,22 @@ import { encodePath } from "./paths.ts";
|
|
|
31
31
|
const HAS_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
32
32
|
const SAFE_LINK_SCHEME = /^(?:https?|mailto):/i;
|
|
33
33
|
const HTTP_SCHEME = /^https?:/i;
|
|
34
|
+
/** A GFM extended autolink: `http(s)://` or `www.` followed by a domain. */
|
|
35
|
+
const AUTOLINK = /^(?:https?:\/\/|www\.)[^\s<>]*/i;
|
|
36
|
+
|
|
37
|
+
/** GFM starts an autolink at the line start or after whitespace or one of `*_~(`. */
|
|
38
|
+
function autolinkBoundary(text: string, index: number): boolean {
|
|
39
|
+
return index === 0 || /[\s*_~(]/.test(text[index - 1]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Drop the trailing punctuation GFM excludes, and an unclosed `)`. */
|
|
43
|
+
function trimAutolink(url: string): string {
|
|
44
|
+
let end = url.replace(/[.,;:!?*_~]+$/, "");
|
|
45
|
+
while (end.endsWith(")") && (end.match(/\(/g)?.length ?? 0) < (end.match(/\)/g)?.length ?? 0)) {
|
|
46
|
+
end = end.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
47
|
+
}
|
|
48
|
+
return end;
|
|
49
|
+
}
|
|
34
50
|
/** A link reference definition: `[id]: dest "title"`. */
|
|
35
51
|
const DEFINITION = /^ {0,3}\[([^\]]+)\]:\s*(\S+)(?:\s+["'(]([^"')]*)["')])?\s*$/;
|
|
36
52
|
|
|
@@ -800,6 +816,23 @@ function renderInline(text: string, ctx: Context): string {
|
|
|
800
816
|
}
|
|
801
817
|
}
|
|
802
818
|
|
|
819
|
+
// GFM extended autolink: a bare `https://` or `www.` URL in running text.
|
|
820
|
+
if ((char === "h" || char === "H" || char === "w" || char === "W") && autolinkBoundary(text, i)) {
|
|
821
|
+
const match = AUTOLINK.exec(text.slice(i));
|
|
822
|
+
if (match) {
|
|
823
|
+
const label = trimAutolink(match[0]);
|
|
824
|
+
const dots = label.indexOf(".");
|
|
825
|
+
// Require a domain, so a bare scheme is not a link.
|
|
826
|
+
if (dots > 0 && /\.[^./]/.test(label.slice(dots))) {
|
|
827
|
+
const href = /^www\./i.test(label) ? `http://${label}` : label;
|
|
828
|
+
const external = HTTP_SCHEME.test(href) ? ` target="_blank" rel="noopener noreferrer"` : "";
|
|
829
|
+
out += `<a href="${escapeHtml(href)}"${external}>${escapeHtml(label)}</a>`;
|
|
830
|
+
i += label.length;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
803
836
|
if ((char === "*" || char === "_") && text[i + 1] === char) {
|
|
804
837
|
const close = text.indexOf(char + char, i + 2);
|
|
805
838
|
if (close > i + 2) {
|
|
@@ -11,7 +11,8 @@ import { stat } from "node:fs/promises";
|
|
|
11
11
|
import { THEME_JS } from "../../lib/web/index.ts";
|
|
12
12
|
import { CLIENT_JS } from "./client.ts";
|
|
13
13
|
import type { ServeConfig } from "./config.ts";
|
|
14
|
-
import
|
|
14
|
+
import { changeDirs, isDiffMode, type DiffMode } from "./diff.ts";
|
|
15
|
+
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
|
|
15
16
|
import {
|
|
16
17
|
buildFileIndex,
|
|
17
18
|
buildTree,
|
|
@@ -25,7 +26,8 @@ import {
|
|
|
25
26
|
} from "./files.ts";
|
|
26
27
|
import { FOLDER_ICON, iconHref, languageOf } from "./icons.ts";
|
|
27
28
|
import { fuzzySearch } from "./fuzzy.ts";
|
|
28
|
-
import { renderDirectoryPage, renderErrorPage, renderFilePage } from "./html.ts";
|
|
29
|
+
import { renderDirectoryPage, renderDiffPage, renderErrorPage, renderFileDiffPage, renderFilePage } from "./html.ts";
|
|
30
|
+
import type { ChangeSummary } from "./html.ts";
|
|
29
31
|
import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "./paths.ts";
|
|
30
32
|
|
|
31
33
|
export interface ServeContext {
|
|
@@ -33,6 +35,8 @@ export interface ServeContext {
|
|
|
33
35
|
config: ServeConfig;
|
|
34
36
|
/** Read-only git context for the page header; omitted when unavailable. */
|
|
35
37
|
git?: GitStatusProvider;
|
|
38
|
+
/** Read-only diff provider; omitted when the diff view is unavailable. */
|
|
39
|
+
diff?: GitDiffProvider;
|
|
36
40
|
/** Cached quick-open file index; built on demand when omitted. */
|
|
37
41
|
index?: FileIndexProvider;
|
|
38
42
|
}
|
|
@@ -97,6 +101,33 @@ async function gitFor(ctx: ServeContext): Promise<GitStatus | undefined> {
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
/** The change list for a mode, or `undefined` outside a repository. */
|
|
105
|
+
async function diffFor(ctx: ServeContext, mode: DiffMode) {
|
|
106
|
+
if (!ctx.config.gitDiff || !ctx.diff) return undefined;
|
|
107
|
+
try {
|
|
108
|
+
return await ctx.diff.list(mode);
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The marker/totals index derived from a change list. */
|
|
115
|
+
function summaryOf(list: DiffList): ChangeSummary {
|
|
116
|
+
return {
|
|
117
|
+
count: list.files.length,
|
|
118
|
+
insertions: list.insertions,
|
|
119
|
+
deletions: list.deletions,
|
|
120
|
+
files: new Set(list.files.map((file) => file.rel)),
|
|
121
|
+
dirs: changeDirs(list.files),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The `head` change summary, for the header chip, tree markers, and file action. */
|
|
126
|
+
async function changed(ctx: ServeContext): Promise<ChangeSummary | undefined> {
|
|
127
|
+
const list = await diffFor(ctx, "head");
|
|
128
|
+
return list ? summaryOf(list) : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
async function errorResponse(
|
|
101
132
|
ctx: ServeContext,
|
|
102
133
|
status: number,
|
|
@@ -137,6 +168,7 @@ async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse>
|
|
|
137
168
|
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
138
169
|
const tree = await treeFor(ctx, rel);
|
|
139
170
|
const git = await gitFor(ctx);
|
|
171
|
+
const changes = await changed(ctx);
|
|
140
172
|
return htmlResponse(
|
|
141
173
|
200,
|
|
142
174
|
renderDirectoryPage({
|
|
@@ -147,6 +179,7 @@ async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse>
|
|
|
147
179
|
thumbnails: ctx.config.thumbnails,
|
|
148
180
|
maxThumbBytes: ctx.config.maxThumbBytes,
|
|
149
181
|
git,
|
|
182
|
+
summary: changes,
|
|
150
183
|
}),
|
|
151
184
|
);
|
|
152
185
|
}
|
|
@@ -161,6 +194,7 @@ async function view(ctx: ServeContext, rawRel: string, raw: boolean): Promise<Se
|
|
|
161
194
|
const file = await readFileView(ctx.root, rel, ctx.config.maxFileBytes, ctx.config.maxTextLines, ctx.config.markdown);
|
|
162
195
|
const tree = await treeFor(ctx, rel);
|
|
163
196
|
const git = await gitFor(ctx);
|
|
197
|
+
const changes = await changed(ctx);
|
|
164
198
|
return htmlResponse(
|
|
165
199
|
200,
|
|
166
200
|
renderFilePage({
|
|
@@ -171,6 +205,59 @@ async function view(ctx: ServeContext, rawRel: string, raw: boolean): Promise<Se
|
|
|
171
205
|
maxFileBytes: ctx.config.maxFileBytes,
|
|
172
206
|
git,
|
|
173
207
|
markdownRaw: raw,
|
|
208
|
+
changed: changes?.files.has(rel),
|
|
209
|
+
summary: changes,
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The change overview page for a mode. A non-repository is an empty state, not an error. */
|
|
215
|
+
async function diffOverview(ctx: ServeContext, mode: DiffMode): Promise<ServeResponse> {
|
|
216
|
+
if (!ctx.config.gitDiff || !ctx.diff) throw new HttpError(404, "The diff view is not available.");
|
|
217
|
+
const list = await diffFor(ctx, mode);
|
|
218
|
+
const tree = await treeFor(ctx, "");
|
|
219
|
+
const git = await gitFor(ctx);
|
|
220
|
+
return htmlResponse(
|
|
221
|
+
200,
|
|
222
|
+
renderDiffPage({
|
|
223
|
+
rootLabel: ctx.root,
|
|
224
|
+
mode,
|
|
225
|
+
files: list?.files ?? [],
|
|
226
|
+
stat: list?.stat ?? "",
|
|
227
|
+
tree,
|
|
228
|
+
git,
|
|
229
|
+
summary: list ? summaryOf(list) : undefined,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A single file's unified diff for a mode. */
|
|
235
|
+
async function diffFile(ctx: ServeContext, rawRel: string, mode: DiffMode): Promise<ServeResponse> {
|
|
236
|
+
if (!ctx.config.gitDiff || !ctx.diff) throw new HttpError(404, "The diff view is not available.");
|
|
237
|
+
const abs = await resolveRequestPath(ctx.root, rawRel);
|
|
238
|
+
const rel = relativePath(ctx.root, abs);
|
|
239
|
+
const info = await stat(abs).catch(() => undefined);
|
|
240
|
+
if (info?.isDirectory()) throw new HttpError(400, "That path is a folder.");
|
|
241
|
+
let diff: Awaited<ReturnType<GitDiffProvider["file"]>>;
|
|
242
|
+
try {
|
|
243
|
+
diff = await ctx.diff.file(rel, mode);
|
|
244
|
+
} catch {
|
|
245
|
+
diff = undefined;
|
|
246
|
+
}
|
|
247
|
+
if (!diff) throw new HttpError(404, "No diff is available for that file.");
|
|
248
|
+
const tree = await treeFor(ctx, rel);
|
|
249
|
+
const git = await gitFor(ctx);
|
|
250
|
+
const changes = await changed(ctx);
|
|
251
|
+
return htmlResponse(
|
|
252
|
+
200,
|
|
253
|
+
renderFileDiffPage({
|
|
254
|
+
rootLabel: ctx.root,
|
|
255
|
+
rel,
|
|
256
|
+
mode,
|
|
257
|
+
diff,
|
|
258
|
+
tree,
|
|
259
|
+
git,
|
|
260
|
+
summary: changes,
|
|
174
261
|
}),
|
|
175
262
|
);
|
|
176
263
|
}
|
|
@@ -276,6 +363,11 @@ export async function handleRequest(
|
|
|
276
363
|
if (pathname === "/favicon.ico") return { status: 204, headers: { ...HTML_SECURITY }, body: "" };
|
|
277
364
|
if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
|
|
278
365
|
if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
|
|
366
|
+
const rawMode = search.get("mode");
|
|
367
|
+
if (rawMode !== null && !isDiffMode(rawMode)) return errorResponse(ctx, 400, "Unknown diff mode.");
|
|
368
|
+
const mode: DiffMode = isDiffMode(rawMode) ? rawMode : "head";
|
|
369
|
+
if (pathname === "/diff" || pathname === "/diff/") return await diffOverview(ctx, mode);
|
|
370
|
+
if (pathname.startsWith("/diff/")) return await diffFile(ctx, pathname.slice("/diff/".length), mode);
|
|
279
371
|
if (pathname === "/" || pathname === "/browse" || pathname === "/browse/") return await browse(ctx, "");
|
|
280
372
|
if (pathname.startsWith("/browse/")) return await browse(ctx, pathname.slice("/browse/".length));
|
|
281
373
|
if (pathname.startsWith("/view/"))
|
|
@@ -11,7 +11,7 @@ import { createReadStream, realpathSync } from "node:fs";
|
|
|
11
11
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
12
12
|
import type { ServeConfig } from "./config.ts";
|
|
13
13
|
import { createFileIndex } from "./files.ts";
|
|
14
|
-
import type { GitStatusProvider } from "./git.ts";
|
|
14
|
+
import type { GitDiffProvider, GitStatusProvider } from "./git.ts";
|
|
15
15
|
import { handleRequest, type ServeContext, type ServeResponse } from "./router.ts";
|
|
16
16
|
|
|
17
17
|
export interface FileServer {
|
|
@@ -30,6 +30,8 @@ export interface FileServerOptions {
|
|
|
30
30
|
config: ServeConfig;
|
|
31
31
|
/** Read-only git context for the page header; omitted when unavailable. */
|
|
32
32
|
git?: GitStatusProvider;
|
|
33
|
+
/** Read-only diff provider for the change and diff views; omitted when unavailable. */
|
|
34
|
+
diff?: GitDiffProvider;
|
|
33
35
|
/** Override the request handler (tests). */
|
|
34
36
|
handler?: typeof handleRequest;
|
|
35
37
|
}
|
|
@@ -68,6 +70,7 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
68
70
|
root,
|
|
69
71
|
config: options.config,
|
|
70
72
|
git: options.git,
|
|
73
|
+
diff: options.diff,
|
|
71
74
|
index: createFileIndex(root, options.config.maxIndexEntries),
|
|
72
75
|
};
|
|
73
76
|
const handler = options.handler ?? handleRequest;
|
|
@@ -5,6 +5,12 @@ handler, or reveals it in the file manager. `/open` does it interactively and
|
|
|
5
5
|
the `open_path` tool does it from a script, both resolving the path against the
|
|
6
6
|
effective working directory.
|
|
7
7
|
|
|
8
|
+
## Tool rendering
|
|
9
|
+
|
|
10
|
+
How the `open_path` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
11
|
+
|
|
12
|
+
<img src="../../assets/tools/open_path.png" alt="The open_path tool's call and result, drawn by the generic fallback" width="100%">
|
|
13
|
+
|
|
8
14
|
## Quickstart
|
|
9
15
|
|
|
10
16
|
```bash
|
|
@@ -9,6 +9,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent packa
|
|
|
9
9
|
pi install ./ # install the package
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Tool rendering
|
|
13
|
+
|
|
14
|
+
How the `goal` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
15
|
+
|
|
16
|
+
<img src="../../assets/tools/goal.png" alt="The goal tool's result: the call line and an indented body with the Goal header and the objective body" width="100%">
|
|
17
|
+
|
|
12
18
|
## What it does
|
|
13
19
|
|
|
14
20
|
- Registers one model-callable tool, `goal`, that records the session
|
|
@@ -110,14 +116,13 @@ The tool works in every mode. The persistent widget requires interactive
|
|
|
110
116
|
`/goal <prompt>` runs the loop: each model call uses a cancellable `tui` spinner
|
|
111
117
|
in interactive mode and runs directly in RPC; the questions and the confirm step
|
|
112
118
|
are host dialogs, forwarded by RPC. In print and JSON mode the loop is skipped
|
|
113
|
-
and the typed text is sent. The widget is always a single line; the transcript
|
|
114
|
-
|
|
119
|
+
and the typed text is sent. The widget is always a single line; the transcript
|
|
120
|
+
result is the status header alone, directly under the call line that names the
|
|
121
|
+
objective:
|
|
115
122
|
|
|
116
123
|
```
|
|
117
124
|
goal → set: Refactor the parser to support streaming input
|
|
118
|
-
|
|
119
|
-
Goal · active
|
|
120
|
-
⊚ Refactor the parser to support streaming input and ship it with tests
|
|
125
|
+
Goal · active
|
|
121
126
|
```
|
|
122
127
|
|
|
123
128
|
The renderer reuses the slot's component (`context.lastComponent`) rather than
|
|
@@ -205,9 +210,10 @@ depends on config.
|
|
|
205
210
|
hide all rails and restore them on close.
|
|
206
211
|
- **One-line widget, achieved hidden by default.** The rail follows the
|
|
207
212
|
`todo` grammar (`Goal · active · <objective>`): accent label leads, status
|
|
208
|
-
follows, no prefixed glyph, clipped to the terminal. The transcript
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
follows, no prefixed glyph, clipped to the terminal. The transcript result is
|
|
214
|
+
the status header alone (`Goal · active`), because the call line above it
|
|
215
|
+
already carries the objective; it does not restate it. `achieved: "show"`
|
|
216
|
+
restores the dimmed widget line; config only affects presentation.
|
|
211
217
|
- **Sanitize at the boundary.** `normalizeGoal` and `parseClarification` both route
|
|
212
218
|
model text through `lib/format.ts`'s `sanitizeLine`, so an embedded newline
|
|
213
219
|
cannot corrupt the widget and raw escapes cannot restyle the terminal.
|
|
@@ -223,9 +229,9 @@ depends on config.
|
|
|
223
229
|
| `clarify-parse.ts` | Parse the `{ understood, goal, questions }` reply (and the plain-text fallback), normalize questions, and expose the limits (pure). |
|
|
224
230
|
| `clarify.ts` | The two model calls, their cancellable loaders, the in-command questionnaire, and the confirm/edit dialog (host). |
|
|
225
231
|
| `state.ts` | Branch reconstruction (pure). |
|
|
226
|
-
| `format.ts` | Model-facing and terminal text plus the
|
|
232
|
+
| `format.ts` | Model-facing and terminal text plus the header and rail vocabulary (pure). |
|
|
227
233
|
| `config.ts` | Widget config load/validation (the `goal` section of `bytefly.json`). |
|
|
228
|
-
| `tui.ts` | The persistent goal widget and the transcript
|
|
234
|
+
| `tui.ts` | The persistent goal widget and the transcript status header. |
|
|
229
235
|
| `runtime.ts` | Session-scoped state and widget synchronization. |
|
|
230
236
|
| `tools.ts` | `goal` tool registration and rendering. |
|
|
231
237
|
| `commands.ts` | `/goal`. |
|
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model-facing and transcript text for the goal (pure).
|
|
3
3
|
*
|
|
4
|
-
* The transcript result is
|
|
5
|
-
* line
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
* (`Goal · active · <objective>`) matching the `todo` and `jobs` widgets. The
|
|
9
|
-
* symbols and labels live here; the rail indent constants are shared with
|
|
10
|
-
* `todo` in `lib/ui.ts`.
|
|
4
|
+
* The transcript result is the status header (`Goal · active`) under the call
|
|
5
|
+
* line, which already names the objective; the persistent widget is a plain
|
|
6
|
+
* label-first line (`Goal · active · <objective>`) matching the `todo` and
|
|
7
|
+
* `jobs` widgets. The status labels live here.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
14
11
|
import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
-
import {
|
|
12
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
13
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
16
14
|
import type { Goal, GoalStatus } from "./types.ts";
|
|
17
15
|
|
|
18
|
-
/** Active-goal glyph. */
|
|
19
|
-
export const ACTIVE_SYMBOL = GLYPHS.target;
|
|
20
|
-
/** Achieved-goal glyph. */
|
|
21
|
-
export const ACHIEVED_SYMBOL = GLYPHS.check;
|
|
22
|
-
|
|
23
16
|
/** Longest objective preview shown in the transcript call line. */
|
|
24
17
|
const CALL_PREVIEW_WIDTH = 48;
|
|
25
18
|
|
|
26
|
-
/** Themed status glyph, shared by the widget and the transcript renderer. */
|
|
27
|
-
export function goalGlyph(goal: Goal, theme: Theme): string {
|
|
28
|
-
return goal.status === "achieved" ? theme.fg("success", ACHIEVED_SYMBOL) : theme.fg("accent", ACTIVE_SYMBOL);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
19
|
/** Human label for a status, e.g. `active`. */
|
|
32
20
|
export function goalStatusLabel(status: GoalStatus): string {
|
|
33
21
|
return status === "achieved" ? "achieved" : "active";
|
|
@@ -38,10 +26,10 @@ export function goalStatusLabel(status: GoalStatus): string {
|
|
|
38
26
|
* the status word is dim while active, success-colored once achieved.
|
|
39
27
|
*/
|
|
40
28
|
export function goalHeader(goal: Goal, theme: Theme): string {
|
|
41
|
-
const label = theme
|
|
42
|
-
const separator = theme
|
|
29
|
+
const label = paint(theme, "accent", "Goal");
|
|
30
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
43
31
|
const word = goalStatusLabel(goal.status);
|
|
44
|
-
const status = goal.status === "achieved" ? theme
|
|
32
|
+
const status = goal.status === "achieved" ? paint(theme, "ok", word) : paint(theme, "faint", word);
|
|
45
33
|
return `${label}${separator}${status}`;
|
|
46
34
|
}
|
|
47
35
|
|
|
@@ -52,15 +40,15 @@ export function goalHeader(goal: Goal, theme: Theme): string {
|
|
|
52
40
|
* Callers clip it to the available width.
|
|
53
41
|
*/
|
|
54
42
|
export function goalLine(goal: Goal, theme: Theme): string {
|
|
55
|
-
const separator = theme
|
|
43
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
56
44
|
const word = goalStatusLabel(goal.status);
|
|
57
|
-
const status = goal.status === "achieved" ? theme
|
|
58
|
-
return `${theme
|
|
45
|
+
const status = goal.status === "achieved" ? paint(theme, "ok", word) : paint(theme, "faint", word);
|
|
46
|
+
return `${paint(theme, "accent", "Goal")}${separator}${status}${separator}${goalObjective(goal, theme)}`;
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
/** Themed objective text: dim once achieved, normal while active. */
|
|
62
50
|
export function goalObjective(goal: Goal, theme: Theme): string {
|
|
63
|
-
return goal.status === "achieved" ? theme
|
|
51
|
+
return goal.status === "achieved" ? paint(theme, "faint", goal.objective) : paint(theme, "label", goal.objective);
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
/** Model-facing result text. */
|
package/extensions/goal/tools.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
11
|
-
import type { ExtensionAPI
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
13
|
import { GOAL_TOOL } from "../../lib/tool-names.ts";
|
|
14
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
16
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
17
|
+
import { formatCallText, formatGoalText } from "./format.ts";
|
|
17
18
|
import type { GoalRuntime } from "./runtime.ts";
|
|
18
19
|
import { GoalParams, GoalResult, normalizeGoal, type GoalArgs } from "./schema.ts";
|
|
19
20
|
import { GoalResult as GoalResultView } from "./tui.ts";
|
|
@@ -21,9 +22,6 @@ import type { GoalAction, GoalDetails } from "./types.ts";
|
|
|
21
22
|
|
|
22
23
|
export const TOOL_NAME = GOAL_TOOL;
|
|
23
24
|
|
|
24
|
-
/** Longest objective shown in an unexpanded transcript result. */
|
|
25
|
-
const RESULT_PREVIEW_WIDTH = 72;
|
|
26
|
-
|
|
27
25
|
/** Best-effort action of a rejected call, for the error result's details. */
|
|
28
26
|
function attemptedAction(args: Partial<GoalArgs>): GoalAction {
|
|
29
27
|
if (args.status === "achieved") return "achieve";
|
|
@@ -31,14 +29,6 @@ function attemptedAction(args: Partial<GoalArgs>): GoalAction {
|
|
|
31
29
|
return "set";
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/** Themed body for the transcript result: the collapsed preview or the full objective. */
|
|
35
|
-
function resultBody(goal: NonNullable<GoalDetails["goal"]>, expanded: boolean, theme: Theme): string {
|
|
36
|
-
const text = expanded ? goal.objective : previewObjective(goal.objective, RESULT_PREVIEW_WIDTH);
|
|
37
|
-
// Route both states through `goalObjective`, so an achieved goal is dimmed
|
|
38
|
-
// whether it is collapsed or expanded.
|
|
39
|
-
return goalObjective({ objective: text, status: goal.status }, theme);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
/**
|
|
43
33
|
* JSON-safe mirror of the goal for `structuredContent`.
|
|
44
34
|
*
|
|
@@ -102,12 +92,12 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
102
92
|
const text = reuseText(context.lastComponent);
|
|
103
93
|
text.setText(
|
|
104
94
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
105
|
-
theme
|
|
95
|
+
paint(theme, "meta", formatCallText(args.objective, context.argsComplete, args.status)),
|
|
106
96
|
);
|
|
107
97
|
return text;
|
|
108
98
|
},
|
|
109
99
|
|
|
110
|
-
renderResult(result,
|
|
100
|
+
renderResult(result, _options, theme, context) {
|
|
111
101
|
const text = reuseText(context.lastComponent);
|
|
112
102
|
const details = toolDetails<GoalDetails>(result.details);
|
|
113
103
|
if (!details) {
|
|
@@ -116,20 +106,19 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
116
106
|
return text;
|
|
117
107
|
}
|
|
118
108
|
if (details.error) {
|
|
119
|
-
text.setText(theme
|
|
109
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
120
110
|
return text;
|
|
121
111
|
}
|
|
122
112
|
if (!details.goal) {
|
|
123
113
|
// A neutral marker, deliberately not the achieved check.
|
|
124
|
-
text.setText(theme
|
|
114
|
+
text.setText(paint(theme, "faint", `${GLYPHS.stopped} `) + paint(theme, "meta", "Cleared the goal"));
|
|
125
115
|
return text;
|
|
126
116
|
}
|
|
127
|
-
const body = resultBody(details.goal, expanded, theme);
|
|
128
117
|
const view =
|
|
129
118
|
context.lastComponent instanceof GoalResultView
|
|
130
119
|
? context.lastComponent
|
|
131
|
-
: new GoalResultView(details.goal,
|
|
132
|
-
view.update(details.goal,
|
|
120
|
+
: new GoalResultView(details.goal, theme);
|
|
121
|
+
view.update(details.goal, theme);
|
|
133
122
|
return view;
|
|
134
123
|
},
|
|
135
124
|
});
|
package/extensions/goal/tui.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal rendering for the goal.
|
|
3
3
|
*
|
|
4
|
-
* Two surfaces share one vocabulary.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* aligned under the text; it matches the todo transcript result's indent + glyph
|
|
10
|
-
* grammar so the two read as a pair.
|
|
11
|
-
* `GoalResult` wraps `goalRailLines` uncapped for the transcript.
|
|
4
|
+
* Two surfaces share one vocabulary. The persistent above-editor widget is a
|
|
5
|
+
* single label-first rail line (`Goal · active · <objective>`, dimmed once
|
|
6
|
+
* achieved), matching the `todo` and `jobs` widgets. The transcript result is
|
|
7
|
+
* the status header alone (`Goal · active`): the call line already names the
|
|
8
|
+
* objective, so the result only states the new status.
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { truncateToWidth,
|
|
16
|
-
import {
|
|
17
|
-
import { goalGlyph, goalHeader, goalLine } from "./format.ts";
|
|
12
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
13
|
+
import { goalHeader, goalLine } from "./format.ts";
|
|
18
14
|
import type { AchievedStyle, Goal } from "./types.ts";
|
|
19
15
|
|
|
20
16
|
/** Widget key used with `ctx.ui.setWidget()`. */
|
|
@@ -25,27 +21,6 @@ export interface GoalWidgetOptions {
|
|
|
25
21
|
achieved?: AchievedStyle;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
/**
|
|
29
|
-
* Rows for the transcript goal rail: the themed header plus `body` wrapped and
|
|
30
|
-
* led by the status glyph. Continuation rows are padded to the text column.
|
|
31
|
-
*/
|
|
32
|
-
export function goalRailLines(goal: Goal, body: string, theme: Theme, width: number): string[] {
|
|
33
|
-
const w = Math.max(1, width);
|
|
34
|
-
const glyph = goalGlyph(goal, theme);
|
|
35
|
-
const glyphWidth = visibleWidth(glyph);
|
|
36
|
-
const textColumn = BODY_INDENT + glyphWidth + GLYPH_GAP;
|
|
37
|
-
const inner = Math.max(1, w - textColumn);
|
|
38
|
-
|
|
39
|
-
const wrapped = wrapTextWithAnsi(body, inner);
|
|
40
|
-
const pad = " ".repeat(BODY_INDENT);
|
|
41
|
-
const continuation = " ".repeat(textColumn);
|
|
42
|
-
const lines = [goalHeader(goal, theme)];
|
|
43
|
-
wrapped.forEach((line, index) => {
|
|
44
|
-
lines.push(index === 0 ? `${pad}${glyph}${" ".repeat(GLYPH_GAP)}${line}` : `${continuation}${line}`);
|
|
45
|
-
});
|
|
46
|
-
return lines.map((line) => truncateToWidth(line, w, "…"));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
24
|
/** Rows for the goal widget: a single rail line, or nothing when hidden. */
|
|
50
25
|
export function goalWidgetLines(goal: Goal, theme: Theme, width: number, options: GoalWidgetOptions = {}): string[] {
|
|
51
26
|
const w = Math.max(1, width);
|
|
@@ -68,30 +43,27 @@ export class GoalWidget implements Component {
|
|
|
68
43
|
}
|
|
69
44
|
}
|
|
70
45
|
|
|
71
|
-
/** Transcript result block: the
|
|
46
|
+
/** Transcript result block: the status header, separated from the call line. */
|
|
72
47
|
export class GoalResult implements Component {
|
|
73
48
|
private goal: Goal;
|
|
74
|
-
private body: string;
|
|
75
49
|
private theme: Theme;
|
|
76
50
|
|
|
77
|
-
constructor(goal: Goal,
|
|
51
|
+
constructor(goal: Goal, theme: Theme) {
|
|
78
52
|
this.goal = goal;
|
|
79
|
-
this.body = body;
|
|
80
53
|
this.theme = theme;
|
|
81
54
|
}
|
|
82
55
|
|
|
83
56
|
/** Update in place so the transcript can reuse this component across renders. */
|
|
84
|
-
update(goal: Goal,
|
|
57
|
+
update(goal: Goal, theme: Theme): void {
|
|
85
58
|
this.goal = goal;
|
|
86
|
-
this.body = body;
|
|
87
59
|
this.theme = theme;
|
|
88
60
|
}
|
|
89
61
|
|
|
90
62
|
invalidate(): void {}
|
|
91
63
|
|
|
92
64
|
render(width: number): string[] {
|
|
93
|
-
// The leading blank line separates the
|
|
65
|
+
// The leading blank line separates the header from the call line, matching
|
|
94
66
|
// the built-ins' header/output gap.
|
|
95
|
-
return ["",
|
|
67
|
+
return ["", truncateToWidth(goalHeader(this.goal, this.theme), Math.max(1, width), "…")];
|
|
96
68
|
}
|
|
97
69
|
}
|
package/extensions/job/README.md
CHANGED
|
@@ -5,12 +5,20 @@ the background and manage them without blocking the turn. A job starts
|
|
|
5
5
|
immediately and keeps running across tool calls; poll it, tail its output, wait
|
|
6
6
|
for it, or kill it.
|
|
7
7
|
|
|
8
|
+
<img src="../../assets/job.png" alt="The /jobs screen: a summary line, the job rows with status glyphs and elapsed times, and the focused job's command, cwd, and last output line" width="100%">
|
|
9
|
+
|
|
8
10
|
```
|
|
9
11
|
pi --extension ./extensions/job # load just this extension
|
|
10
12
|
pi -e . # load the whole @gavin-hu/my-pi-agent package
|
|
11
13
|
pi install ./ # install the package
|
|
12
14
|
```
|
|
13
15
|
|
|
16
|
+
## Tool rendering
|
|
17
|
+
|
|
18
|
+
How the `job` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
19
|
+
|
|
20
|
+
<img src="../../assets/tools/job.png" alt="The job tool's result: the call line and an indented body with a finished and a failed job row, each with its status glyph and elapsed time" width="100%">
|
|
21
|
+
|
|
14
22
|
## What it does
|
|
15
23
|
|
|
16
24
|
- Registers one model-callable tool, `job`, with actions `start`, `list`,
|
|
@@ -81,7 +89,7 @@ JobDetails {
|
|
|
81
89
|
### Transcript
|
|
82
90
|
|
|
83
91
|
The call line names the action and its target. The result is themed and reuses
|
|
84
|
-
the slot's component: `list` draws the same
|
|
92
|
+
the slot's component: `list` draws the same rows as `/jobs`, `logs` draws a
|
|
85
93
|
blank line and the sanitized tail, and a single-job action draws one outcome
|
|
86
94
|
line. A `kill` that sent a signal appends it (`· SIGTERM sent`) instead of
|
|
87
95
|
showing the job's pre-kill running state, a `start` badges `detached` and
|
|
@@ -89,7 +97,6 @@ showing the job's pre-kill running state, a `start` badges `detached` and
|
|
|
89
97
|
|
|
90
98
|
```
|
|
91
99
|
job list
|
|
92
|
-
|
|
93
100
|
▸ j1 tests 3.2s
|
|
94
101
|
✗ j2 build 0.4s
|
|
95
102
|
```
|
|
@@ -199,8 +206,8 @@ rewritten.
|
|
|
199
206
|
| Execution mode | `sequential` — actions share the in-memory job table. |
|
|
200
207
|
| Annotations | `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true` — plan mode blocks it. |
|
|
201
208
|
| `outputSchema` / `structuredContent` | None. Results return `JobDetails` in tool-result `details` only. |
|
|
202
|
-
| State storage | One durable registry file per session, `~/.pi/agent/jobs/<projectKey>/registry-<sessionHash>.json` (keyed by the session cwd, so entering a worktree does not move it), merged from every session file on load; a dead session's records are adopted and a legacy shared `registry.json` is migrated once. Ids are reserved with an atomic `O_EXCL` `<id>.lock` before `start`, and a POSIX job's exit code is written to a per-job `.status` file. Tool results carry `JobDetails`; unreported completions arrive as
|
|
203
|
-
| Lifecycle | `session_start` loads and reconciles the registry and arms `onFinish` and `onComplete`; `session_shutdown` kills session-owned jobs and clears both status chips. `session_tree` repaints; `agent_start`/`agent_settled` track busy; `context` prunes stale
|
|
209
|
+
| State storage | One durable registry file per session, `~/.pi/agent/jobs/<projectKey>/registry-<sessionHash>.json` (keyed by the session cwd, so entering a worktree does not move it), merged from every session file on load; a dead session's records are adopted and a legacy shared `registry.json` is migrated once. Ids are reserved with an atomic `O_EXCL` `<id>.lock` before `start`, and a POSIX job's exit code is written to a per-job `.status` file. Tool results carry `JobDetails`; unreported completions arrive as a shared transcript notice (`my-pi-agent/notice`) at `before_agent_start`. |
|
|
210
|
+
| Lifecycle | `session_start` loads and reconciles the registry and arms `onFinish` and `onComplete`; `session_shutdown` kills session-owned jobs and clears both status chips. `session_tree` repaints; `agent_start`/`agent_settled` track busy; `context` prunes stale notices of its own source. |
|
|
204
211
|
| Event bus | Each job that reaches a terminal state is published on `my-pi-agent/job-finished` (`lib/events.ts`) with its id, label, status, exit code, and signal, from the one `settle` path every transition goes through. bytefly's notify capability subscribes there instead of importing this extension. |
|
|
205
212
|
|
|
206
213
|
## Design notes
|
|
@@ -248,11 +255,11 @@ rewritten.
|
|
|
248
255
|
settles it (recovering an exit code from the status file, else `unknown`)
|
|
249
256
|
when the pid disappears.
|
|
250
257
|
- **Notify by default, wake opt-in.** A failed job raises one `ctx.ui.notify`
|
|
251
|
-
(a clean exit is left to the list and chips). Finished jobs are also
|
|
252
|
-
as a
|
|
253
|
-
`wake: true` job (or `wakeOnFinish`) additionally
|
|
254
|
-
agent is idle; auto-wake is not the default because
|
|
255
|
-
tokens.
|
|
258
|
+
(a clean exit is left to the list and chips). Finished jobs are also posted
|
|
259
|
+
as a shared transcript notice — model-facing text plus the notice block — at the
|
|
260
|
+
next `before_agent_start`. A `wake: true` job (or `wakeOnFinish`) additionally
|
|
261
|
+
triggers one turn when the agent is idle; auto-wake is not the default because
|
|
262
|
+
it easily loops and burns tokens.
|
|
256
263
|
- **Width-1 glyphs and cached latest line.** The running chip is `▸ N` and the
|
|
257
264
|
failure chip `✗ N`, each a two-token badge under its own status key, so the
|
|
258
265
|
status bar compacts each independently (`▸N` / `✗N`) and an unreported
|
|
@@ -264,8 +271,10 @@ rewritten.
|
|
|
264
271
|
spawn/liveness/kill/clock functions, so the whole suite runs without
|
|
265
272
|
launching a process.
|
|
266
273
|
- **The transcript reuses the `/jobs` grammar.** `job list` renders the same
|
|
267
|
-
`jobRow`
|
|
268
|
-
|
|
274
|
+
`jobRow` rows as the screen (running first, capped, `… N more`), minus the
|
|
275
|
+
selection-marker column (`listRow`'s `marker: false`), which only the
|
|
276
|
+
interactive screen needs; `job logs` draws a themed tail with a `… N earlier
|
|
277
|
+
lines` note, and single-job actions
|
|
269
278
|
draw one `formatJobOutcomeLine` (plus a signal note on `kill` and
|
|
270
279
|
`detached`/`wake` badges on `start`). A capped collapsed result ends with the
|
|
271
280
|
`app.tools.expand` hint, and an expanded list shows every job. The renderers
|
|
@@ -289,10 +298,10 @@ rewritten.
|
|
|
289
298
|
| `session.ts` | Per-session heartbeat markers and owner-liveness rules. |
|
|
290
299
|
| `store.ts` | Durable store: registry dir, this session's atomic merge-write, deletions. |
|
|
291
300
|
| `logs.ts` | Bounded, sanitized log-tail reading. |
|
|
292
|
-
| `ui.ts` | Footer status chips. |
|
|
301
|
+
| `ui.ts` | Footer status chips, over the shared `lib/tui/status-controller.ts`. |
|
|
293
302
|
| `waiters.ts` | `wait` resolver bookkeeping. |
|
|
294
303
|
| `runtime.ts` | Composition: job table, handles, process events, repaint clock. |
|
|
295
|
-
| `tui.ts` | The `/jobs` screen and the transcript `JobResult`
|
|
304
|
+
| `tui.ts` | The `/jobs` screen and the transcript `JobResult` rows, drawn from the `lib/tui/` kit (`titleRule`, `listRow`, `emptyState`, `IdCursor`, `LinePane`). |
|
|
296
305
|
| `tools.ts` | The `job` tool and its transcript rendering. |
|
|
297
306
|
| `commands.ts` | The `/jobs` command. |
|
|
298
307
|
| `types.ts` | Shared types. |
|