@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- 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 +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- 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/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- 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 +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
package/lib/tui/glyphs.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { keyText, type Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { paint } from "./tokens.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Leading glyphs for status chips, footer segments, and transcript rails.
|
|
@@ -29,8 +30,7 @@ export const GLYPHS = {
|
|
|
29
30
|
jobsRunning: "▸",
|
|
30
31
|
jobsFailure: "✗",
|
|
31
32
|
stopped: "⊘",
|
|
32
|
-
|
|
33
|
-
board: "⊞",
|
|
33
|
+
http: "◉",
|
|
34
34
|
delegation: "↳",
|
|
35
35
|
wechat: "✆",
|
|
36
36
|
gaugeFull: "▰",
|
|
@@ -51,6 +51,7 @@ export const GLYPHS = {
|
|
|
51
51
|
prev: "‹",
|
|
52
52
|
next: "›",
|
|
53
53
|
update: "↑",
|
|
54
|
+
autoMode: "▶",
|
|
54
55
|
prompt: "❯",
|
|
55
56
|
} as const;
|
|
56
57
|
|
|
@@ -70,8 +71,7 @@ export const STATUS_KEYS = {
|
|
|
70
71
|
jobsFailure: "jobs-failure",
|
|
71
72
|
watch: "watch",
|
|
72
73
|
watchMatches: "watch-matches",
|
|
73
|
-
|
|
74
|
-
board: "board",
|
|
74
|
+
http: "http",
|
|
75
75
|
rewind: "rewind",
|
|
76
76
|
agents: "agents",
|
|
77
77
|
wechat: "wechat",
|
|
@@ -84,25 +84,25 @@ export const STATUS_KEYS = {
|
|
|
84
84
|
*/
|
|
85
85
|
export const EXPAND_KEYBINDING = "app.tools.expand";
|
|
86
86
|
|
|
87
|
-
/** The bound expand key,
|
|
87
|
+
/** The bound expand key, in the faint role, or `""` when the binding is unknown. */
|
|
88
88
|
export function expandKey(theme: Theme): string {
|
|
89
89
|
const key = keyText(EXPAND_KEYBINDING);
|
|
90
|
-
return key ? theme
|
|
90
|
+
return key ? paint(theme, "faint", key) : "";
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Pi's key-hint format: the key
|
|
94
|
+
* Pi's key-hint format: the key faint, its description meta. An unbound key
|
|
95
95
|
* (`""`) yields `""`, so a hint never prints a stray description.
|
|
96
96
|
*/
|
|
97
97
|
export function keyHintText(theme: Theme, key: string, description: string): string {
|
|
98
|
-
return key ? theme
|
|
98
|
+
return key ? paint(theme, "faint", key) + paint(theme, "meta", ` ${description}`) : "";
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* The built-in-style expand affordance: a
|
|
103
|
-
* key in
|
|
102
|
+
* The built-in-style expand affordance: a meta `(ctrl+o to expand)` with the
|
|
103
|
+
* key in faint. Degrades to `(to expand)` when the binding is unknown.
|
|
104
104
|
*/
|
|
105
105
|
export function expandHint(theme: Theme): string {
|
|
106
106
|
const hint = keyHintText(theme, keyText(EXPAND_KEYBINDING), "to expand)");
|
|
107
|
-
return theme
|
|
107
|
+
return paint(theme, "meta", "(") + (hint || paint(theme, "meta", "to expand)"));
|
|
108
108
|
}
|
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/tokens.ts
CHANGED
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
* A component names a role (`title`, `meta`, `bad`) and the active theme
|
|
6
6
|
* supplies the color, so switching themes re-skins every surface without a
|
|
7
7
|
* component changing. The mapping is deliberately indirect: Pi's token set is
|
|
8
|
-
* closed, so the kit only ever references it, never a literal color
|
|
8
|
+
* closed, so the kit only ever references it, never a literal color — and this
|
|
9
|
+
* table is the one place it does, tool vocabulary included, so a remap here
|
|
10
|
+
* reaches every surface the kit and the renderers draw.
|
|
9
11
|
*
|
|
10
12
|
* Value-only: one frozen lookup table and pure helpers.
|
|
13
|
+
*
|
|
14
|
+
* Two vocabularies live here and they do not mix: `ok`/`warn`/`bad` are verdicts —
|
|
15
|
+
* they say how something *is* — and `hue1`…`hue5` are categories, which say only
|
|
16
|
+
* "this is not those". A surface that shows hues keeps its verdicts to the places
|
|
17
|
+
* that earn one, because a reader cannot tell a borrowed green from a judgement.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
@@ -35,7 +42,24 @@ export type TuiRole =
|
|
|
35
42
|
/** Error. */
|
|
36
43
|
| "bad"
|
|
37
44
|
/** Neutral notice. */
|
|
38
|
-
| "info"
|
|
45
|
+
| "info"
|
|
46
|
+
/**
|
|
47
|
+
* A category hue: identity, never a verdict.
|
|
48
|
+
*
|
|
49
|
+
* Blue, purple, pink, green, and amber are the shipped palette's five colourful
|
|
50
|
+
* tokens, so a categorical surface reads as a chart. Green and amber are the same
|
|
51
|
+
* tokens `ok` and `warn` resolve to — that is the cost of Pi's closed token set —
|
|
52
|
+
* which is why a hue never appears in a slot that also carries a judgement.
|
|
53
|
+
*/
|
|
54
|
+
| "hue1"
|
|
55
|
+
| "hue2"
|
|
56
|
+
| "hue3"
|
|
57
|
+
| "hue4"
|
|
58
|
+
| "hue5"
|
|
59
|
+
/** A tool call's own name: the transcript block's only accent. */
|
|
60
|
+
| "toolTitle"
|
|
61
|
+
/** A tool result's body text. */
|
|
62
|
+
| "toolOutput";
|
|
39
63
|
|
|
40
64
|
/** Role -> theme color. The single place the kit names a Pi token. */
|
|
41
65
|
export const TUI_TOKENS: Readonly<Record<TuiRole, ThemeColor>> = {
|
|
@@ -50,6 +74,13 @@ export const TUI_TOKENS: Readonly<Record<TuiRole, ThemeColor>> = {
|
|
|
50
74
|
warn: "warning",
|
|
51
75
|
bad: "error",
|
|
52
76
|
info: "muted",
|
|
77
|
+
hue1: "accent",
|
|
78
|
+
hue2: "mdListBullet",
|
|
79
|
+
hue3: "mdCode",
|
|
80
|
+
hue4: "success",
|
|
81
|
+
hue5: "warning",
|
|
82
|
+
toolTitle: "toolTitle",
|
|
83
|
+
toolOutput: "toolOutput",
|
|
53
84
|
};
|
|
54
85
|
|
|
55
86
|
/** Paint `text` in a role's color. */
|
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. */
|
|
@@ -30,9 +31,9 @@ export interface ToolMessageResult {
|
|
|
30
31
|
isError?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** The bold tool name in
|
|
34
|
+
/** The bold tool name in the toolTitle role, the block's only accent. */
|
|
34
35
|
export function toolCallTitle(theme: Theme, name: string): string {
|
|
35
|
-
return theme
|
|
36
|
+
return paint(theme, "toolTitle", theme.bold(name));
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -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") {
|
|
@@ -259,11 +275,24 @@ export class TailResult<Row> implements Component {
|
|
|
259
275
|
return lines;
|
|
260
276
|
}
|
|
261
277
|
if (input.earlier > 0) lines.push(clip(earlierHint(this.theme, input.earlier, input.earlierLabel), w));
|
|
262
|
-
for (const line of input.lines) lines.push(this.theme
|
|
278
|
+
for (const line of input.lines) lines.push(paint(this.theme, "toolOutput", clip(line, w)));
|
|
263
279
|
return lines;
|
|
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
|
@@ -42,6 +42,27 @@ What stays page-side: what a status *says*. `paintStatus` owns the text, its too
|
|
|
42
42
|
the `bf-status--<tone>` class; the board composes `live · 3m` and appends its own class.
|
|
43
43
|
A shared runtime that also decided the wording would be a second copy of the page's voice.
|
|
44
44
|
|
|
45
|
+
## One inline Markdown renderer, spliced in
|
|
46
|
+
|
|
47
|
+
Two served pages read Markdown written by a model or a person: the plan review's document
|
|
48
|
+
and the board's card notes. Both need the same two answers — what an inline token is, and
|
|
49
|
+
which target a link may point at — so `markdown.ts` holds one body of each and exports
|
|
50
|
+
`INLINE_MARKDOWN_JS` for a bundle to splice. The review page's block half
|
|
51
|
+
([`../../extensions/plan/web-markdown.ts`](../../extensions/plan/web-markdown.ts)) calls
|
|
52
|
+
`renderInline` for the text inside a block; the board calls it for each note. A page that
|
|
53
|
+
writes its own is how one of them starts accepting a `javascript:` target the other refuses.
|
|
54
|
+
|
|
55
|
+
The renderer appends a text node or one of four atoms — `code`, `strong`, `em`, `a` — and
|
|
56
|
+
never parses markup, so the board's rule that a stored field is never `innerHTML` holds with
|
|
57
|
+
Markdown in play. A link is made only when `safeHref` allows the target: `http`, `https`, or
|
|
58
|
+
`mailto`, no credentials, and no protocol-relative or disguised relative target, resolved
|
|
59
|
+
against the page's own URL at call time.
|
|
60
|
+
|
|
61
|
+
The body is JavaScript on purpose, like the runtime's: no annotation survives into the
|
|
62
|
+
string, and no backtick or `${` may appear in one. It is the inline half only — a card note
|
|
63
|
+
is one sanitized line, so inline is the whole of it, while blocks, tables, and fenced code
|
|
64
|
+
stay the review page's own half.
|
|
65
|
+
|
|
45
66
|
## Name a class once, in a table
|
|
46
67
|
|
|
47
68
|
A page's document is TypeScript and its client is a string inside a template literal, so
|
|
@@ -116,7 +116,33 @@ export function postJson(url: string, payload: unknown): Promise<PostResult> {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* The
|
|
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.
|
|
120
146
|
*
|
|
121
147
|
* `test` holds each to its module body and proves the string is safe to splice.
|
|
122
148
|
*/
|
package/lib/web/components.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
28
28
|
.bf-btn:disabled{opacity:.5;cursor:default}
|
|
29
29
|
.bf-chip{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);color:var(--muted);white-space:nowrap}
|
|
30
30
|
.bf-chip--accent{color:var(--accent);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
31
|
+
/* A field the card carries rather than a state it is in: the same pill, worn quietly. */
|
|
32
|
+
.bf-chip--outline{color:var(--muted);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
31
33
|
.bf-chip--success{color:var(--success)}
|
|
32
34
|
.bf-chip--warn{color:var(--warn)}
|
|
33
35
|
.bf-chip--danger{color:var(--danger)}
|
|
@@ -35,7 +37,7 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
35
37
|
.bf-segmented__item{appearance:none;border:0;border-radius:0;background:transparent;color:var(--text);font:var(--fs-xs) var(--font-ui);padding:5px var(--space-3);cursor:pointer}
|
|
36
38
|
.bf-segmented__item+.bf-segmented__item{border-left:1px solid var(--border)}
|
|
37
39
|
.bf-segmented__item:hover{color:var(--accent);text-decoration:none}
|
|
38
|
-
.bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
|
|
40
|
+
.bf-segmented__item[aria-current="page"],.bf-segmented__item[aria-selected="true"],.bf-segmented__item[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent)}
|
|
39
41
|
.bf-input{border:0;background:transparent;color:var(--text);font:var(--fs-sm) var(--font-mono);width:100%;outline:none}
|
|
40
42
|
.bf-field{display:flex;align-items:center;gap:var(--space-1);background:var(--canvas);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-1) var(--space-2);color:var(--muted);font-size:var(--fs-sm)}
|
|
41
43
|
/* The field is the control: ringing the input inside it drew a border within a border. */
|
|
@@ -73,6 +75,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
|
|
|
73
75
|
.bf-keys{display:grid;grid-template-columns:auto 1fr;gap:8px var(--space-4);margin:0;padding:var(--space-4)}
|
|
74
76
|
.bf-keys dt{white-space:nowrap}
|
|
75
77
|
.bf-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
|
|
78
|
+
/* One line under the table, for a rule that is not a key: what an open dialog keeps. */
|
|
79
|
+
.bf-keys__note{margin:0;padding:0 var(--space-4) var(--space-4);color:var(--muted);font-size:var(--fs-xs)}
|
|
76
80
|
.bf-kbd{display:inline-block;min-width:20px;padding:1px 6px;border:1px solid var(--border);border-bottom-width:2px;border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:var(--fs-xs) var(--font-mono);text-align:center}
|
|
77
81
|
.bf-tree{font-family:var(--font-mono);font-size:var(--fs-sm);overflow-x:auto}
|
|
78
82
|
.bf-tree ul{list-style:none;margin:0;padding-left:14px}
|
package/lib/web/index.ts
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline Markdown, for every served page: one renderer and one link policy.
|
|
3
|
+
*
|
|
4
|
+
* Two pages read Markdown written by a model or a person — the plan review's document and
|
|
5
|
+
* the board's card notes — and each needs the same two answers: what an inline token is,
|
|
6
|
+
* and which target a link may point at. A second copy of either is how one page starts
|
|
7
|
+
* accepting a `javascript:` target the other refuses, so the body lives here and each page
|
|
8
|
+
* splices it into its own bundle.
|
|
9
|
+
*
|
|
10
|
+
* The renderer appends **text nodes and four elements only** — `code`, `strong`, `em`, and
|
|
11
|
+
* `a` — so plan, comment, and note text never becomes markup and no page needs `innerHTML`.
|
|
12
|
+
* A link is made only for a target `safeHref` allows: `http`, `https`, or `mailto`, no
|
|
13
|
+
* credentials, and no protocol-relative or disguised relative target.
|
|
14
|
+
*
|
|
15
|
+
* It is the inline half only. A card note is one sanitized line, so inline is the whole of
|
|
16
|
+
* it; the review page's blocks, tables, fenced code, and lists are its own half, and they
|
|
17
|
+
* call `renderInline` for the text inside each.
|
|
18
|
+
*
|
|
19
|
+
* The body is JavaScript: no annotation survives into a string, and no backtick or `${`
|
|
20
|
+
* may appear in one. `base` is the page's own URL at call time, which is what makes a
|
|
21
|
+
* target that only looks relative land as text.
|
|
22
|
+
*/
|
|
23
|
+
export const INLINE_MARKDOWN_JS = String.raw` // A link is rendered only for an http(s) or mailto target. Resolving against
|
|
24
|
+
// the review origin is what catches a target that only looks relative: the
|
|
25
|
+
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
26
|
+
// it is parsed, and a disguised target must stay text.
|
|
27
|
+
function safeHref(url, base) {
|
|
28
|
+
var trimmed = String(url).trim();
|
|
29
|
+
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
30
|
+
var resolved;
|
|
31
|
+
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
32
|
+
if (resolved.protocol === "mailto:") return trimmed;
|
|
33
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
34
|
+
if (resolved.username || resolved.password) return null;
|
|
35
|
+
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
36
|
+
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Inline token order matters: code, emphasis, and links are matched before a
|
|
40
|
+
// bare URL, so a URL inside a link target or a code span is never re-split.
|
|
41
|
+
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
|
|
42
|
+
|
|
43
|
+
function renderInline(container, text) {
|
|
44
|
+
String(text).split(INLINE).forEach(function (part) {
|
|
45
|
+
if (!part) return;
|
|
46
|
+
var match;
|
|
47
|
+
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
48
|
+
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
49
|
+
}
|
|
50
|
+
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
51
|
+
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
52
|
+
}
|
|
53
|
+
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
54
|
+
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
55
|
+
}
|
|
56
|
+
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
57
|
+
var href = safeHref(match[2], location.href);
|
|
58
|
+
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
59
|
+
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
60
|
+
container.appendChild(link); return;
|
|
61
|
+
}
|
|
62
|
+
// GFM extended autolink: a bare http(s) or www. URL in running text.
|
|
63
|
+
if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
|
|
64
|
+
var url = match[0].replace(/[.,;:!?*_~]+$/, "");
|
|
65
|
+
while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
|
|
66
|
+
url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
67
|
+
}
|
|
68
|
+
var dot = url.indexOf(".");
|
|
69
|
+
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
70
|
+
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
71
|
+
if (safe) {
|
|
72
|
+
var auto = el("a");
|
|
73
|
+
auto.textContent = url;
|
|
74
|
+
auto.href = safe;
|
|
75
|
+
auto.target = "_blank";
|
|
76
|
+
auto.rel = "noreferrer noopener";
|
|
77
|
+
container.appendChild(auto);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
container.appendChild(document.createTextNode(part));
|
|
82
|
+
});
|
|
83
|
+
}`;
|
package/lib/web/markup.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface DocHeadOptions {
|
|
|
16
16
|
title: string;
|
|
17
17
|
/** Trusted CSS assembled by the page. */
|
|
18
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;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
@@ -41,6 +47,17 @@ export function tabTitle(product: string, subject?: string, count?: string): str
|
|
|
41
47
|
*/
|
|
42
48
|
export const TAB_TITLE_JS = String(tabTitle);
|
|
43
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)}"`;
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
/**
|
|
45
62
|
* The shared document head: charset, viewport, colour scheme, title, the page
|
|
46
63
|
* CSS, and the pre-paint theme bootstrap. The body and its scripts stay with
|
|
@@ -53,7 +70,7 @@ export function docHead(options: DocHeadOptions): string {
|
|
|
53
70
|
<title>${escapeHtml(options.title)}</title>
|
|
54
71
|
${markIcon()}
|
|
55
72
|
<style>${options.css}</style>
|
|
56
|
-
<script src="${THEME_JS_PATH}"></script>`;
|
|
73
|
+
<script src="${options.base ?? ""}${THEME_JS_PATH}"></script>`;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/**
|
|
@@ -202,7 +219,7 @@ export interface KeyHint {
|
|
|
202
219
|
* for the sheet, `#keys-close` for its own close button — and the trigger button
|
|
203
220
|
* is the page's own concern, since the two pages name it differently.
|
|
204
221
|
*/
|
|
205
|
-
export function keysSheet(rows: readonly KeyHint[]): string {
|
|
222
|
+
export function keysSheet(rows: readonly KeyHint[], note?: string): string {
|
|
206
223
|
const kbd = (key: string): string => `<kbd class="bf-kbd">${escapeHtml(key)}</kbd>`;
|
|
207
224
|
const body = rows
|
|
208
225
|
.map((row) => {
|
|
@@ -214,6 +231,7 @@ export function keysSheet(rows: readonly KeyHint[]): string {
|
|
|
214
231
|
<div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="keys-title">
|
|
215
232
|
<div class="bf-dialog__head"><h2 id="keys-title">Keyboard shortcuts</h2><span class="bf-spacer"></span><button type="button" class="bf-btn" id="keys-close">Close</button></div>
|
|
216
233
|
<dl class="bf-keys">${body}</dl>
|
|
234
|
+
${note === undefined ? "" : `<p class="bf-keys__note">${escapeHtml(note)}</p>`}
|
|
217
235
|
</div>
|
|
218
236
|
</div>`;
|
|
219
237
|
}
|
package/lib/web/runtime.ts
CHANGED
|
@@ -156,20 +156,19 @@ export function dialogScript(options: DialogOptions): string {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
// Tab cycles inside the dialog: leaving it would put focus behind the scrim,
|
|
159
|
-
// where the page looks inert but is not.
|
|
159
|
+
// where the page looks inert but is not. Every Tab is redirected rather than only
|
|
160
|
+
// the two that wrap, because the panel is the first node and the *container* of the
|
|
161
|
+
// others: a browser's own backwards step from the last node has no sibling to land
|
|
162
|
+
// on and walks out of the dialog instead.
|
|
160
163
|
dialog.addEventListener("keydown", function (event) {
|
|
161
164
|
if (event.key !== "Tab") return;
|
|
162
165
|
var nodes = focusables();
|
|
163
166
|
if (nodes.length === 0) { event.preventDefault(); return; }
|
|
164
|
-
var
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else if (!event.shiftKey && document.activeElement === last) {
|
|
170
|
-
event.preventDefault();
|
|
171
|
-
first.focus();
|
|
172
|
-
}
|
|
167
|
+
var at = nodes.indexOf(document.activeElement);
|
|
168
|
+
var step = event.shiftKey ? -1 : 1;
|
|
169
|
+
var next = at === -1 ? (event.shiftKey ? nodes.length - 1 : 0) : (at + step + nodes.length) % nodes.length;
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
nodes[next].focus();
|
|
173
172
|
});
|
|
174
173
|
|
|
175
174
|
// Capture, so Esc closes the dialog before a page handler peels another layer.
|
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.22.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",
|
|
@@ -84,6 +84,11 @@
|
|
|
84
84
|
"lib/**",
|
|
85
85
|
"skills/**",
|
|
86
86
|
"themes/**",
|
|
87
|
+
"assets/screens/board.png",
|
|
88
|
+
"assets/screens/chrome.png",
|
|
89
|
+
"assets/screens/file-browse.png",
|
|
90
|
+
"assets/screens/plan-review.png",
|
|
91
|
+
"assets/screens/plan.png",
|
|
87
92
|
"!**/*.test.ts"
|
|
88
93
|
],
|
|
89
94
|
"publishConfig": {
|
|
@@ -92,6 +97,7 @@
|
|
|
92
97
|
},
|
|
93
98
|
"pi": {
|
|
94
99
|
"extensions": [
|
|
100
|
+
"./extensions/serve/index.ts",
|
|
95
101
|
"./extensions/worktree/index.ts",
|
|
96
102
|
"./extensions/rewind/index.ts",
|
|
97
103
|
"./extensions/question/index.ts",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ cannot show; the repository supplies every command, path, and convention that ca
|
|
|
13
13
|
be verified. Plan mode is the review gate: no write happens before approval.
|
|
14
14
|
|
|
15
15
|
`/skill:init` loads this skill. If plan mode is not active, call
|
|
16
|
-
`
|
|
16
|
+
`plan` with action `enter` and wait for confirmation. Keep the interview and the plan
|
|
17
17
|
review in that order; do not write the file early to save a step. A declined plan
|
|
18
18
|
mode ends the skill: stop and explain, rather than writing directly.
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ mode ends the skill: stop and explain, rather than writing directly.
|
|
|
29
29
|
└─> draft: new file, exact edits, or an honest no-op
|
|
30
30
|
└─> quality gate
|
|
31
31
|
└─> plan outline with ## Steps and ## Verification
|
|
32
|
-
└─>
|
|
32
|
+
└─> plan · write ──> plan · exit
|
|
33
33
|
├─ not approved ──> refine the same plan
|
|
34
34
|
└─ approved ──────> write/edit the target
|
|
35
35
|
└─ read back + quality gate
|
|
@@ -147,7 +147,7 @@ evidence, not as a replacement for reading the repository.
|
|
|
147
147
|
|
|
148
148
|
## 5. Plan and review
|
|
149
149
|
|
|
150
|
-
- Save the proposal with `
|
|
150
|
+
- Save the proposal with `plan` action `write` in the documented outline. The plan must
|
|
151
151
|
include:
|
|
152
152
|
- `## Goal` — the outcome for the instruction file;
|
|
153
153
|
- `## Context` — the sourced evidence sheet, the effective-file finding, and the
|
|
@@ -162,9 +162,9 @@ evidence, not as a replacement for reading the repository.
|
|
|
162
162
|
- `## Verification` — the project's own check when it declares one (`run_check`,
|
|
163
163
|
or the narrower docs or structure test that guards the instruction file), then
|
|
164
164
|
reading the written file back against the checklist.
|
|
165
|
-
- Call `
|
|
165
|
+
- Call `plan` action `exit` with the returned path.
|
|
166
166
|
- If the user refines or rejects the plan, stay in plan mode, rewrite the same
|
|
167
|
-
plan file with `
|
|
167
|
+
plan file with `plan` action `write` (`plan_path`), and present it again.
|
|
168
168
|
- No write to the target may happen before approval.
|
|
169
169
|
|
|
170
170
|
## 6. Apply and verify
|
package/skills/next/SKILL.md
CHANGED
|
@@ -15,11 +15,9 @@ project wins.
|
|
|
15
15
|
The conversation decides where the candidates come from (section 3); the request
|
|
16
16
|
decides only what to ask and where to look.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
request is already scoped to one of those, name the skill that answers it and
|
|
22
|
-
hand over, instead of returning a generic list.
|
|
18
|
+
A sibling skill, where it is installed, owns a narrower question: an audit of
|
|
19
|
+
*where to refactor* is `refactor`. When the request is already scoped to it, name
|
|
20
|
+
that skill and hand over, instead of returning a generic list.
|
|
23
21
|
|
|
24
22
|
A next-steps block that the host draws itself is not this skill's output: that is
|
|
25
23
|
the reply's suggested follow-up, a turn-level hint. This skill inspects state, so
|
|
@@ -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
|
-
}
|