@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -16,7 +16,7 @@ import type { CompactionResult, Theme } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
17
17
|
import { messageOf } from "../../lib/errors.ts";
|
|
18
18
|
import { formatTokens } from "../../lib/format.ts";
|
|
19
|
-
import { screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
19
|
+
import { screenFooter, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
20
|
import { ConfirmState } from "../../lib/tui/confirm.ts";
|
|
21
21
|
import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll } from "../../lib/tui/cursor.ts";
|
|
22
22
|
import { gaugeBar, gaugeRole } from "../../lib/tui/gauge.ts";
|
|
@@ -26,7 +26,7 @@ import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
|
26
26
|
import { formatBudget, formatCompaction, formatDrift } from "./format.ts";
|
|
27
27
|
import type { ContextBucket, ContextReport } from "./types.ts";
|
|
28
28
|
|
|
29
|
-
/** Fixed rows around the scrollable body: title, gauge, budget, blank, blank, footer,
|
|
29
|
+
/** Fixed rows around the scrollable body: title, gauge, budget, blank, blank, footer, closing rule. */
|
|
30
30
|
const CHROME_ROWS = 7;
|
|
31
31
|
|
|
32
32
|
/** Cells in the composition bar. */
|
|
@@ -137,16 +137,28 @@ export class ContextComponent implements Component {
|
|
|
137
137
|
const { percent, tokens, window } = this.report;
|
|
138
138
|
if (window <= 0) return truncateToWidth(` ${paint(this.theme, "meta", "no model context window")}`, width);
|
|
139
139
|
const role = gaugeRole(percent);
|
|
140
|
+
// The bar and the printed percentage come from one rounded value, so a
|
|
141
|
+
// label reading `0%` can never sit beside a filled cell. The tone stays on
|
|
142
|
+
// the exact percent, so the warn and danger thresholds do not move.
|
|
143
|
+
const shown = percent === null ? null : Math.round(percent);
|
|
140
144
|
const value = tokens === null ? `? / ${formatTokens(window)}` : `${formatTokens(tokens)} / ${formatTokens(window)}`;
|
|
141
|
-
const bar = gaugeBar(this.theme, { percent, blocks: 10 });
|
|
142
|
-
const label =
|
|
145
|
+
const bar = gaugeBar(this.theme, { percent: shown, blocks: 10 });
|
|
146
|
+
const label = shown === null ? "unknown" : `${shown}%`;
|
|
143
147
|
return truncateToWidth(` ${paint(this.theme, role, value)} ${bar} ${paint(this.theme, role, label)}`, width);
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
/** The composition header: the denominator the percent column is a share of. */
|
|
151
|
+
private compositionLine(width: number): string {
|
|
152
|
+
const label = paint(this.theme, "label", "Composition");
|
|
153
|
+
const total = paint(this.theme, "meta", `${formatTokens(this.report.estimate)} estimate`);
|
|
154
|
+
const full = ` ${label} · ${total}`;
|
|
155
|
+
return visibleWidth(full) <= width ? full : truncateToWidth(` ${label}`, width);
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
/** One composition row; optional columns drop as the terminal narrows. */
|
|
147
159
|
private bucketLine(bucket: ContextBucket, width: number): string {
|
|
148
160
|
const indent = " ".repeat(bucket.depth > 0 ? 4 : 2);
|
|
149
|
-
const labelWidth = bucket.depth > 0 ?
|
|
161
|
+
const labelWidth = bucket.depth > 0 ? 20 : 22;
|
|
150
162
|
const label = padCell(bucket.label, labelWidth);
|
|
151
163
|
const tokens = formatTokens(bucket.tokens).padStart(6);
|
|
152
164
|
const share = shareCell(bucket.share);
|
|
@@ -167,13 +179,17 @@ export class ContextComponent implements Component {
|
|
|
167
179
|
return truncateToWidth(head, width);
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
/** The scrollable body: composition, drift, compaction, and the largest
|
|
182
|
+
/** The scrollable body: composition, drift, compaction, and the largest messages. */
|
|
171
183
|
private buildBody(width: number): string[] {
|
|
172
184
|
const report = this.report;
|
|
173
|
-
const lines: string[] =
|
|
185
|
+
const lines: string[] = [];
|
|
186
|
+
if (report.buckets.length > 0) {
|
|
187
|
+
lines.push(this.compositionLine(width));
|
|
188
|
+
for (const bucket of report.buckets) lines.push(this.bucketLine(bucket, width));
|
|
189
|
+
}
|
|
174
190
|
|
|
175
191
|
const drift = formatDrift(report);
|
|
176
|
-
if (drift) lines.push("", truncateToWidth(` ${paint(this.theme, "
|
|
192
|
+
if (drift) lines.push("", truncateToWidth(` ${paint(this.theme, "meta", drift)}`, width));
|
|
177
193
|
|
|
178
194
|
lines.push(
|
|
179
195
|
"",
|
|
@@ -184,7 +200,7 @@ export class ContextComponent implements Component {
|
|
|
184
200
|
);
|
|
185
201
|
|
|
186
202
|
if (report.largest.length > 0) {
|
|
187
|
-
lines.push("", ` ${paint(this.theme, "label", "Largest
|
|
203
|
+
lines.push("", ` ${paint(this.theme, "label", "Largest messages")}`);
|
|
188
204
|
for (const block of report.largest) {
|
|
189
205
|
const tokens = formatTokens(block.tokens).padStart(6);
|
|
190
206
|
const role = padCell(block.role, 11);
|
|
@@ -230,7 +246,7 @@ export class ContextComponent implements Component {
|
|
|
230
246
|
if (this.body.length > this.visible) {
|
|
231
247
|
lines.push(formatRange(this.theme, w, { start: this.scroll, end, total: this.body.length }));
|
|
232
248
|
}
|
|
233
|
-
lines.push(
|
|
249
|
+
lines.push(...screenFooter(this.theme, w, this.footerLine(w)));
|
|
234
250
|
return lines.map((line) => truncateToWidth(line, w));
|
|
235
251
|
}
|
|
236
252
|
|
|
@@ -79,7 +79,7 @@ export interface CompactionHistory {
|
|
|
79
79
|
last: CompactionSummary | null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** One of the largest single
|
|
82
|
+
/** One of the largest single messages in the projected context. */
|
|
83
83
|
export interface LargestBlock {
|
|
84
84
|
/** Estimated tokens. */
|
|
85
85
|
tokens: number;
|
|
@@ -99,7 +99,7 @@ export interface ContextReport {
|
|
|
99
99
|
percent: number | null;
|
|
100
100
|
/** `window - tokens`, `null` when `tokens` is unknown. */
|
|
101
101
|
free: number | null;
|
|
102
|
-
/** `window - reserve - tokens`, `null` when `tokens` is unknown. */
|
|
102
|
+
/** `window - reserve - tokens`, `null` when `tokens` is unknown or automatic compaction is off. */
|
|
103
103
|
headroom: number | null;
|
|
104
104
|
/** The resolved `reserveTokens` for the active model. */
|
|
105
105
|
reserve: number;
|
|
@@ -115,8 +115,10 @@ export interface ContextReport {
|
|
|
115
115
|
estimate: number;
|
|
116
116
|
/** `tokens - estimate` when the host's estimate is known, else `null`. */
|
|
117
117
|
drift: number | null;
|
|
118
|
+
/** True when the projection holds no assistant message: Pi's total is its message-only fallback. */
|
|
119
|
+
beforeFirstReply: boolean;
|
|
118
120
|
/** The branch's compaction history. */
|
|
119
121
|
compaction: CompactionHistory;
|
|
120
|
-
/** The largest projected
|
|
122
|
+
/** The largest projected messages, biggest first. */
|
|
121
123
|
largest: LargestBlock[];
|
|
122
124
|
}
|
|
@@ -56,8 +56,9 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
56
56
|
external images are shown as links rather than fetched.
|
|
57
57
|
- **Sortable listings.** The Name / Size / Modified headers sort the current
|
|
58
58
|
directory, directories first, and the choice is remembered per browser.
|
|
59
|
-
- **Copy and wrap.** Copy a file's root-relative path or its contents,
|
|
60
|
-
|
|
59
|
+
- **Copy and wrap.** Copy a file's root-relative path or its contents, copy a
|
|
60
|
+
`path:12-30` reference for the lines you selected, and toggle line wrapping for
|
|
61
|
+
text and raw views.
|
|
61
62
|
- **Theme toggle.** A header button switches between the dark and light
|
|
62
63
|
palettes and remembers the choice per browser; until it is used, the palette
|
|
63
64
|
follows the system `prefers-color-scheme`.
|
|
@@ -66,7 +67,8 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
66
67
|
highlighted match; Enter opens the file.
|
|
67
68
|
- **Keyboard.** `/` or `Ctrl/Cmd+K` opens quick open, `-` folds or expands
|
|
68
69
|
every folder in the tree, `u` toggles line wrapping, `r` switches rendered and
|
|
69
|
-
raw Markdown, `
|
|
70
|
+
raw Markdown, `y` copies a reference to the selected lines, `n`/`p` step
|
|
71
|
+
between a diff's hunks, and `?` opens a shortcut
|
|
70
72
|
sheet. A diff's hunk headers pin to the top of the code viewport while its
|
|
71
73
|
body scrolls.
|
|
72
74
|
- **Read-only and loopback-scoped.** No editing, no writing, no LAN exposure.
|
|
@@ -88,11 +90,13 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
88
90
|
connecting (accent), stalled (amber, after three missed frames), or offline
|
|
89
91
|
(red); the branch chip, the `● N changes +A −D` chip, and the change markers in
|
|
90
92
|
the tree and the listing update in place; and the footer line reports `up 2m
|
|
91
|
-
14s · 37 requests`. The chip names its state and never counts,
|
|
92
|
-
|
|
93
|
-
the same loopback server, so nothing extra is
|
|
94
|
-
that stream is also how the server notices the
|
|
95
|
-
after which it stops listening.
|
|
93
|
+
14s · 37 requests`. The chip names its state and never counts, and a frame
|
|
94
|
+
whose change set did not move repaints nothing at all. One Server-Sent Events
|
|
95
|
+
stream per page carries it, on the same loopback server, so nothing extra is
|
|
96
|
+
opened and nothing is written; that stream is also how the server notices the
|
|
97
|
+
last page closing its window, after which it stops listening. A page that lists
|
|
98
|
+
a folder also checks that folder, and offers a `⟳ reload` control when a file
|
|
99
|
+
has appeared or gone on disk since it drew its rows.
|
|
96
100
|
- **No dependencies.** `node:http` plus the inline sprite, CSS, and client
|
|
97
101
|
script.
|
|
98
102
|
|
|
@@ -136,10 +140,24 @@ errors. The chip names its state rather than counting, so it is repainted only
|
|
|
136
140
|
when that state changes. The same frame carries the branch (or detached head), the
|
|
137
141
|
uncommitted-change totals and paths, the server's uptime, and the requests it
|
|
138
142
|
has answered, so the branch chip, the changes chip, the markers, and the footer
|
|
139
|
-
line are painted from one snapshot rather than four requests.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
line are painted from one snapshot rather than four requests. Each change set
|
|
144
|
+
also carries a short revision, and a frame whose revision the page already
|
|
145
|
+
painted does no work at all — so an idle tree costs no DOM, however many rows the
|
|
146
|
+
page drew. Losing the stream leaves everything the page last showed in place: it
|
|
147
|
+
is read-only, so a stale label is never a stale action. `"live": false` turns the
|
|
148
|
+
whole surface off — no chip, no stream, no `/events` route — and so does
|
|
149
|
+
JavaScript off.
|
|
150
|
+
|
|
151
|
+
A page that lists a folder carries the revision of the names it drew, and asks
|
|
152
|
+
`/api/dir-rev` for the folder's current one on the frame cadence — one directory
|
|
153
|
+
read every five seconds by default, skipped while the tab is hidden and stopped
|
|
154
|
+
for good once it has an answer. A difference reveals `⟳ reload` beside the live
|
|
155
|
+
chip, which reloads the page; the rows themselves are never touched, because a
|
|
156
|
+
page that could grow a row is a page that writes. A folder whose listing was
|
|
157
|
+
truncated, and a file, `/diff`, or error page, has nothing to compare and shows
|
|
158
|
+
no control. At most eight pages may hold the stream at once: a ninth is refused
|
|
159
|
+
and its chip reads `offline`, which keeps one local page from pinning the server
|
|
160
|
+
— and the git read behind each frame — alive.
|
|
143
161
|
|
|
144
162
|
The stream is also how the server knows a page is still there: when the last one
|
|
145
163
|
leaves, the port is released after ten seconds and the chip is cleared, because
|
|
@@ -147,6 +165,32 @@ nothing is left to serve. That is why a page with JavaScript off — or with liv
|
|
|
147
165
|
status off — never counts as a reader, and the server then waits for
|
|
148
166
|
`/browse stop` or the end of the session.
|
|
149
167
|
|
|
168
|
+
### The page
|
|
169
|
+
|
|
170
|
+
The page is keyboard-first, and the tree is one tab stop rather than two per
|
|
171
|
+
folder:
|
|
172
|
+
|
|
173
|
+
| Key | What it does in the tree |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `↑` / `↓` | Move between the rows on screen |
|
|
176
|
+
| `→` | Open a folder (reading its children first if needed), or enter it |
|
|
177
|
+
| `←` | Close the folder, or step out to its parent |
|
|
178
|
+
| `Space` | Open or close the focused folder |
|
|
179
|
+
| `Enter` | Follow the focused row's link |
|
|
180
|
+
| `Home` / `End` | First or last row on screen |
|
|
181
|
+
|
|
182
|
+
The folder's own link carries `aria-expanded`, so a screen reader hears its
|
|
183
|
+
state once; the twisty beside it is a pointer control kept out of the tab order.
|
|
184
|
+
A mouse click moves the tab stop to that row, so `Tab` comes back to where you
|
|
185
|
+
were. Elsewhere the page keeps its own keys: `/` or `Ctrl`/`Cmd`+`K` for quick
|
|
186
|
+
open, `-` to fold, `u` to wrap, `r` for raw Markdown, `n`/`p` between diff hunks,
|
|
187
|
+
and `?` for the sheet, whose dialog now traps `Tab` and returns focus to its
|
|
188
|
+
button.
|
|
189
|
+
|
|
190
|
+
Printing is supported: the paper gets the light palette whatever the screen is
|
|
191
|
+
showing, the chrome and the tree are dropped, and a file or diff prints whole
|
|
192
|
+
instead of inside its scroll box.
|
|
193
|
+
|
|
150
194
|
## Routes
|
|
151
195
|
|
|
152
196
|
| Route | Result |
|
|
@@ -186,7 +230,7 @@ document and the project document (project wins):
|
|
|
186
230
|
"diffContext": 3, // unified diff context lines
|
|
187
231
|
"maxDiffLines": 4000, // diff lines rendered before truncation
|
|
188
232
|
"live": true, // stream live status to open pages
|
|
189
|
-
"liveIntervalMs":
|
|
233
|
+
"liveIntervalMs": 5000 // ms between status frames while a page is connected
|
|
190
234
|
}
|
|
191
235
|
```
|
|
192
236
|
|
|
@@ -223,11 +267,18 @@ document and the project document (project wins):
|
|
|
223
267
|
attributes; only the fixed `tok-*` class names reach the page.
|
|
224
268
|
- **Event stream.** `/events` is a `GET` on the same loopback server, behind
|
|
225
269
|
the same method and `Host` checks, and it only reads: the frame is the branch,
|
|
226
|
-
the change paths, an uptime, and a request count, all of
|
|
227
|
-
feeds already renders. It carries no page policy
|
|
228
|
-
document), sends `Cache-Control: no-store`, and
|
|
229
|
-
A tick runs only while a page is attached, and
|
|
230
|
-
idle server does no work and a forgotten tab
|
|
270
|
+
the change paths and their revision, an uptime, and a request count, all of
|
|
271
|
+
which the page it feeds already renders. It carries no page policy
|
|
272
|
+
(`text/event-stream` is not a document), sends `Cache-Control: no-store`, and
|
|
273
|
+
cannot be cached or replayed. A tick runs only while a page is attached, and
|
|
274
|
+
the timer is `unref`ed, so an idle server does no work and a forgotten tab
|
|
275
|
+
cannot hold the process open. At most eight pages may attach; a ninth is
|
|
276
|
+
answered `204`, the one status an event source never reconnects after.
|
|
277
|
+
- **Directory check.** `/api/dir-rev` answers one folder's listing revision, so
|
|
278
|
+
the page can tell whether its rows are stale. It goes through the same path
|
|
279
|
+
guard as every other route and reads one directory; its answer is a hash of the
|
|
280
|
+
names, not the names themselves, so it discloses nothing the page above it does
|
|
281
|
+
not already show.
|
|
231
282
|
- **Caps.** `maxFileBytes` (inline rendering), `maxTextLines` (rendered lines),
|
|
232
283
|
`maxThumbBytes` (thumbnails), and `maxDirEntries` (listings) bound every read.
|
|
233
284
|
- **Git reads.** The header chip and the diff view run read-only git
|
|
@@ -245,9 +296,11 @@ document and the project document (project wins):
|
|
|
245
296
|
approximated rather than parsed.
|
|
246
297
|
- Live status updates what the page already contains: the header chips, the
|
|
247
298
|
markers on the rows it rendered, and the footer line. A file that appears on
|
|
248
|
-
disk
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
disk does not join the listing by itself — the page offers `⟳ reload` instead —
|
|
300
|
+
and the check covers the folder the page lists, by name, not the folders inside
|
|
301
|
+
it, a listed file's contents, the `/diff` table, or a file page's `changes`
|
|
302
|
+
action, which stay page-load snapshots. The branch chip is only patched when
|
|
303
|
+
the page was served inside a repository.
|
|
251
304
|
- `/browse` has no auth; a malicious local page could probe the port. The `Host`
|
|
252
305
|
allowlist and the absence of CORS headers mitigate reads, and the server is
|
|
253
306
|
read-only.
|
|
@@ -288,6 +341,18 @@ document and the project document (project wins):
|
|
|
288
341
|
method and `Host`, parses the URL, matches a route, and runs every path
|
|
289
342
|
through `paths.ts`. It returns a plain `ServeResponse`, so routing is tested
|
|
290
343
|
without a socket; `server.ts` is the only part that binds a port.
|
|
344
|
+
- **One snapshot, one frame.** The status source is shared by every attached
|
|
345
|
+
page, so a tick costs one `git status`, one `git diff --stat`, and one
|
|
346
|
+
serialization however many tabs are open. The frame carries a revision of the
|
|
347
|
+
change set, and the client compares it before painting, which is why an idle
|
|
348
|
+
page does no DOM work. The cadence is what bounds those reads, so the default
|
|
349
|
+
is five seconds rather than matching the providers' two-second render cache.
|
|
350
|
+
- **A poll, not a watcher.** The folder check is one request from the page to
|
|
351
|
+
`/api/dir-rev`, not an `fs.watch`: recursive watching is the one part of
|
|
352
|
+
`node:fs` whose availability and event semantics differ per platform, and it
|
|
353
|
+
would add a long-lived resource to start and release around `close()`. The
|
|
354
|
+
check compares names, so it is a directory read rather than a stat pass, and it
|
|
355
|
+
stops at the first difference because the control, not the checker, is the fix.
|
|
291
356
|
- **Progressive UI.** The page is server-rendered HTML with an inline style
|
|
292
357
|
block and inline SVG icons. `client.ts` enhances it: the filter hides
|
|
293
358
|
non-matching rows while keeping ancestors, disclosure triangles lazily
|
|
@@ -399,7 +464,9 @@ document and the project document (project wins):
|
|
|
399
464
|
| `urls.ts` | Shared link/image URL policy: scheme allowlist and `/view`/`/raw` rewriting |
|
|
400
465
|
| `git.ts` | Read-only branch/dirty provider for the header chip |
|
|
401
466
|
| `icons.ts` | Inline SVG sprite and the per-language mapping |
|
|
402
|
-
| `client.ts` | The `/app.js` source
|
|
467
|
+
| `client.ts` | The `/app.js` source: sort, wrap, copy, hunk navigation, reading progress, and the page keymap |
|
|
468
|
+
| `tree.ts` | The sidebar block: path filter, fold-all, lazy tree loading, and the tree's keyboard model (interpolated into `client.ts`) |
|
|
469
|
+
| `palette.ts` | The quick-open palette: fuzzy search, the combobox wiring, and the `window.bfPalette` contract (interpolated into `client.ts`) |
|
|
403
470
|
| `live-client.ts` | The client's live status block: the event stream, the chips, the markers, and the footer line (interpolated into `client.ts`) |
|
|
404
471
|
| `live.ts` | The live status snapshot, its frame, and the fan-out channel that ticks it |
|
|
405
472
|
| `styles.ts` | The page-only `browse-*` stylesheet (the `bf-*` kit comes from `lib/web`) |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header chrome for the browse pages.
|
|
3
|
+
*
|
|
4
|
+
* The chips the toolbar carries — the git branch, the change totals, the live
|
|
5
|
+
* connection state — and the footer's live line. Everything here is pure string
|
|
6
|
+
* building over an escaped view model: a branch name, a count, and a status word
|
|
7
|
+
* are all text a repository can choose, so each one crosses `escapeHtml` before
|
|
8
|
+
* it reaches the page. The page shell and the page bodies stay in `html.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { escapeHtml } from "../../lib/escape.ts";
|
|
12
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
13
|
+
import type { GitStatus } from "./git.ts";
|
|
14
|
+
import type { ChangeIndex } from "./live.ts";
|
|
15
|
+
|
|
16
|
+
/** The header's git chip: the branch, or the detached head; changes are the changes chip's job. */
|
|
17
|
+
export function renderGit(git: GitStatus | undefined): string {
|
|
18
|
+
if (!git) return "";
|
|
19
|
+
const name = git.branch ?? git.head;
|
|
20
|
+
if (!name) return "";
|
|
21
|
+
const glyph = git.branch ? GLYPHS.branch : GLYPHS.detached;
|
|
22
|
+
const aria = git.branch ? `git branch ${git.branch}` : `git detached at ${git.head ?? ""}`;
|
|
23
|
+
return `<span class="bf-chip browse-git" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span><span class="browse-git__name">${escapeHtml(name)}</span></span>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The live chrome a page renders: how often its stream is sent a frame. */
|
|
27
|
+
export interface LivePage {
|
|
28
|
+
intervalMs: number;
|
|
29
|
+
/** Root-relative folder the page listed; absent on a page with no listing to check. */
|
|
30
|
+
dir?: string;
|
|
31
|
+
/** Revision of the names that listing drew, which the reload control compares against. */
|
|
32
|
+
rev?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The header's live controls: the connection chip, and the reload control the
|
|
37
|
+
* client reveals once the folder the page drew has changed on disk. Both start
|
|
38
|
+
* hidden, so a page with JS off — or with live status disabled — never shows a
|
|
39
|
+
* state or a control it cannot keep true; the reload control is not rendered at
|
|
40
|
+
* all when the page has no listing revision to compare.
|
|
41
|
+
*
|
|
42
|
+
* The chip is deliberately not a `role="status"` region: the header is where a
|
|
43
|
+
* reader looks for the page's state, not a place to announce every change to it.
|
|
44
|
+
*/
|
|
45
|
+
export function renderLiveChrome(live: LivePage | undefined): string {
|
|
46
|
+
if (!live) return "";
|
|
47
|
+
const rev =
|
|
48
|
+
live.rev === undefined ? "" : ` data-dir="${escapeHtml(live.dir ?? "")}" data-rev="${escapeHtml(live.rev)}"`;
|
|
49
|
+
const chip = `<span class="bf-chip bf-chip--accent browse-live" id="live-state" title="Live status from the browse server" data-interval="${live.intervalMs}"${rev} hidden><span class="browse-live__glyph" aria-hidden="true">${escapeHtml(GLYPHS.browse)}</span><span class="browse-live__text">connecting</span></span>`;
|
|
50
|
+
if (live.rev === undefined) return chip;
|
|
51
|
+
return `${chip}<button type="button" class="bf-chip bf-chip--warn browse-reload" id="reload-hint" title="This folder changed on disk" hidden>⟳ reload</button>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The footer's live line, hidden until the first frame: uptime and requests are
|
|
56
|
+
* server facts the rendered page does not know at build time.
|
|
57
|
+
*/
|
|
58
|
+
export function renderLiveStatus(): string {
|
|
59
|
+
return `<span class="browse-live-line" id="live-status" hidden></span>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The header's changes chip, linking to the diff overview. With live status on,
|
|
64
|
+
* a hidden shell is always rendered so the chip can appear and disappear as the
|
|
65
|
+
* working tree changes, without the client inserting elements into the header.
|
|
66
|
+
*/
|
|
67
|
+
export function renderChanges(summary: ChangeIndex | undefined, live: LivePage | undefined): string {
|
|
68
|
+
const count = summary?.count ?? 0;
|
|
69
|
+
if (count <= 0) {
|
|
70
|
+
return live
|
|
71
|
+
? `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" hidden></a>`
|
|
72
|
+
: "";
|
|
73
|
+
}
|
|
74
|
+
const label = `${count} change${count === 1 ? "" : "s"}`;
|
|
75
|
+
const insertions = summary?.insertions ?? 0;
|
|
76
|
+
const deletions = summary?.deletions ?? 0;
|
|
77
|
+
const totals =
|
|
78
|
+
insertions + deletions > 0
|
|
79
|
+
? ` <span class="browse-diff-totals"><span class="browse-diff-add">+${insertions}</span> <span class="browse-diff-del">−${deletions}</span></span>`
|
|
80
|
+
: "";
|
|
81
|
+
const aria = `${label} against HEAD${insertions + deletions > 0 ? `, ${insertions} insertions and ${deletions} deletions` : ""}`;
|
|
82
|
+
return `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" aria-label="${escapeHtml(aria)}"><span class="browse-diff__dot" aria-hidden="true">●</span>${escapeHtml(label)}${totals}</a>`;
|
|
83
|
+
}
|