@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
package/lib/tui/render-cache.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
export class RenderCache {
|
|
32
32
|
private width: number | null = null;
|
|
33
33
|
private source: readonly string[] | undefined;
|
|
34
|
+
/** The last key's parts, for the value-keyed form; `undefined` until one is set. */
|
|
35
|
+
private parts: readonly unknown[] | undefined;
|
|
34
36
|
private cached: string[] = [];
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -47,9 +49,36 @@ export class RenderCache {
|
|
|
47
49
|
return this.cached;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The lines from `build()`, for a component that derives them from a key.
|
|
54
|
+
*
|
|
55
|
+
* The identity form above chains on the inner component's array. A component with no
|
|
56
|
+
* inner component has nothing to chain on — a header's view and a footer's snapshot are
|
|
57
|
+
* rebuilt every frame — so it describes its inputs instead, and this compares them:
|
|
58
|
+
* primitives by value, and anything else by identity (a theme object is the case that
|
|
59
|
+
* matters, since its strings are what get cached).
|
|
60
|
+
*
|
|
61
|
+
* The key is only as complete as the caller makes it: a part left out is a frame that
|
|
62
|
+
* shows stale lines. Positions are compared, so an undefined part is a real one.
|
|
63
|
+
*/
|
|
64
|
+
keyed(parts: readonly unknown[], build: () => string[]): string[] {
|
|
65
|
+
const previous = this.parts;
|
|
66
|
+
if (
|
|
67
|
+
previous !== undefined &&
|
|
68
|
+
previous.length === parts.length &&
|
|
69
|
+
previous.every((part, at) => part === parts[at])
|
|
70
|
+
) {
|
|
71
|
+
return this.cached;
|
|
72
|
+
}
|
|
73
|
+
this.cached = build();
|
|
74
|
+
this.parts = [...parts];
|
|
75
|
+
return this.cached;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/** Drop the memo; call from the component's `invalidate()`. */
|
|
51
79
|
invalidate(): void {
|
|
52
80
|
this.width = null;
|
|
81
|
+
this.parts = undefined;
|
|
53
82
|
this.cached = [];
|
|
54
83
|
}
|
|
55
84
|
}
|
package/lib/tui/tool.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
22
22
|
import { formatDuration, sanitizeLine, stripControlChars } from "../format.ts";
|
|
23
23
|
import { clip, clipMiddle } from "./columns.ts";
|
|
24
24
|
import { expandHint, SEPARATORS } from "./glyphs.ts";
|
|
25
|
+
import { RenderCache } from "./render-cache.ts";
|
|
25
26
|
import { paint } from "./tokens.ts";
|
|
26
27
|
|
|
27
28
|
/** The parts of a tool result a renderer reads. */
|
|
@@ -223,10 +224,18 @@ export type TailResultInput<Row> =
|
|
|
223
224
|
* section, or the themed tail with an "earlier" note. The caller owns the row
|
|
224
225
|
* renderer, so the extension's own row grammar — a peer-session tag, a status
|
|
225
226
|
* glyph — stays with its vocabulary.
|
|
227
|
+
*
|
|
228
|
+
* `render` runs on every frame Pi draws, for every block in the transcript, and
|
|
229
|
+
* this block is the shared one behind the board's card, a session's peer list and
|
|
230
|
+
* a shaped tail — each of which re-derived its rows and clipped every line per
|
|
231
|
+
* frame before this memo. The lines depend on the input, the theme and the width,
|
|
232
|
+
* and none of them changes without `setInput`, `invalidate`, or a new width, so
|
|
233
|
+
* the memo is keyed on the width and dropped by those two calls.
|
|
226
234
|
*/
|
|
227
235
|
export class TailResult<Row> implements Component {
|
|
228
236
|
private input: TailResultInput<Row>;
|
|
229
237
|
private theme: Theme;
|
|
238
|
+
private readonly cache = new RenderCache();
|
|
230
239
|
|
|
231
240
|
constructor(input: TailResultInput<Row>, theme: Theme) {
|
|
232
241
|
this.input = input;
|
|
@@ -237,12 +246,19 @@ export class TailResult<Row> implements Component {
|
|
|
237
246
|
setInput(input: TailResultInput<Row>, theme: Theme): void {
|
|
238
247
|
this.input = input;
|
|
239
248
|
this.theme = theme;
|
|
249
|
+
this.cache.invalidate();
|
|
240
250
|
}
|
|
241
251
|
|
|
242
|
-
invalidate(): void {
|
|
252
|
+
invalidate(): void {
|
|
253
|
+
this.cache.invalidate();
|
|
254
|
+
}
|
|
243
255
|
|
|
244
256
|
render(width: number): string[] {
|
|
245
257
|
const w = Math.max(1, width);
|
|
258
|
+
return this.cache.lines(w, undefined, () => this.build(w));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private build(w: number): string[] {
|
|
246
262
|
const lines: string[] = [""];
|
|
247
263
|
const input = this.input;
|
|
248
264
|
if (input.kind === "list") {
|
|
@@ -264,6 +280,19 @@ export class TailResult<Row> implements Component {
|
|
|
264
280
|
}
|
|
265
281
|
}
|
|
266
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Whether the component the transcript handed back is this kit's tail block.
|
|
285
|
+
*
|
|
286
|
+
* `TailResult` carries its row type and `instanceof` erases it. The caller is the
|
|
287
|
+
* one that knows which rows it drew, so the claim lives here as a guard rather
|
|
288
|
+
* than as a cast in each extension that reuses its block through
|
|
289
|
+
* `context.lastComponent` — and a component from another tool can only reach that
|
|
290
|
+
* check by being this class, which is why the erased type is safe to assert.
|
|
291
|
+
*/
|
|
292
|
+
export function isTailResult<Row>(value: unknown): value is TailResult<Row> {
|
|
293
|
+
return value instanceof TailResult;
|
|
294
|
+
}
|
|
295
|
+
|
|
267
296
|
/** The render context fields the elapsed timer needs. */
|
|
268
297
|
export interface ToolRenderClock {
|
|
269
298
|
/** Pi's per-call render state, opaque here: this module only ever casts it. */
|
package/lib/web/README.md
CHANGED
|
@@ -21,6 +21,47 @@ counts its own stylesheet (`pages.test.ts`, `web.test.ts`). A page rule is
|
|
|
21
21
|
namespaced (`review-*`, `browse-*`) and overrides a kit class rather than
|
|
22
22
|
re-declaring it.
|
|
23
23
|
|
|
24
|
+
## One browser runtime, spliced in
|
|
25
|
+
|
|
26
|
+
Three surfaces render pages from a bundle held as a template literal, and each had written
|
|
27
|
+
the same three helpers for itself: an element builder, a "did this key land in a field?"
|
|
28
|
+
test, and a status line's paint. No type checker and no AST scan compares text inside a
|
|
29
|
+
string, so the plan's `el(tag, className)` and the board's `el(tag, className, text)` were
|
|
30
|
+
two builders, and the plan compared `INPUT` where the board lowercased the tag.
|
|
31
|
+
|
|
32
|
+
`client-runtime.ts` holds one body of each, as ordinary TypeScript, and exports
|
|
33
|
+
`CLIENT_RUNTIME_JS` — the four functions as source, in the order a client reads them. A
|
|
34
|
+
page carries it the way it carries `plural` and its class table: the source crosses the
|
|
35
|
+
boundary, and the page runs the function instead of a copy of it.
|
|
36
|
+
|
|
37
|
+
The bodies are JavaScript on purpose — no annotation survives into the string, and no
|
|
38
|
+
backtick or `${` may appear in one. The DOM is reached through `globalThis`, because this
|
|
39
|
+
project compiles without a DOM lib; the cast names the guarantee the page makes.
|
|
40
|
+
|
|
41
|
+
What stays page-side: what a status *says*. `paintStatus` owns the text, its tooltip, and
|
|
42
|
+
the `bf-status--<tone>` class; the board composes `live · 3m` and appends its own class.
|
|
43
|
+
A shared runtime that also decided the wording would be a second copy of the page's voice.
|
|
44
|
+
|
|
45
|
+
## Name a class once, in a table
|
|
46
|
+
|
|
47
|
+
A page's document is TypeScript and its client is a string inside a template literal, so
|
|
48
|
+
the two halves cannot import from each other: a class name used to be a literal in each,
|
|
49
|
+
and one assembled at runtime (`"board-card__reason" + kind`) could not be found by
|
|
50
|
+
reading either half. That is how a class reaches a stylesheet with no rule behind it —
|
|
51
|
+
an unstyled element, and nothing fails.
|
|
52
|
+
|
|
53
|
+
So a page declares its vocabulary once, in a `ClassNames` table beside its own modules,
|
|
54
|
+
and both halves read it: the document interpolates `${N.toolbar}`, the bundle carries
|
|
55
|
+
`namesScript(table)`. A name is then a symbol, and a test can compare the set a page
|
|
56
|
+
wears with the set its stylesheet draws, in both directions.
|
|
57
|
+
|
|
58
|
+
`namesScript` is the boundary, and it throws rather than trusting a caller: a value must
|
|
59
|
+
be `KEBAB` — lowercase, hyphenated — because the table is spliced into a template
|
|
60
|
+
literal, and a backtick would end the string while `${` would interpolate at build time.
|
|
61
|
+
The global it binds must be a JavaScript `IDENTIFIER`. The table's *keys* are the page's
|
|
62
|
+
own English; its *values* are what CSS matches, and nothing else in the page may spell
|
|
63
|
+
one.
|
|
64
|
+
|
|
24
65
|
## Color is a role
|
|
25
66
|
|
|
26
67
|
`WEB_THEME_TOKENS` is derived from `themes/bytefly-dark.json` and
|
|
@@ -99,8 +140,17 @@ choice. Neither page reloads for a theme.
|
|
|
99
140
|
|
|
100
141
|
## Markup atoms and escaping
|
|
101
142
|
|
|
102
|
-
`docHead`, `brandLockup`, `themeToggle`, `chip`, `button`, and
|
|
103
|
-
strings.
|
|
143
|
+
`docHead`, `brandLockup`, `markIcon`, `tabTitle`, `themeToggle`, `chip`, `button`, and
|
|
144
|
+
`keysSheet` return strings. `docHead` links the mark as the tab icon — two SVG
|
|
145
|
+
data URIs from the same blocks the lockup draws, the dark one plain and the light
|
|
146
|
+
one behind `prefers-color-scheme` — because an icon is its own document and
|
|
147
|
+
cannot read the page's tokens.
|
|
148
|
+
|
|
149
|
+
`tabTitle` is where the three served pages' tab titles agree: `<page> · <subject> ·
|
|
150
|
+
<count>`, the page's name lowercase and the same word its `brandLockup` carries, a part
|
|
151
|
+
the page does not have left out rather than drawn empty. The board's count is live, so
|
|
152
|
+
its page repaints the title on every frame and the body crosses into the bundle as
|
|
153
|
+
`TAB_TITLE_JS`, the way `plural` does — one shape for the string, not two. Every dynamic **value** goes through `escapeHtml` from
|
|
104
154
|
[`../escape.ts`](../escape.ts); the fields documented as trusted (`css`, an
|
|
105
155
|
`icon` glyph, an attribute key, a client id) are code-authored constants, never
|
|
106
156
|
model or file text. A page that renders its own document follows the same rule.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser runtime both page clients splice into their bundles: the element builder,
|
|
3
|
+
* the "is the user typing?" test, the status line, and the JSON POST.
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces had written each of these for themselves, and because a bundle is a
|
|
6
|
+
* template literal no type checker and no AST scan ever compared them: the plan's
|
|
7
|
+
* `el(tag, className)` and the board's `el(tag, className, text)` were two builders, and
|
|
8
|
+
* the plan's `typing` compared `INPUT` where the board's lowercased the tag. Both are
|
|
9
|
+
* now one body, written here as real TypeScript, and each client carries the *source*
|
|
10
|
+
* (`CLIENT_RUNTIME_JS`) the same way it carries `plural` and the class table.
|
|
11
|
+
*
|
|
12
|
+
* The functions are plain JavaScript on purpose: their text is spliced into a bundle, so
|
|
13
|
+
* no annotation may survive into the string and no backtick or `${` may appear in a body.
|
|
14
|
+
* The DOM is reached through `globalThis`, because this project compiles without a DOM
|
|
15
|
+
* lib — the cast names the guarantee the page makes: it is a browser, and it has one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The pieces of a DOM node the runtime touches, and the ones a page client needs beyond
|
|
20
|
+
* it. The page supplies the real thing; this is the shape its bundle is typed against.
|
|
21
|
+
*/
|
|
22
|
+
export interface ClientNode {
|
|
23
|
+
className: string;
|
|
24
|
+
textContent: string | null;
|
|
25
|
+
title: string;
|
|
26
|
+
hidden: boolean;
|
|
27
|
+
href: string;
|
|
28
|
+
id: string;
|
|
29
|
+
style: { width: string };
|
|
30
|
+
type: string;
|
|
31
|
+
scrollTop: number;
|
|
32
|
+
contains(other: ClientNode | null): boolean;
|
|
33
|
+
focus(): void;
|
|
34
|
+
classList: { add(name: string): void; remove(name: string): void; toggle(name: string, on?: boolean): void };
|
|
35
|
+
setAttribute(name: string, value: string): void;
|
|
36
|
+
getAttribute(name: string): string | null;
|
|
37
|
+
removeAttribute(name: string): void;
|
|
38
|
+
appendChild(child: ClientNode): void;
|
|
39
|
+
remove(): void;
|
|
40
|
+
replaceChildren(): void;
|
|
41
|
+
querySelector(selector: string): ClientNode | null;
|
|
42
|
+
querySelectorAll(selector: string): ArrayLike<ClientNode>;
|
|
43
|
+
scrollIntoView?(options?: { block?: string }): void;
|
|
44
|
+
addEventListener(type: string, listener: (event: { target?: unknown }) => void): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What `document.createElement` has to answer for these helpers. */
|
|
48
|
+
interface RuntimeDocument {
|
|
49
|
+
createElement(tag: string): ClientNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One element with an optional class and text.
|
|
54
|
+
*
|
|
55
|
+
* `text` is set as `textContent`, never parsed: a card title and a plan's prose are both
|
|
56
|
+
* model- or user-written, and the one property that cannot execute is the one to use.
|
|
57
|
+
*/
|
|
58
|
+
export function el(tag: string, className?: string, text?: string | null): ClientNode {
|
|
59
|
+
// Reached through `globalThis` and cast here, because this project compiles without a
|
|
60
|
+
// DOM lib; the cast names what the page guarantees — it is a browser, and it has one.
|
|
61
|
+
const node = (globalThis as unknown as { document: RuntimeDocument }).document.createElement(tag);
|
|
62
|
+
if (className) node.className = className;
|
|
63
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a key event landed in a field, where every key is text rather than a shortcut.
|
|
69
|
+
*
|
|
70
|
+
* The tag is lowercased once, so a document parsed as XHTML (`tagName` upper case) and a
|
|
71
|
+
* page built by `createElement` (`tagName` lower case) both answer the same.
|
|
72
|
+
*/
|
|
73
|
+
export function typing(target: { tagName?: string; isContentEditable?: boolean }): boolean {
|
|
74
|
+
if (!target || !target.tagName) return target?.isContentEditable === true;
|
|
75
|
+
const tag = target.tagName.toLowerCase();
|
|
76
|
+
return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable === true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Paint a status line: its text, the same text as its tooltip, and its tone's class.
|
|
81
|
+
*
|
|
82
|
+
* The status slot is the page's own id, so the caller passes the node; what is shared is
|
|
83
|
+
* the rule that a tone is a `bf-status--*` class and that the text is also the title (a
|
|
84
|
+
* clamped line has no other way to be read).
|
|
85
|
+
*/
|
|
86
|
+
export function paintStatus(node: ClientNode, text: string, kind?: string): void {
|
|
87
|
+
node.textContent = text || "";
|
|
88
|
+
node.title = text || "";
|
|
89
|
+
node.className = kind ? "bf-status bf-status--" + kind : "bf-status";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One answer from the server: whether it accepted, its status, and its parsed body. */
|
|
93
|
+
interface PostResult {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
status: number;
|
|
96
|
+
body: unknown;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* POST JSON and read the reply's body, whatever the status.
|
|
101
|
+
*
|
|
102
|
+
* A page that only honoured the happy path would report `server returned 409` where the
|
|
103
|
+
* server had said *why*, so the body is parsed on both branches and the caller decides.
|
|
104
|
+
*/
|
|
105
|
+
export function postJson(url: string, payload: unknown): Promise<PostResult> {
|
|
106
|
+
return fetch(url, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify(payload),
|
|
110
|
+
}).then((response: { ok: boolean; status: number; json(): Promise<unknown> }) =>
|
|
111
|
+
response.json().then(
|
|
112
|
+
(body: unknown): PostResult => ({ ok: response.ok, status: response.status, body }),
|
|
113
|
+
(): PostResult => ({ ok: response.ok, status: response.status, body: null }),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The prefix this page was served under, as the server wrote it on `<html data-base>`.
|
|
120
|
+
*
|
|
121
|
+
* A session serves several pages on one origin, so a bundle cannot assume it owns the
|
|
122
|
+
* root: `""` at the origin's root, `/files` under a mount. The attribute is read every
|
|
123
|
+
* time rather than cached at load, because a fragment runs in whatever order the bundle
|
|
124
|
+
* lists it and none of them may depend on another having run first.
|
|
125
|
+
*/
|
|
126
|
+
export function pageBase(): string {
|
|
127
|
+
// Cast for the same reason as `el`: this project compiles without a DOM lib, and the
|
|
128
|
+
// page guarantees a document with a root element.
|
|
129
|
+
const root = (
|
|
130
|
+
globalThis as unknown as { document: { documentElement: { getAttribute(name: string): string | null } } }
|
|
131
|
+
).document.documentElement;
|
|
132
|
+
return root.getAttribute("data-base") ?? "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* {@link pageBase} on its own, for a bundle to splice at its **outermost** scope.
|
|
137
|
+
*
|
|
138
|
+
* A page's bundle is a stack of fragments, and a fragment's helper is only visible to the
|
|
139
|
+
* ones nested inside it — so this one goes where every fragment can reach it, which is not
|
|
140
|
+
* where the rest of the runtime goes.
|
|
141
|
+
*/
|
|
142
|
+
export const CLIENT_BASE_JS = String(pageBase);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The runtime, as a bundle receives it: the four bodies, in the order a client reads them.
|
|
146
|
+
*
|
|
147
|
+
* `test` holds each to its module body and proves the string is safe to splice.
|
|
148
|
+
*/
|
|
149
|
+
export const CLIENT_RUNTIME_JS = [el, typing, paintStatus, postJson].map(String).join("\n\n");
|
package/lib/web/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Bytefly web brand system: tokens, base rules, components, layout,
|
|
3
|
-
* runtime,
|
|
4
|
-
* leaf module so the surface reads as one kit.
|
|
3
|
+
* runtime, markup atoms, and the class-name table a page's bundle carries. Import
|
|
4
|
+
* from `lib/web/index.ts` rather than a leaf module so the surface reads as one kit.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from "./base.ts";
|
|
8
|
+
export * from "./client-runtime.ts";
|
|
8
9
|
export * from "./components.ts";
|
|
9
10
|
export * from "./layout.ts";
|
|
10
11
|
export * from "./markup.ts";
|
|
12
|
+
export * from "./names.ts";
|
|
11
13
|
export * from "./runtime.ts";
|
|
12
14
|
export * from "./tokens.ts";
|
package/lib/web/markup.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { escapeHtml } from "../escape.ts";
|
|
10
10
|
import { THEME_JS_PATH } from "./runtime.ts";
|
|
11
|
+
import { WEB_THEME_TOKENS } from "./tokens.ts";
|
|
11
12
|
|
|
12
13
|
/** Options for {@link docHead}. */
|
|
13
14
|
export interface DocHeadOptions {
|
|
@@ -15,6 +16,46 @@ export interface DocHeadOptions {
|
|
|
15
16
|
title: string;
|
|
16
17
|
/** Trusted CSS assembled by the page. */
|
|
17
18
|
css: string;
|
|
19
|
+
/**
|
|
20
|
+
* The mount prefix this page answers under, e.g. `/files`; `""` for the origin's
|
|
21
|
+
* root. A page's scripts and links are emitted under it, because a session serves
|
|
22
|
+
* several pages on one origin.
|
|
23
|
+
*/
|
|
24
|
+
base?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A page's browser-tab title: `<page> · <subject> · <count>`.
|
|
29
|
+
*
|
|
30
|
+
* One convention across the served pages, so a strip of tabs reads as one product. The
|
|
31
|
+
* page's name comes first, lowercase and the same word its header's `brandLockup`
|
|
32
|
+
* carries; then what the page is showing; then the count a reader opens the tab for. A
|
|
33
|
+
* part the page does not have is left out rather than drawn empty, so a board with
|
|
34
|
+
* nothing open reads `board · my-pi-agent` and a plan reads `plan review · <its title>`.
|
|
35
|
+
*/
|
|
36
|
+
export function tabTitle(product: string, subject?: string, count?: string): string {
|
|
37
|
+
const kept = [product, subject, count];
|
|
38
|
+
return kept.filter((part) => part !== undefined && part !== "").join(" · ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The same body, as the browser receives it.
|
|
43
|
+
*
|
|
44
|
+
* The board's count is live, so its tab is repainted on every frame and this rule has to
|
|
45
|
+
* cross into the bundle. It is written as concatenation with no backtick and no
|
|
46
|
+
* interpolation, because the bundle is a template the source is spliced into.
|
|
47
|
+
*/
|
|
48
|
+
export const TAB_TITLE_JS = String(tabTitle);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `data-base` attribute a page's `<html>` carries, which its own bundle reads
|
|
52
|
+
* (`base()` in `client-runtime.ts`) to build every URL it emits.
|
|
53
|
+
*
|
|
54
|
+
* `""` — the origin's root — writes nothing at all, so a page served alone is markup
|
|
55
|
+
* without a mount to name.
|
|
56
|
+
*/
|
|
57
|
+
export function baseAttribute(base: string): string {
|
|
58
|
+
return base === "" ? "" : ` data-base="${escapeHtml(base)}"`;
|
|
18
59
|
}
|
|
19
60
|
|
|
20
61
|
/**
|
|
@@ -27,8 +68,70 @@ export function docHead(options: DocHeadOptions): string {
|
|
|
27
68
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
28
69
|
<meta name="color-scheme" content="dark light">
|
|
29
70
|
<title>${escapeHtml(options.title)}</title>
|
|
71
|
+
${markIcon()}
|
|
30
72
|
<style>${options.css}</style>
|
|
31
|
-
<script src="${THEME_JS_PATH}"></script>`;
|
|
73
|
+
<script src="${options.base ?? ""}${THEME_JS_PATH}"></script>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The mark's blocks: `█▀█` over `█▀ █`, as `[x, height]` per row in a 4x2 grid.
|
|
78
|
+
*
|
|
79
|
+
* One shape, three places — the header lockup, the tab icon, and the TUI header's
|
|
80
|
+
* half-blocks — so it is written once. The rows differ: the top fills x=2, the bottom
|
|
81
|
+
* leaves it empty and fills x=3, which is what makes the mark read as two glyphs.
|
|
82
|
+
*/
|
|
83
|
+
const MARK_ROWS: ReadonlyArray<{ group: "top" | "bottom"; rects: ReadonlyArray<readonly [number, number]> }> = [
|
|
84
|
+
{
|
|
85
|
+
group: "top",
|
|
86
|
+
rects: [
|
|
87
|
+
[0, 1],
|
|
88
|
+
[1, 0.5],
|
|
89
|
+
[2, 1],
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
group: "bottom",
|
|
94
|
+
rects: [
|
|
95
|
+
[0, 1],
|
|
96
|
+
[1, 0.5],
|
|
97
|
+
[3, 1],
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** One row of the mark, as `<rect>`s starting at `y`. */
|
|
103
|
+
function markRowRects(row: (typeof MARK_ROWS)[number], y: number): string {
|
|
104
|
+
return row.rects.map(([x, height]) => `<rect x="${x}" y="${y}" width="1" height="${height}"/>`).join("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The mark as a standalone SVG, in one appearance's colours.
|
|
109
|
+
*
|
|
110
|
+
* A favicon is its own document: it cannot read the page's tokens, so the fills are
|
|
111
|
+
* resolved here from the same palette the page paints with, and each appearance gets
|
|
112
|
+
* its own icon.
|
|
113
|
+
*/
|
|
114
|
+
function markSvg(fills: { top: string; bottom: string }): string {
|
|
115
|
+
const rows = MARK_ROWS.map((row, index) => `<g fill="${fills[row.group]}">${markRowRects(row, index)}</g>`);
|
|
116
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 2">${rows.join("")}</svg>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The tab icon: the mark, in the appearance the reader's system is in.
|
|
121
|
+
*
|
|
122
|
+
* The dark icon is the plain `rel="icon"` — the page paints dark at `:root`, and a
|
|
123
|
+
* browser that reads no `media` query on an icon keeps it. The light one is the
|
|
124
|
+
* override, so a light tab does not carry a mark tuned for a dark one.
|
|
125
|
+
*/
|
|
126
|
+
export function markIcon(): string {
|
|
127
|
+
const dark = encodeURIComponent(
|
|
128
|
+
markSvg({ top: WEB_THEME_TOKENS.dark["--accent"], bottom: WEB_THEME_TOKENS.dark["--warn"] }),
|
|
129
|
+
);
|
|
130
|
+
const light = encodeURIComponent(
|
|
131
|
+
markSvg({ top: WEB_THEME_TOKENS.light["--accent"], bottom: WEB_THEME_TOKENS.light["--warn"] }),
|
|
132
|
+
);
|
|
133
|
+
return `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${dark}">
|
|
134
|
+
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: light)" href="data:image/svg+xml,${light}">`;
|
|
32
135
|
}
|
|
33
136
|
|
|
34
137
|
/**
|
|
@@ -37,7 +140,8 @@ export function docHead(options: DocHeadOptions): string {
|
|
|
37
140
|
* as a 4x2 grid so the accent and warning rows stay crisp at any zoom.
|
|
38
141
|
*/
|
|
39
142
|
export function brandLockup(product: string): string {
|
|
40
|
-
|
|
143
|
+
const rows = MARK_ROWS.map((row, index) => `<g class="bf-mark__${row.group}">${markRowRects(row, index)}</g>`);
|
|
144
|
+
return `<span class="bf-brand"><svg class="bf-mark" viewBox="0 0 4 2" aria-hidden="true">${rows.join("")}</svg><span class="bf-wordmark">bytefly</span><span class="bf-context">${escapeHtml(product)}</span></span>`;
|
|
41
145
|
}
|
|
42
146
|
|
|
43
147
|
/**
|
package/lib/web/names.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page's class names, as a table a bundle can carry.
|
|
3
|
+
*
|
|
4
|
+
* A browser page in this package is a TypeScript document plus a client script held in
|
|
5
|
+
* a template literal, so the two halves cannot import from each other: a class name is a
|
|
6
|
+
* string in one and a string in the other, and a name built by concatenation
|
|
7
|
+
* (`"board-card__reason" + kind`) cannot be found by reading either. A table fixes both:
|
|
8
|
+
* the document and the bundle read the same values, and a test can compare the set the
|
|
9
|
+
* page wears with the set its stylesheet draws.
|
|
10
|
+
*
|
|
11
|
+
* The bundle carries the table as **source**, so every value must survive being spliced
|
|
12
|
+
* into a template literal: a backtick would end the string, and `${` would interpolate
|
|
13
|
+
* at build time. `KEBAB` is that contract, and {@link namesScript} throws rather than
|
|
14
|
+
* trusting a caller — a bad name is a build error here, not a mystery on the page.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A page's vocabulary: a key per thing it dresses, and the class it wears. */
|
|
18
|
+
export type ClassNames = Readonly<Record<string, string>>;
|
|
19
|
+
|
|
20
|
+
/** What a class name may be: lowercase, hyphenated, and nothing a script could misread. */
|
|
21
|
+
export const KEBAB = /^[a-z][a-z0-9_-]*$/;
|
|
22
|
+
|
|
23
|
+
/** What the global the table is bound to may be: a JavaScript identifier, and nothing else. */
|
|
24
|
+
export const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
25
|
+
|
|
26
|
+
/** The declared global the table is bound to in a page's bundle. */
|
|
27
|
+
export const NAMES_GLOBAL = "N";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The table as one script statement, for a client bundle to start with.
|
|
31
|
+
*
|
|
32
|
+
* `JSON.stringify` is the serializer because the values are already strings that pass
|
|
33
|
+
* {@link KEBAB}; the check before it is what makes that true, and its message names the
|
|
34
|
+
* offending key and value so the fix is obvious.
|
|
35
|
+
*/
|
|
36
|
+
export function namesScript(table: ClassNames, global: string = NAMES_GLOBAL): string {
|
|
37
|
+
for (const [key, value] of Object.entries(table)) {
|
|
38
|
+
if (!KEBAB.test(value)) throw new Error(`the class name for "${key}" is not a kebab-case name: "${value}"`);
|
|
39
|
+
}
|
|
40
|
+
if (!IDENTIFIER.test(global)) throw new Error(`"${global}" is not a name a script can declare`);
|
|
41
|
+
return `var ${global} = ${JSON.stringify(table)};`;
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, file-read, check, memory, context, session-bus, board, team, wechat, and bytefly.",
|
|
3
|
+
"version": "0.21.0",
|
|
4
|
+
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, serve, subagent, job, watch, web-access, file-browse, file-open, file-read, check, memory, context, session-bus, board, team, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
},
|
|
93
93
|
"pi": {
|
|
94
94
|
"extensions": [
|
|
95
|
+
"./extensions/serve/index.ts",
|
|
95
96
|
"./extensions/worktree/index.ts",
|
|
96
97
|
"./extensions/rewind/index.ts",
|
|
97
98
|
"./extensions/question/index.ts",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The board's one setting: the port its page is pinned to.
|
|
3
|
-
*
|
|
4
|
-
* The `board` section of the shared `bytefly.json` (global then project, project
|
|
5
|
-
* wins). `0` — the default — takes one free port for the session and remembers it,
|
|
6
|
-
* which is what a session that never bookmarks its URL wants; a positive value pins
|
|
7
|
-
* the page, so a bookmarked URL survives a restart. A pinned port that is taken
|
|
8
|
-
* fails the start rather than moving the page out from under the bookmark.
|
|
9
|
-
*
|
|
10
|
-
* Pin it in the project document when more than one repository runs a board: a
|
|
11
|
-
* global value is one port for every board on the machine.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { clampInteger, loadConfigSection } from "../../lib/config.ts";
|
|
15
|
-
|
|
16
|
-
/** The `board` section. */
|
|
17
|
-
export interface BoardConfig {
|
|
18
|
-
/** TCP port on `127.0.0.1`; `0` picks one for the session, a positive value pins it. */
|
|
19
|
-
port: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const DEFAULT_BOARD_CONFIG: BoardConfig = { port: 0 };
|
|
23
|
-
|
|
24
|
-
/** Validate and clamp a raw config object over the defaults. */
|
|
25
|
-
export function normalizeBoardConfig(
|
|
26
|
-
raw: Record<string, unknown> | undefined,
|
|
27
|
-
base: BoardConfig = DEFAULT_BOARD_CONFIG,
|
|
28
|
-
): BoardConfig {
|
|
29
|
-
if (!raw) return base;
|
|
30
|
-
return { port: clampInteger(raw.port, base.port, 0, 65_535) };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Load the effective config for a working directory. */
|
|
34
|
-
export function loadBoardConfig(cwd: string): BoardConfig {
|
|
35
|
-
return loadConfigSection(cwd, "board", DEFAULT_BOARD_CONFIG, normalizeBoardConfig);
|
|
36
|
-
}
|