@gavin-hu/my-pi-agent 0.19.0 → 0.21.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 +19 -12
- package/README.zh-CN.md +15 -9
- package/docs/extensions/README.md +2 -1
- package/docs/extensions/board.md +329 -151
- package/docs/extensions/bytefly.md +427 -273
- package/docs/extensions/file-browse.md +46 -44
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +86 -24
- package/docs/extensions/serve.md +129 -0
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +56 -28
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +48 -17
- package/extensions/board/format.ts +107 -9
- package/extensions/board/index.ts +14 -2
- package/extensions/board/join.ts +31 -10
- package/extensions/board/page.ts +202 -156
- package/extensions/board/result.ts +10 -24
- package/extensions/board/rows.ts +1 -1
- package/extensions/board/schema.ts +31 -10
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +7 -5
- package/extensions/board/tools.ts +211 -34
- package/extensions/board/transport/router.ts +27 -110
- package/extensions/board/transport/server.ts +76 -56
- package/extensions/board/types.ts +57 -0
- package/extensions/board/view/client.ts +24 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +6 -178
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +60 -89
- package/extensions/board/view/render.ts +228 -200
- package/extensions/board/view/styles.ts +72 -49
- package/extensions/board/view/team.ts +63 -41
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -11
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +93 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/panel/commands.ts +28 -1
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings.ts +72 -23
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/check/commands.ts +2 -1
- package/extensions/check/render.ts +10 -1
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +8 -547
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/index.ts +27 -48
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -6
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +78 -46
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +23 -26
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +27 -22
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/runtime.ts +4 -4
- package/extensions/job/tools.ts +19 -7
- package/extensions/job/tui.ts +2 -28
- package/extensions/plan/commands.ts +2 -2
- package/extensions/plan/feedback.ts +88 -18
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +24 -2
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +43 -4
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +10 -1
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tools.ts +2 -2
- package/extensions/plan/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +160 -0
- package/extensions/plan/web-chrome.ts +7 -5
- package/extensions/plan/web-client.ts +271 -325
- package/extensions/plan/web-collab.ts +7 -7
- package/extensions/plan/web-comments.ts +57 -16
- package/extensions/plan/web-composer.ts +91 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +258 -0
- package/extensions/plan/web-markdown.ts +1 -1
- package/extensions/plan/web-outline.ts +212 -0
- package/extensions/plan/web-page.ts +59 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +160 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +201 -0
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +77 -130
- package/extensions/rewind/commands/review.ts +2 -1
- package/extensions/serve/config.ts +37 -0
- package/extensions/serve/index.ts +71 -0
- package/extensions/serve/registry.ts +251 -0
- package/extensions/session-bus/join-request.ts +16 -9
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/rows.ts +7 -3
- package/extensions/session-bus/runtime.ts +100 -38
- package/extensions/session-bus/store.ts +14 -2
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +44 -9
- package/extensions/team/format.ts +14 -0
- package/extensions/team/index.ts +2 -1
- package/extensions/team/tools.ts +30 -9
- package/extensions/team/types.ts +17 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/todo/tui.ts +21 -3
- package/extensions/wechat/bridge.ts +43 -172
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +15 -285
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +9 -7
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +111 -32
- package/lib/board/round-text.ts +11 -2
- package/lib/board/round.ts +25 -20
- package/lib/board/rules.ts +144 -11
- package/lib/board/session.ts +84 -18
- package/lib/board/store.ts +268 -55
- package/lib/board/view.ts +114 -24
- package/lib/bus-request.ts +83 -0
- package/lib/format.ts +22 -0
- package/lib/http/index.ts +11 -7
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +25 -0
- package/lib/http/serve.ts +170 -43
- package/lib/http/server.ts +3 -3
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +20 -13
- package/lib/policy.ts +14 -1
- package/lib/requirements.ts +2 -2
- package/lib/session-label.ts +23 -7
- package/lib/session-roster.ts +74 -3
- package/lib/session-tool.ts +37 -21
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/README.md +6 -3
- package/lib/tui/glyphs.ts +3 -6
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tool.ts +30 -1
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +149 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +106 -2
- package/lib/web/names.ts +42 -0
- package/package.json +3 -2
- package/extensions/board/config.ts +0 -36
- package/extensions/board/view/actions.ts +0 -306
- package/extensions/board/writes.ts +0 -99
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/job/process.ts +0 -35
|
@@ -38,9 +38,9 @@ import {
|
|
|
38
38
|
sanitizeHtmlBlock,
|
|
39
39
|
sanitizeInlineTag,
|
|
40
40
|
} from "./raw-html.ts";
|
|
41
|
-
import {
|
|
41
|
+
import { emitAnchor, emitImage, emitImageLink } from "./emit.ts";
|
|
42
|
+
import { type LinkBase, resolveImage, resolveLink } from "./urls.ts";
|
|
42
43
|
|
|
43
|
-
const HTTP_SCHEME = /^https?:/i;
|
|
44
44
|
/** A GFM extended autolink: `http(s)://` or `www.` followed by a domain. */
|
|
45
45
|
const AUTOLINK = /^(?:https?:\/\/|www\.)[^\s<>]*/i;
|
|
46
46
|
|
|
@@ -65,12 +65,9 @@ interface LinkDefinition {
|
|
|
65
65
|
title?: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
* Per-document render state: the resolution base, the reference definitions,
|
|
70
|
-
* and the raw-HTML balancing state.
|
|
71
|
-
*/
|
|
68
|
+
/** Per-document render state: where links resolve, the definitions, and the raw-HTML sink. */
|
|
72
69
|
interface Context {
|
|
73
|
-
|
|
70
|
+
link: LinkBase;
|
|
74
71
|
defs: Map<string, LinkDefinition>;
|
|
75
72
|
/** Emit sanitized raw HTML; `false` escapes it as text. */
|
|
76
73
|
html: boolean;
|
|
@@ -82,6 +79,8 @@ interface Context {
|
|
|
82
79
|
export interface MarkdownOptions {
|
|
83
80
|
/** Emit sanitized raw HTML (the default) or escape it (`false`). */
|
|
84
81
|
html?: boolean;
|
|
82
|
+
/** The prefix local links are written under, e.g. `/files`; `""` for the origin's root. */
|
|
83
|
+
base?: string;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
/**
|
|
@@ -92,7 +91,7 @@ export interface MarkdownOptions {
|
|
|
92
91
|
export function renderMarkdown(source: string, baseDir = "", options: MarkdownOptions = {}): string {
|
|
93
92
|
const html = options.html ?? true;
|
|
94
93
|
const { defs, body } = collectDefinitions(source.split(/\r\n|\r|\n/), html);
|
|
95
|
-
const context = createContext(baseDir, defs, html);
|
|
94
|
+
const context = createContext({ dir: baseDir, base: options.base ?? "" }, defs, html);
|
|
96
95
|
const rendered = renderBlocks(body, context);
|
|
97
96
|
return html ? rendered + closeHtmlSink(context.sink) : rendered;
|
|
98
97
|
}
|
|
@@ -110,10 +109,10 @@ function headingSlug(text: string): string {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
/** Build the per-document render state, suffixing repeated heading slugs. */
|
|
113
|
-
function createContext(
|
|
112
|
+
function createContext(link: LinkBase, defs: Map<string, LinkDefinition>, html: boolean): Context {
|
|
114
113
|
const seen = new Map<string, number>();
|
|
115
114
|
return {
|
|
116
|
-
|
|
115
|
+
link,
|
|
117
116
|
defs,
|
|
118
117
|
html,
|
|
119
118
|
sink: createHtmlSink(),
|
|
@@ -338,7 +337,7 @@ function renderHtmlBlock(lines: string[], start: number, block: HtmlBlockStart,
|
|
|
338
337
|
if (isBlank(lines[i])) break;
|
|
339
338
|
collected.push(lines[i]);
|
|
340
339
|
}
|
|
341
|
-
return [sanitizeHtmlBlock(collected.join("\n"), ctx.
|
|
340
|
+
return [sanitizeHtmlBlock(collected.join("\n"), ctx.link, ctx.sink), i];
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
interface Fence {
|
|
@@ -641,22 +640,19 @@ function parseReferenceLink(text: string, start: number, ctx: Context): ParsedLi
|
|
|
641
640
|
|
|
642
641
|
function renderLink(parsed: ParsedLink, ctx: Context): string {
|
|
643
642
|
const label = renderInline(parsed.label, ctx);
|
|
644
|
-
const target = resolveLink(parsed.dest, ctx.
|
|
643
|
+
const target = resolveLink(parsed.dest, ctx.link);
|
|
645
644
|
if (!target) return label;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
return
|
|
645
|
+
// The anchor's shape is `emit.ts`'s, the same one the raw-HTML sanitizer emits: a link
|
|
646
|
+
// written in Markdown and the same link written as HTML are the same link.
|
|
647
|
+
return emitAnchor({ href: target.href, content: label, title: parsed.title, external: target.external });
|
|
649
648
|
}
|
|
650
649
|
|
|
651
650
|
function renderImage(parsed: ParsedLink, ctx: Context): string {
|
|
652
651
|
const alt = escapeHtml(parsed.label);
|
|
653
|
-
const target = resolveImage(parsed.dest, ctx.
|
|
652
|
+
const target = resolveImage(parsed.dest, ctx.link);
|
|
654
653
|
if (!target) return alt;
|
|
655
|
-
if (!target.image)
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
const title = parsed.title ? ` title="${escapeHtml(parsed.title)}"` : "";
|
|
659
|
-
return `<img class="md-img" src="${escapeHtml(target.href)}" alt="${alt}" loading="lazy" decoding="async"${title}>`;
|
|
654
|
+
if (!target.image) return emitImageLink(target.href, parsed.label || parsed.dest);
|
|
655
|
+
return emitImage({ href: target.href, alt, title: parsed.title });
|
|
660
656
|
}
|
|
661
657
|
|
|
662
658
|
function matchRun(text: string, start: number, char: string): number {
|
|
@@ -812,13 +808,15 @@ function renderInline(text: string, ctx: Context): string {
|
|
|
812
808
|
const match = /^<((?:https?|mailto):[^>\s]+)>/i.exec(text.slice(i));
|
|
813
809
|
if (match) {
|
|
814
810
|
const href = match[1];
|
|
815
|
-
|
|
816
|
-
|
|
811
|
+
// Resolved rather than pattern-matched: `urls.ts` owns which schemes open in a
|
|
812
|
+
// new tab, and a `<mailto:>` link is external but does not.
|
|
813
|
+
const external = resolveLink(href, ctx.link)?.external ?? false;
|
|
814
|
+
out += emitAnchor({ href, content: escapeHtml(href), external });
|
|
817
815
|
i += match[0].length;
|
|
818
816
|
continue;
|
|
819
817
|
}
|
|
820
818
|
if (ctx.html) {
|
|
821
|
-
const tag = sanitizeInlineTag(text, i, ctx.
|
|
819
|
+
const tag = sanitizeInlineTag(text, i, ctx.link, tags);
|
|
822
820
|
if (tag) {
|
|
823
821
|
out += tag.html;
|
|
824
822
|
i = tag.end;
|
|
@@ -836,8 +834,7 @@ function renderInline(text: string, ctx: Context): string {
|
|
|
836
834
|
// Require a domain, so a bare scheme is not a link.
|
|
837
835
|
if (dots > 0 && /\.[^./]/.test(label.slice(dots))) {
|
|
838
836
|
const href = /^www\./i.test(label) ? `http://${label}` : label;
|
|
839
|
-
|
|
840
|
-
out += `<a href="${escapeHtml(href)}"${external}>${escapeHtml(label)}</a>`;
|
|
837
|
+
out += emitAnchor({ href, content: escapeHtml(label), external: true });
|
|
841
838
|
i += label.length;
|
|
842
839
|
continue;
|
|
843
840
|
}
|
|
@@ -122,7 +122,7 @@ export const PALETTE_CLIENT_JS = ` // Quick open: a fuzzy path palette backed by
|
|
|
122
122
|
|
|
123
123
|
function runSearch() {
|
|
124
124
|
var query = paletteInput.value.trim();
|
|
125
|
-
fetch("/api/files?q=" + encodeURIComponent(query))
|
|
125
|
+
fetch(pageBase() + "/api/files?q=" + encodeURIComponent(query))
|
|
126
126
|
.then(function (response) {
|
|
127
127
|
if (!response.ok) throw new Error("search failed");
|
|
128
128
|
return response.json();
|
|
@@ -212,7 +212,7 @@ export const PALETTE_CLIENT_JS = ` // Quick open: a fuzzy path palette backed by
|
|
|
212
212
|
|
|
213
213
|
function openActive() {
|
|
214
214
|
var result = paletteResults[paletteActive];
|
|
215
|
-
if (result) window.location.assign("/view/" + encodeSegments(result.path));
|
|
215
|
+
if (result) window.location.assign(pageBase() + "/view/" + encodeSegments(result.path));
|
|
216
216
|
}
|
|
217
217
|
var quickOpenButton = document.getElementById("quick-open");
|
|
218
218
|
if (quickOpenButton) quickOpenButton.addEventListener("click", openPalette);
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { decodeCodePoint, escapeHtml } from "../../../lib/escape.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { emitImage, emitImageLink, EXTERNAL_ATTRS } from "./emit.ts";
|
|
30
|
+
import { type LinkBase, resolveImage, resolveLink } from "./urls.ts";
|
|
30
31
|
|
|
31
32
|
/** Elements re-emitted with sanitized attributes. */
|
|
32
33
|
const BLOCK_ELEMENTS = new Set([
|
|
@@ -139,6 +140,12 @@ const BLOCK_OPEN = /^ {0,3}<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/;
|
|
|
139
140
|
const COMMENT_OPEN = /^ {0,3}<!--/;
|
|
140
141
|
const DECLARATION_OPEN = /^ {0,3}<[!?]/;
|
|
141
142
|
/** The five XML references, plus numeric ones, are the only refs decoded in a value. */
|
|
143
|
+
/**
|
|
144
|
+
* The five references an attribute value may carry, decoded.
|
|
145
|
+
*
|
|
146
|
+
* Not the Markdown renderer's table: a tag's attribute is not running text, and decoding
|
|
147
|
+
* ` ` or `—` inside one would invent characters the author did not write.
|
|
148
|
+
*/
|
|
142
149
|
const ATTRIBUTE_ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
143
150
|
const ATTRIBUTE_ENTITY = /&(?:#([0-9]{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z]+));/g;
|
|
144
151
|
|
|
@@ -198,7 +205,7 @@ export function closesSuppressed(line: string, name: string): boolean {
|
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
/** Sanitize one HTML block, emitting text and allowlisted tags into `sink`. */
|
|
201
|
-
export function sanitizeHtmlBlock(source: string,
|
|
208
|
+
export function sanitizeHtmlBlock(source: string, linkBase: LinkBase, sink: HtmlSink): string {
|
|
202
209
|
let out = "";
|
|
203
210
|
let i = 0;
|
|
204
211
|
while (i < source.length) {
|
|
@@ -228,7 +235,7 @@ export function sanitizeHtmlBlock(source: string, baseDir: string, sink: HtmlSin
|
|
|
228
235
|
out += closeTo(sink.stack, sink.stack.lastIndexOf(tag.name));
|
|
229
236
|
continue;
|
|
230
237
|
}
|
|
231
|
-
out += tag.name === "img" ? sanitizeImage(tag,
|
|
238
|
+
out += tag.name === "img" ? sanitizeImage(tag, linkBase) : sanitizeOpenTag(tag, linkBase);
|
|
232
239
|
if (tag.name !== "img" && !VOID.has(tag.name) && !tag.selfClosing) sink.stack.push(tag.name);
|
|
233
240
|
}
|
|
234
241
|
return out;
|
|
@@ -243,7 +250,7 @@ export function sanitizeHtmlBlock(source: string, baseDir: string, sink: HtmlSin
|
|
|
243
250
|
export function sanitizeInlineTag(
|
|
244
251
|
source: string,
|
|
245
252
|
start: number,
|
|
246
|
-
|
|
253
|
+
linkBase: LinkBase,
|
|
247
254
|
stack: string[],
|
|
248
255
|
): { html: string; end: number } | undefined {
|
|
249
256
|
const tag = parseRawTag(source, start);
|
|
@@ -253,7 +260,7 @@ export function sanitizeInlineTag(
|
|
|
253
260
|
if (SUPPRESSED.has(tag.name)) return { html: "", end: skipSuppressed(source, tag.next, tag) };
|
|
254
261
|
if (!ALLOWED.has(tag.name) || !PHRASING.has(tag.name)) return undefined;
|
|
255
262
|
if (tag.end) return { html: closeTo(stack, stack.lastIndexOf(tag.name)), end: tag.next };
|
|
256
|
-
const html = tag.name === "img" ? sanitizeImage(tag,
|
|
263
|
+
const html = tag.name === "img" ? sanitizeImage(tag, linkBase) : sanitizeOpenTag(tag, linkBase);
|
|
257
264
|
if (tag.name !== "img" && !VOID.has(tag.name) && !tag.selfClosing) stack.push(tag.name);
|
|
258
265
|
return { html, end: tag.next };
|
|
259
266
|
}
|
|
@@ -395,7 +402,7 @@ function skipSuppressed(source: string, from: number, tag: RawTag): number {
|
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
/** Rebuild an allowlisted start tag from its parsed name and allowed attributes. */
|
|
398
|
-
function sanitizeOpenTag(tag: RawTag,
|
|
405
|
+
function sanitizeOpenTag(tag: RawTag, linkBase: LinkBase): string {
|
|
399
406
|
const out: string[] = [];
|
|
400
407
|
const id = attribute(tag, "id");
|
|
401
408
|
if (id !== undefined && id !== "") out.push(` id="${escapeHtml(id)}"`);
|
|
@@ -404,10 +411,10 @@ function sanitizeOpenTag(tag: RawTag, baseDir: string): string {
|
|
|
404
411
|
switch (tag.name) {
|
|
405
412
|
case "a": {
|
|
406
413
|
const href = attribute(tag, "href");
|
|
407
|
-
const target = href === undefined ? undefined : resolveLink(href,
|
|
414
|
+
const target = href === undefined ? undefined : resolveLink(href, linkBase);
|
|
408
415
|
if (target !== undefined) {
|
|
409
416
|
out.push(` href="${escapeHtml(target.href)}"`);
|
|
410
|
-
if (target.external) out.push(
|
|
417
|
+
if (target.external) out.push(EXTERNAL_ATTRS);
|
|
411
418
|
}
|
|
412
419
|
break;
|
|
413
420
|
}
|
|
@@ -451,21 +458,19 @@ function sanitizeOpenTag(tag: RawTag, baseDir: string): string {
|
|
|
451
458
|
* (the page CSP forbids remote images), and an unresolvable one leaves the alt
|
|
452
459
|
* text, so a broken badge is still readable.
|
|
453
460
|
*/
|
|
454
|
-
function sanitizeImage(tag: RawTag,
|
|
461
|
+
function sanitizeImage(tag: RawTag, linkBase: LinkBase): string {
|
|
455
462
|
const alt = attribute(tag, "alt") ?? "";
|
|
456
463
|
const src = attribute(tag, "src");
|
|
457
|
-
const target = src === undefined ? undefined : resolveImage(src,
|
|
464
|
+
const target = src === undefined ? undefined : resolveImage(src, linkBase);
|
|
458
465
|
if (target === undefined) return alt === "" ? "" : escapeHtml(alt);
|
|
459
|
-
if (!target.image)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (title !== undefined) out.push(` title="${escapeHtml(title)}"`);
|
|
470
|
-
return `<img${out.join("")} loading="lazy" decoding="async">`;
|
|
466
|
+
if (!target.image) return emitImageLink(target.href, alt === "" ? target.href : alt);
|
|
467
|
+
// The element is `emit.ts`'s, so an `<img>` written as HTML and the same one written as
|
|
468
|
+
// Markdown are the same element, down to the lazy decoding.
|
|
469
|
+
return emitImage({
|
|
470
|
+
href: target.href,
|
|
471
|
+
alt,
|
|
472
|
+
title: attribute(tag, "title"),
|
|
473
|
+
width: numeric(attribute(tag, "width")),
|
|
474
|
+
height: numeric(attribute(tag, "height")),
|
|
475
|
+
});
|
|
471
476
|
}
|
|
@@ -189,7 +189,7 @@ export const TREE_CLIENT_JS = ` var tree = document.querySelector("nav.bf-tree")
|
|
|
189
189
|
li.dataset.loading = "true";
|
|
190
190
|
twisty.textContent = "\\u2026";
|
|
191
191
|
if (link) link.setAttribute("aria-busy", "true");
|
|
192
|
-
fetch("/api/tree?path=" + encodeURIComponent(path))
|
|
192
|
+
fetch(pageBase() + "/api/tree?path=" + encodeURIComponent(path))
|
|
193
193
|
.then(function (response) {
|
|
194
194
|
if (!response.ok) throw new Error("load failed");
|
|
195
195
|
return response.json();
|
|
@@ -26,6 +26,20 @@ export interface ResolvedLink {
|
|
|
26
26
|
external: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Where a destination resolves from, and what it is written under.
|
|
31
|
+
*
|
|
32
|
+
* `dir` is the root-relative directory the destination is relative to; `base` is the
|
|
33
|
+
* prefix the page answers under, so a page served at `/files` writes
|
|
34
|
+
* `/files/view/<path>` and one holding the origin's root writes `/view/<path>`. The two
|
|
35
|
+
* are one value because a caller that knows one almost always knows the other, and a
|
|
36
|
+
* second parameter is a second thing to thread through ten functions.
|
|
37
|
+
*/
|
|
38
|
+
export interface LinkBase {
|
|
39
|
+
dir: string;
|
|
40
|
+
base: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/** An image destination the page may emit; `image: false` means link to it instead. */
|
|
30
44
|
export interface ResolvedImage {
|
|
31
45
|
href: string;
|
|
@@ -53,15 +67,15 @@ function resolveLocalPath(href: string, baseDir: string): { rel: string; suffix:
|
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/** Resolve a link destination, or `undefined` when the scheme or path is not allowed. */
|
|
56
|
-
export function resolveLink(dest: string,
|
|
70
|
+
export function resolveLink(dest: string, linkBase: LinkBase): ResolvedLink | undefined {
|
|
57
71
|
const value = dest.trim();
|
|
58
72
|
if (value === "") return undefined;
|
|
59
73
|
if (value.startsWith("#")) return { href: value, external: false };
|
|
60
74
|
if (SAFE_LINK_SCHEME.test(value)) return { href: value, external: HTTP_SCHEME.test(value) };
|
|
61
75
|
if (HAS_SCHEME.test(value)) return undefined;
|
|
62
|
-
const local = resolveLocalPath(value,
|
|
76
|
+
const local = resolveLocalPath(value, linkBase.dir);
|
|
63
77
|
if (!local) return undefined;
|
|
64
|
-
return { href:
|
|
78
|
+
return { href: `${linkBase.base}/view/${encodePath(local.rel)}${local.suffix}`, external: false };
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/**
|
|
@@ -69,12 +83,12 @@ export function resolveLink(dest: string, baseDir: string): ResolvedLink | undef
|
|
|
69
83
|
* URL resolves with `image: false` because the page CSP forbids remote images, so
|
|
70
84
|
* the caller renders a link instead of an `<img>`.
|
|
71
85
|
*/
|
|
72
|
-
export function resolveImage(dest: string,
|
|
86
|
+
export function resolveImage(dest: string, linkBase: LinkBase): ResolvedImage | undefined {
|
|
73
87
|
const value = dest.trim();
|
|
74
88
|
if (value === "") return undefined;
|
|
75
89
|
if (HTTP_SCHEME.test(value)) return { href: value, image: false };
|
|
76
90
|
if (HAS_SCHEME.test(value)) return undefined;
|
|
77
|
-
const local = resolveLocalPath(value,
|
|
91
|
+
const local = resolveLocalPath(value, linkBase.dir);
|
|
78
92
|
if (!local) return undefined;
|
|
79
|
-
return { href:
|
|
93
|
+
return { href: `${linkBase.base}/raw/${encodePath(local.rel)}${local.suffix}`, image: true };
|
|
80
94
|
}
|
package/extensions/goal/tui.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
13
|
+
import { RenderCache } from "../../lib/tui/render-cache.ts";
|
|
13
14
|
import { goalHeader, goalLine } from "./format.ts";
|
|
14
15
|
import type { AchievedStyle, Goal } from "./types.ts";
|
|
15
16
|
|
|
@@ -47,6 +48,7 @@ export class GoalWidget implements Component {
|
|
|
47
48
|
export class GoalHeader implements Component {
|
|
48
49
|
private goal: Goal;
|
|
49
50
|
private theme: Theme;
|
|
51
|
+
private readonly cache = new RenderCache();
|
|
50
52
|
|
|
51
53
|
constructor(goal: Goal, theme: Theme) {
|
|
52
54
|
this.goal = goal;
|
|
@@ -57,13 +59,21 @@ export class GoalHeader implements Component {
|
|
|
57
59
|
update(goal: Goal, theme: Theme): void {
|
|
58
60
|
this.goal = goal;
|
|
59
61
|
this.theme = theme;
|
|
62
|
+
this.cache.invalidate();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
invalidate(): void {
|
|
65
|
+
invalidate(): void {
|
|
66
|
+
this.cache.invalidate();
|
|
67
|
+
}
|
|
63
68
|
|
|
64
69
|
render(width: number): string[] {
|
|
70
|
+
const w = Math.max(1, width);
|
|
71
|
+
return this.cache.lines(w, undefined, () => this.build(w));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private build(w: number): string[] {
|
|
65
75
|
// The leading blank line separates the header from the call line, matching
|
|
66
76
|
// the built-ins' header/output gap.
|
|
67
|
-
return ["", truncateToWidth(goalHeader(this.goal, this.theme),
|
|
77
|
+
return ["", truncateToWidth(goalHeader(this.goal, this.theme), w, "…")];
|
|
68
78
|
}
|
|
69
79
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type KillTreeFn,
|
|
30
30
|
type SpawnedProcess,
|
|
31
31
|
type SpawnFn,
|
|
32
|
+
startTokenOf,
|
|
32
33
|
type StdinSink,
|
|
33
34
|
} from "../../lib/spawn.ts";
|
|
34
35
|
import { loadConfig, type JobsConfig } from "./config.ts";
|
|
@@ -38,7 +39,6 @@ import { defaultInterval } from "../../lib/timer.ts";
|
|
|
38
39
|
import { clipLabel } from "../../lib/tui/columns.ts";
|
|
39
40
|
import { compareJobs, formatLogs } from "./format.ts";
|
|
40
41
|
import { readLogTail } from "./logs.ts";
|
|
41
|
-
import { defaultStartToken, type LivenessFn, type StartTokenFn } from "./process.ts";
|
|
42
42
|
import {
|
|
43
43
|
loadRegistries,
|
|
44
44
|
mergeRecords,
|
|
@@ -114,9 +114,9 @@ export interface SendResult {
|
|
|
114
114
|
|
|
115
115
|
export interface RuntimeOptions {
|
|
116
116
|
spawn?: SpawnFn;
|
|
117
|
-
liveness?:
|
|
117
|
+
liveness?: typeof isPidAlive;
|
|
118
118
|
killTree?: KillTreeFn;
|
|
119
|
-
startToken?:
|
|
119
|
+
startToken?: typeof startTokenOf;
|
|
120
120
|
now?: () => number;
|
|
121
121
|
/** Platform seam: whether this is Windows. Defaults to the running platform. */
|
|
122
122
|
isWindows?: () => boolean;
|
|
@@ -183,7 +183,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
183
183
|
const spawn = options.spawn ?? defaultSpawn;
|
|
184
184
|
const liveness = options.liveness ?? isPidAlive;
|
|
185
185
|
const killTree = options.killTree ?? defaultKillTree;
|
|
186
|
-
const startToken = options.startToken ??
|
|
186
|
+
const startToken = options.startToken ?? startTokenOf;
|
|
187
187
|
const now = options.now ?? (() => Date.now());
|
|
188
188
|
const isWindows = options.isWindows ?? (() => process.platform === "win32");
|
|
189
189
|
|
package/extensions/job/tools.ts
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { errorResult, textResult } from "../../lib/tool-result.ts";
|
|
13
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
callLine,
|
|
16
|
+
detailsOrLine,
|
|
17
|
+
elapsedSuffix,
|
|
18
|
+
isTailResult,
|
|
19
|
+
rejectionLine,
|
|
20
|
+
syncElapsedTimer,
|
|
21
|
+
TailResult,
|
|
22
|
+
} from "../../lib/tui/tool.ts";
|
|
15
23
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
24
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
25
|
import {
|
|
@@ -26,8 +34,8 @@ import {
|
|
|
26
34
|
import type { JobsRuntime } from "./runtime.ts";
|
|
27
35
|
import { JobParams, normalizeCall, type JobArgs, type JobCall } from "./schema.ts";
|
|
28
36
|
import { toRecord } from "./store.ts";
|
|
29
|
-
import {
|
|
30
|
-
import type { JobDetails } from "./types.ts";
|
|
37
|
+
import { jobTail, type JobResultInput } from "./tui.ts";
|
|
38
|
+
import type { JobDetails, JobRecord } from "./types.ts";
|
|
31
39
|
|
|
32
40
|
export const TOOL_NAME = "job";
|
|
33
41
|
|
|
@@ -224,8 +232,10 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
224
232
|
}
|
|
225
233
|
const shown = options.expanded ? all : all.slice(Math.max(0, all.length - RESULT_ROWS));
|
|
226
234
|
const input: JobResultInput = { kind: "logs", lines: shown, earlier: all.length - shown.length };
|
|
227
|
-
const
|
|
228
|
-
|
|
235
|
+
const previous = context.lastComponent;
|
|
236
|
+
const tail = jobTail(input);
|
|
237
|
+
const view = isTailResult<JobRecord>(previous) ? previous : new TailResult<JobRecord>(tail, theme);
|
|
238
|
+
view.setInput(tail, theme);
|
|
229
239
|
return view;
|
|
230
240
|
}
|
|
231
241
|
|
|
@@ -242,8 +252,10 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
242
252
|
more: ordered.length - shown.length,
|
|
243
253
|
sessionId: details.sessionId,
|
|
244
254
|
};
|
|
245
|
-
const
|
|
246
|
-
|
|
255
|
+
const previous = context.lastComponent;
|
|
256
|
+
const tail = jobTail(input);
|
|
257
|
+
const view = isTailResult<JobRecord>(previous) ? previous : new TailResult<JobRecord>(tail, theme);
|
|
258
|
+
view.setInput(tail, theme);
|
|
247
259
|
return view;
|
|
248
260
|
}
|
|
249
261
|
|
package/extensions/job/tui.ts
CHANGED
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
19
18
|
import { formatDuration, sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
20
19
|
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
21
20
|
import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
|
|
22
21
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
23
22
|
import { statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
24
|
-
import {
|
|
23
|
+
import type { TailResultInput } from "../../lib/tui/tool.ts";
|
|
25
24
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
26
25
|
import {
|
|
27
26
|
compareJobs,
|
|
@@ -62,7 +61,7 @@ export type JobResultInput =
|
|
|
62
61
|
| { kind: "logs"; lines: string[]; earlier: number };
|
|
63
62
|
|
|
64
63
|
/** Map the job vocabulary onto the shared transcript tail block. */
|
|
65
|
-
function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
64
|
+
export function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
66
65
|
if (input.kind === "logs") {
|
|
67
66
|
return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: "lines" };
|
|
68
67
|
}
|
|
@@ -77,31 +76,6 @@ function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
|
77
76
|
};
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
/**
|
|
81
|
-
* Transcript result block for the `job` tool: the shared tail block over the
|
|
82
|
-
* `/jobs`-style row rail or the themed log tail. Reused across renders.
|
|
83
|
-
*/
|
|
84
|
-
export class JobResult implements Component {
|
|
85
|
-
private readonly view: TailResult<JobRecord>;
|
|
86
|
-
|
|
87
|
-
constructor(input: JobResultInput, theme: Theme) {
|
|
88
|
-
this.view = new TailResult(jobTail(input), theme);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Update in place so the transcript can reuse this component across renders. */
|
|
92
|
-
setInput(input: JobResultInput, theme: Theme): void {
|
|
93
|
-
this.view.setInput(jobTail(input), theme);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
invalidate(): void {
|
|
97
|
-
this.view.invalidate();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
render(width: number): string[] {
|
|
101
|
-
return this.view.render(width);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
79
|
export interface JobListCallbacks {
|
|
106
80
|
/** This session's id: a record with another id belongs to a peer session. */
|
|
107
81
|
sessionId(): string;
|
|
@@ -19,7 +19,7 @@ import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
|
|
|
19
19
|
import { runBrowserReview } from "./review.ts";
|
|
20
20
|
import type { PlanRuntime } from "./runtime.ts";
|
|
21
21
|
import type { PlanSummary } from "./plans.ts";
|
|
22
|
-
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
22
|
+
import { createPlanWebReview, NO_SERVER_DEPS, type PlanWebReview } from "./web.ts";
|
|
23
23
|
|
|
24
24
|
const ENABLED_NOTICE = `Plan mode enabled — ${READ_ONLY_SUMMARY}.`;
|
|
25
25
|
const DISABLED_NOTICE = "Plan mode disabled — full access restored.";
|
|
@@ -219,7 +219,7 @@ async function openPlanIndex(
|
|
|
219
219
|
export function registerCommands(
|
|
220
220
|
pi: ExtensionAPI,
|
|
221
221
|
runtime: PlanRuntime,
|
|
222
|
-
web: PlanWebReview = createPlanWebReview(runtime.plans),
|
|
222
|
+
web: PlanWebReview = createPlanWebReview(runtime.plans, NO_SERVER_DEPS),
|
|
223
223
|
): void {
|
|
224
224
|
pi.registerCommand("plan", {
|
|
225
225
|
description:
|