@gavin-hu/my-pi-agent 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/README.zh-CN.md +13 -8
- package/docs/extensions/README.md +1 -1
- package/docs/extensions/board.md +286 -123
- package/docs/extensions/bytefly.md +381 -223
- package/docs/extensions/file-browse.md +14 -7
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +56 -11
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +45 -17
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +23 -5
- package/extensions/board/format.ts +107 -9
- package/extensions/board/join.ts +1 -1
- package/extensions/board/page.ts +170 -94
- package/extensions/board/result.ts +9 -0
- package/extensions/board/schema.ts +22 -7
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +1 -0
- package/extensions/board/tools.ts +186 -21
- package/extensions/board/transport/router.ts +27 -108
- package/extensions/board/transport/server.ts +18 -29
- package/extensions/board/types.ts +67 -0
- package/extensions/board/view/client.ts +23 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +5 -171
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +48 -88
- package/extensions/board/view/render.ts +213 -190
- package/extensions/board/view/styles.ts +65 -48
- 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 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- 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/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/transport/router.ts +0 -2
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +6 -7
- package/extensions/file-browse/view/markdown.ts +11 -13
- package/extensions/file-browse/view/raw-html.ts +18 -13
- package/extensions/job/runtime.ts +4 -4
- package/extensions/plan/feedback.ts +40 -16
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +17 -1
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +31 -2
- 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/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -1
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/rewind/commands/review.ts +2 -1
- 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/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +41 -8
- 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/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 +6 -5
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +86 -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/store.ts +239 -52
- package/lib/board/view.ts +94 -10
- package/lib/format.ts +22 -0
- package/lib/http/serve.ts +3 -0
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +14 -1
- package/lib/session-roster.ts +74 -3
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/glyphs.ts +2 -3
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/package.json +1 -1
- 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/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,123 @@
|
|
|
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 runtime, as a bundle receives it: four bodies, in the order a client reads them.
|
|
120
|
+
*
|
|
121
|
+
* `test` holds each to its module body and proves the string is safe to splice.
|
|
122
|
+
*/
|
|
123
|
+
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 {
|
|
@@ -17,6 +18,29 @@ export interface DocHeadOptions {
|
|
|
17
18
|
css: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A page's browser-tab title: `<page> · <subject> · <count>`.
|
|
23
|
+
*
|
|
24
|
+
* One convention across the served pages, so a strip of tabs reads as one product. The
|
|
25
|
+
* page's name comes first, lowercase and the same word its header's `brandLockup`
|
|
26
|
+
* carries; then what the page is showing; then the count a reader opens the tab for. A
|
|
27
|
+
* part the page does not have is left out rather than drawn empty, so a board with
|
|
28
|
+
* nothing open reads `board · my-pi-agent` and a plan reads `plan review · <its title>`.
|
|
29
|
+
*/
|
|
30
|
+
export function tabTitle(product: string, subject?: string, count?: string): string {
|
|
31
|
+
const kept = [product, subject, count];
|
|
32
|
+
return kept.filter((part) => part !== undefined && part !== "").join(" · ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The same body, as the browser receives it.
|
|
37
|
+
*
|
|
38
|
+
* The board's count is live, so its tab is repainted on every frame and this rule has to
|
|
39
|
+
* cross into the bundle. It is written as concatenation with no backtick and no
|
|
40
|
+
* interpolation, because the bundle is a template the source is spliced into.
|
|
41
|
+
*/
|
|
42
|
+
export const TAB_TITLE_JS = String(tabTitle);
|
|
43
|
+
|
|
20
44
|
/**
|
|
21
45
|
* The shared document head: charset, viewport, colour scheme, title, the page
|
|
22
46
|
* CSS, and the pre-paint theme bootstrap. The body and its scripts stay with
|
|
@@ -27,17 +51,80 @@ export function docHead(options: DocHeadOptions): string {
|
|
|
27
51
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
28
52
|
<meta name="color-scheme" content="dark light">
|
|
29
53
|
<title>${escapeHtml(options.title)}</title>
|
|
54
|
+
${markIcon()}
|
|
30
55
|
<style>${options.css}</style>
|
|
31
56
|
<script src="${THEME_JS_PATH}"></script>`;
|
|
32
57
|
}
|
|
33
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The mark's blocks: `█▀█` over `█▀ █`, as `[x, height]` per row in a 4x2 grid.
|
|
61
|
+
*
|
|
62
|
+
* One shape, three places — the header lockup, the tab icon, and the TUI header's
|
|
63
|
+
* half-blocks — so it is written once. The rows differ: the top fills x=2, the bottom
|
|
64
|
+
* leaves it empty and fills x=3, which is what makes the mark read as two glyphs.
|
|
65
|
+
*/
|
|
66
|
+
const MARK_ROWS: ReadonlyArray<{ group: "top" | "bottom"; rects: ReadonlyArray<readonly [number, number]> }> = [
|
|
67
|
+
{
|
|
68
|
+
group: "top",
|
|
69
|
+
rects: [
|
|
70
|
+
[0, 1],
|
|
71
|
+
[1, 0.5],
|
|
72
|
+
[2, 1],
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
group: "bottom",
|
|
77
|
+
rects: [
|
|
78
|
+
[0, 1],
|
|
79
|
+
[1, 0.5],
|
|
80
|
+
[3, 1],
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/** One row of the mark, as `<rect>`s starting at `y`. */
|
|
86
|
+
function markRowRects(row: (typeof MARK_ROWS)[number], y: number): string {
|
|
87
|
+
return row.rects.map(([x, height]) => `<rect x="${x}" y="${y}" width="1" height="${height}"/>`).join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The mark as a standalone SVG, in one appearance's colours.
|
|
92
|
+
*
|
|
93
|
+
* A favicon is its own document: it cannot read the page's tokens, so the fills are
|
|
94
|
+
* resolved here from the same palette the page paints with, and each appearance gets
|
|
95
|
+
* its own icon.
|
|
96
|
+
*/
|
|
97
|
+
function markSvg(fills: { top: string; bottom: string }): string {
|
|
98
|
+
const rows = MARK_ROWS.map((row, index) => `<g fill="${fills[row.group]}">${markRowRects(row, index)}</g>`);
|
|
99
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 2">${rows.join("")}</svg>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The tab icon: the mark, in the appearance the reader's system is in.
|
|
104
|
+
*
|
|
105
|
+
* The dark icon is the plain `rel="icon"` — the page paints dark at `:root`, and a
|
|
106
|
+
* browser that reads no `media` query on an icon keeps it. The light one is the
|
|
107
|
+
* override, so a light tab does not carry a mark tuned for a dark one.
|
|
108
|
+
*/
|
|
109
|
+
export function markIcon(): string {
|
|
110
|
+
const dark = encodeURIComponent(
|
|
111
|
+
markSvg({ top: WEB_THEME_TOKENS.dark["--accent"], bottom: WEB_THEME_TOKENS.dark["--warn"] }),
|
|
112
|
+
);
|
|
113
|
+
const light = encodeURIComponent(
|
|
114
|
+
markSvg({ top: WEB_THEME_TOKENS.light["--accent"], bottom: WEB_THEME_TOKENS.light["--warn"] }),
|
|
115
|
+
);
|
|
116
|
+
return `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${dark}">
|
|
117
|
+
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: light)" href="data:image/svg+xml,${light}">`;
|
|
118
|
+
}
|
|
119
|
+
|
|
34
120
|
/**
|
|
35
121
|
* The Bytefly nameplate: the two-tone half-block mark, the `bytefly` wordmark,
|
|
36
122
|
* and the product name. The mark mirrors the TUI header (`█▀█` / `█▀ █`), drawn
|
|
37
123
|
* as a 4x2 grid so the accent and warning rows stay crisp at any zoom.
|
|
38
124
|
*/
|
|
39
125
|
export function brandLockup(product: string): string {
|
|
40
|
-
|
|
126
|
+
const rows = MARK_ROWS.map((row, index) => `<g class="bf-mark__${row.group}">${markRowRects(row, index)}</g>`);
|
|
127
|
+
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
128
|
}
|
|
42
129
|
|
|
43
130
|
/**
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|