@gavin-hu/my-pi-agent 0.18.0 → 0.19.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 +52 -44
- package/README.zh-CN.md +46 -41
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +344 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +554 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +285 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +226 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +258 -162
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
- package/extensions/board/commands.ts +87 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +72 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +312 -0
- package/extensions/board/result.ts +99 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +75 -0
- package/extensions/board/seat.ts +29 -0
- package/extensions/board/snapshot.ts +41 -0
- package/extensions/board/tools.ts +279 -0
- package/extensions/board/transport/router.ts +172 -0
- package/extensions/board/transport/server.ts +149 -0
- package/extensions/board/types.ts +54 -0
- package/extensions/board/view/actions.ts +306 -0
- package/extensions/board/view/client.ts +41 -0
- package/extensions/board/view/keys.ts +121 -0
- package/extensions/board/view/live.ts +397 -0
- package/extensions/board/view/page.ts +158 -0
- package/extensions/board/view/render.ts +496 -0
- package/extensions/board/view/styles.ts +211 -0
- package/extensions/board/view/team.ts +305 -0
- package/extensions/board/writes.ts +99 -0
- package/extensions/bytefly/panel/commands.ts +40 -7
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +3 -20
- package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +147 -38
- package/extensions/plan/index.ts +6 -4
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +8 -11
- package/extensions/plan/policy.ts +20 -12
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +21 -9
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/tools/exit.ts +11 -95
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +0 -8
- package/extensions/plan/web-client.ts +1 -1
- package/extensions/plan/web-page.ts +0 -7
- package/extensions/plan/web-router.ts +3 -57
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +4 -2
- package/extensions/session-bus/runtime.ts +129 -238
- package/extensions/session-bus/store.ts +42 -6
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +4 -4
- package/extensions/session-bus/types.ts +13 -13
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +112 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +105 -0
- package/extensions/team/index.ts +33 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +246 -0
- package/extensions/team/types.ts +48 -0
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +76 -92
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/guard.ts +3 -5
- package/lib/README.md +8 -6
- package/lib/board/card.ts +290 -0
- package/lib/board/links.ts +117 -0
- package/lib/board/page-pointer.ts +256 -0
- package/lib/board/round-text.ts +72 -0
- package/lib/board/round.ts +89 -0
- package/lib/board/rules.ts +304 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +310 -0
- package/lib/board/view.ts +150 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +13 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +234 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +110 -0
- package/lib/http/token.ts +38 -0
- package/lib/policy.ts +12 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +130 -0
- package/lib/session-tool.ts +184 -0
- package/lib/tool-names.ts +25 -2
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/README.md +5 -2
- package/lib/web/components.ts +5 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* pair — so the whole feed is tested without one.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
15
|
+
import type { EventSink } from "../../../lib/http/index.ts";
|
|
16
|
+
import { defaultInterval, type RepeatTimer } from "../../../lib/timer.ts";
|
|
17
|
+
import { LIVE_MAX_CLIENTS } from "../config/config.ts";
|
|
18
|
+
import { changeDirs } from "../text/diff.ts";
|
|
19
|
+
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "../git/git.ts";
|
|
19
20
|
import { changeRevision } from "./rev.ts";
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -141,48 +142,25 @@ export function frameOf(snapshot: LiveSnapshot): string {
|
|
|
141
142
|
return `event: status\ndata: ${JSON.stringify(snapshot)}\n\n`;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
/** A connected page
|
|
145
|
-
export
|
|
146
|
-
/** Write one frame. */
|
|
147
|
-
write(frame: string): void;
|
|
148
|
-
/** End the stream. */
|
|
149
|
-
end(): void;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Starts a repeating tick and returns its cancel. */
|
|
153
|
-
export type LiveTimer = (tick: () => void, intervalMs: number) => () => void;
|
|
154
|
-
|
|
155
|
-
/** Starts a one-shot delay and returns its cancel. */
|
|
156
|
-
export type LiveTimeout = (run: () => void, delayMs: number) => () => void;
|
|
157
|
-
|
|
158
|
-
/** The default ticker: a repeating timer that cannot hold the process open. */
|
|
159
|
-
const defaultTicker: LiveTimer = (tick, intervalMs) => defaultInterval(intervalMs, tick);
|
|
160
|
-
|
|
161
|
-
/** The default grace timer: one shot, and it cannot hold the process open either. */
|
|
162
|
-
const defaultTimeout: LiveTimeout = (run, delayMs) => defaultTimer(delayMs, run);
|
|
145
|
+
/** A connected page, as the channel writes it: the sink's two verbs. */
|
|
146
|
+
export type LiveClient = EventSink;
|
|
163
147
|
|
|
164
148
|
export interface LiveChannelOptions {
|
|
165
149
|
/** What each frame carries. */
|
|
166
150
|
status: LiveStatusSource;
|
|
167
151
|
/** How far apart the frames are, in ms. */
|
|
168
152
|
intervalMs: number;
|
|
169
|
-
/** How long the last page's departure is held before `onIdle` is called, in ms. */
|
|
170
|
-
idleMs: number;
|
|
171
153
|
/** How many pages may attach at once; defaults to `LIVE_MAX_CLIENTS`. */
|
|
172
154
|
maxClients?: number;
|
|
173
|
-
/** Called once the last page has been gone for `idleMs`; omitted, the channel only idles. */
|
|
174
|
-
onIdle?: () => void;
|
|
175
155
|
/** Timer seam; defaults to an unref'd interval. */
|
|
176
|
-
timer?:
|
|
177
|
-
/** Grace seam; defaults to an unref'd timeout. */
|
|
178
|
-
timeout?: LiveTimeout;
|
|
156
|
+
timer?: RepeatTimer;
|
|
179
157
|
}
|
|
180
158
|
|
|
181
159
|
/** The fan-out for one server: pages attach, ticks publish, close ends them. */
|
|
182
160
|
export interface LiveChannel {
|
|
183
161
|
/** Add a connected page, send it the current status, and return its detach. */
|
|
184
162
|
attach(client: LiveClient): () => void;
|
|
185
|
-
/** End every connected page, stop the ticker
|
|
163
|
+
/** End every connected page, and stop the ticker. */
|
|
186
164
|
close(): void;
|
|
187
165
|
/**
|
|
188
166
|
* Whether another page must be refused. The caller asks before it opens the
|
|
@@ -192,18 +170,18 @@ export interface LiveChannel {
|
|
|
192
170
|
}
|
|
193
171
|
|
|
194
172
|
/**
|
|
195
|
-
* Build a channel for one server. The ticker runs only while at least one page
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
173
|
+
* Build a channel for one server. The ticker runs only while at least one page is
|
|
174
|
+
* attached, so an idle server does no status work at all; the last page leaving
|
|
175
|
+
* parks it and the next page starts it again. What happens to a server nobody is
|
|
176
|
+
* reading — the idle grace and the close it leads to — is the server's policy, so
|
|
177
|
+
* the caller hands it to `lib/http`'s `serveLoopback` instead of configuring it
|
|
178
|
+
* here.
|
|
199
179
|
*/
|
|
200
180
|
export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
201
|
-
const timer = options.timer ??
|
|
202
|
-
const timeout = options.timeout ?? defaultTimeout;
|
|
181
|
+
const timer = options.timer ?? defaultInterval;
|
|
203
182
|
const maxClients = options.maxClients ?? LIVE_MAX_CLIENTS;
|
|
204
183
|
const clients = new Set<LiveClient>();
|
|
205
184
|
let cancel: (() => void) | undefined;
|
|
206
|
-
let cancelIdle: (() => void) | undefined;
|
|
207
185
|
let inFlight = false;
|
|
208
186
|
|
|
209
187
|
const stop = (): void => {
|
|
@@ -211,34 +189,9 @@ export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
|
211
189
|
cancel = undefined;
|
|
212
190
|
};
|
|
213
191
|
|
|
214
|
-
const cancelIdleStop = (): void => {
|
|
215
|
-
cancelIdle?.();
|
|
216
|
-
cancelIdle = undefined;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const armIdleStop = (): void => {
|
|
220
|
-
if (!options.onIdle || cancelIdle) return;
|
|
221
|
-
cancelIdle = timeout(() => {
|
|
222
|
-
cancelIdle = undefined;
|
|
223
|
-
options.onIdle?.();
|
|
224
|
-
}, options.idleMs);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
/** Nobody is reading a frame any more: park the ticker and start the grace. */
|
|
228
|
-
const pageLeft = (): void => {
|
|
229
|
-
stop();
|
|
230
|
-
armIdleStop();
|
|
231
|
-
};
|
|
232
|
-
|
|
233
192
|
const send = (frame: string): void => {
|
|
234
|
-
for (const client of clients)
|
|
235
|
-
|
|
236
|
-
client.write(frame);
|
|
237
|
-
} catch {
|
|
238
|
-
clients.delete(client);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (clients.size === 0) pageLeft();
|
|
193
|
+
for (const client of clients) client.write(frame);
|
|
194
|
+
if (clients.size === 0) stop();
|
|
242
195
|
};
|
|
243
196
|
|
|
244
197
|
// One snapshot serves every attached page, so a tick is one git read and one
|
|
@@ -255,17 +208,15 @@ export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
|
255
208
|
|
|
256
209
|
return {
|
|
257
210
|
attach(client) {
|
|
258
|
-
cancelIdleStop();
|
|
259
211
|
clients.add(client);
|
|
260
|
-
cancel ??= timer(() => void publish()
|
|
212
|
+
cancel ??= timer(options.intervalMs, () => void publish());
|
|
261
213
|
void publish();
|
|
262
214
|
return () => {
|
|
263
215
|
if (!clients.delete(client)) return;
|
|
264
|
-
if (clients.size === 0)
|
|
216
|
+
if (clients.size === 0) stop();
|
|
265
217
|
};
|
|
266
218
|
},
|
|
267
219
|
close() {
|
|
268
|
-
cancelIdleStop();
|
|
269
220
|
stop();
|
|
270
221
|
for (const client of clients) client.end();
|
|
271
222
|
clients.clear();
|
|
@@ -19,14 +19,14 @@ import {
|
|
|
19
19
|
parseRequestUrl,
|
|
20
20
|
scriptHeaders,
|
|
21
21
|
type ServeResponse,
|
|
22
|
-
} from "
|
|
23
|
-
import { THEME_JS, THEME_JS_PATH } from "
|
|
24
|
-
import { CLIENT_JS } from "
|
|
25
|
-
import type { LivePage } from "
|
|
26
|
-
import type { ServeConfig } from "
|
|
27
|
-
import { isDiffMode, type DiffMode } from "
|
|
28
|
-
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "
|
|
29
|
-
import { changeIndexOf, type LiveStatusSource } from "
|
|
22
|
+
} from "../../../lib/http/index.ts";
|
|
23
|
+
import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
|
|
24
|
+
import { CLIENT_JS } from "../view/client.ts";
|
|
25
|
+
import type { LivePage } from "../view/chrome.ts";
|
|
26
|
+
import type { ServeConfig } from "../config/config.ts";
|
|
27
|
+
import { isDiffMode, type DiffMode } from "../text/diff.ts";
|
|
28
|
+
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "../git/git.ts";
|
|
29
|
+
import { changeIndexOf, type LiveStatusSource } from "../live/live.ts";
|
|
30
30
|
import {
|
|
31
31
|
buildFileIndex,
|
|
32
32
|
buildTree,
|
|
@@ -38,13 +38,19 @@ import {
|
|
|
38
38
|
type DirEntry,
|
|
39
39
|
type FileIndexProvider,
|
|
40
40
|
type TreeNode,
|
|
41
|
-
} from "
|
|
42
|
-
import { FOLDER_ICON, iconHref, languageOf } from "
|
|
43
|
-
import { fuzzySearch } from "
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
} from "../files/files.ts";
|
|
42
|
+
import { FOLDER_ICON, iconHref, languageOf } from "../view/icons.ts";
|
|
43
|
+
import { fuzzySearch } from "../text/fuzzy.ts";
|
|
44
|
+
import {
|
|
45
|
+
renderDirectoryPage,
|
|
46
|
+
renderDiffPage,
|
|
47
|
+
renderErrorPage,
|
|
48
|
+
renderFileDiffPage,
|
|
49
|
+
renderFilePage,
|
|
50
|
+
} from "../view/html.ts";
|
|
51
|
+
import type { ChangeSummary } from "../view/html.ts";
|
|
52
|
+
import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "../files/paths.ts";
|
|
53
|
+
import { listingRevision } from "../live/rev.ts";
|
|
48
54
|
|
|
49
55
|
export interface ServeContext {
|
|
50
56
|
root: string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP server for the browse extension.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the pure `handleRequest` in `lib/http`'s loopback server: it resolves the
|
|
5
|
+
* root once, feeds the live status channel, and exposes an idempotent `close()`.
|
|
6
|
+
* The request handler is injectable so tests can avoid a socket when they only
|
|
7
|
+
* need routing.
|
|
8
|
+
*
|
|
9
|
+
* What stays here is this extension's own policy: the request handler, its
|
|
10
|
+
* request count, the catch that turns a thrown handler into a plain-text 500, the
|
|
11
|
+
* cap that refuses a page rather than opening a stream for it, and how long a
|
|
12
|
+
* server with nobody reading it is worth keeping.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { realpathSync } from "node:fs";
|
|
16
|
+
import { type ServeResponse, serveLoopback, textResponse } from "../../../lib/http/index.ts";
|
|
17
|
+
import { IDLE_STOP_MS, type ServeConfig } from "../config/config.ts";
|
|
18
|
+
import { createFileIndex } from "../files/files.ts";
|
|
19
|
+
import type { GitDiffProvider, GitStatusProvider } from "../git/git.ts";
|
|
20
|
+
import { createLiveChannel, createLiveStatus } from "../live/live.ts";
|
|
21
|
+
import { handleRequest, type ServeContext } from "./router.ts";
|
|
22
|
+
|
|
23
|
+
export interface FileServer {
|
|
24
|
+
/** Origin URL, e.g. `http://127.0.0.1:49152/`. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** The bound port. */
|
|
27
|
+
port: number;
|
|
28
|
+
/** Resolved absolute root that is actually served. */
|
|
29
|
+
root: string;
|
|
30
|
+
/** Stop listening. Safe to call more than once or before `listen` settles. */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FileServerOptions {
|
|
35
|
+
root: string;
|
|
36
|
+
config: ServeConfig;
|
|
37
|
+
/** Read-only git context for the page header; omitted when unavailable. */
|
|
38
|
+
git?: GitStatusProvider;
|
|
39
|
+
/** Read-only diff provider for the change and diff views; omitted when unavailable. */
|
|
40
|
+
diff?: GitDiffProvider;
|
|
41
|
+
/** Called once the last page has been gone for `idleStopMs`; omitted, the server waits for `close()`. */
|
|
42
|
+
onIdle?: () => void;
|
|
43
|
+
/** Grace before `onIdle`, in ms; defaults to `IDLE_STOP_MS`. */
|
|
44
|
+
idleStopMs?: number;
|
|
45
|
+
/** Override the request handler (tests). */
|
|
46
|
+
handler?: typeof handleRequest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Start a read-only server rooted at `options.root` on 127.0.0.1. */
|
|
50
|
+
export async function createFileServer(options: FileServerOptions): Promise<FileServer> {
|
|
51
|
+
const root = realpathSync.native(options.root);
|
|
52
|
+
const { config } = options;
|
|
53
|
+
// The status feed's two server-owned facts: how long it has been up, and how
|
|
54
|
+
// many requests it has answered. Both are read per frame, never cached.
|
|
55
|
+
let requests = 0;
|
|
56
|
+
const status = config.live
|
|
57
|
+
? createLiveStatus({
|
|
58
|
+
git: options.git,
|
|
59
|
+
diff: config.gitDiff ? options.diff : undefined,
|
|
60
|
+
startedAt: Date.now(),
|
|
61
|
+
requests: () => requests,
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
64
|
+
const live = status ? createLiveChannel({ status, intervalMs: config.liveIntervalMs }) : undefined;
|
|
65
|
+
const context: ServeContext = {
|
|
66
|
+
root,
|
|
67
|
+
config,
|
|
68
|
+
git: options.git,
|
|
69
|
+
diff: options.diff,
|
|
70
|
+
index: createFileIndex(root, config.maxIndexEntries),
|
|
71
|
+
live: status,
|
|
72
|
+
};
|
|
73
|
+
const handler = options.handler ?? handleRequest;
|
|
74
|
+
|
|
75
|
+
const server = await serveLoopback({
|
|
76
|
+
ports: [config.port],
|
|
77
|
+
idle: options.onIdle === undefined ? undefined : { ms: options.idleStopMs ?? IDLE_STOP_MS, onIdle: options.onIdle },
|
|
78
|
+
handler: async (request) => {
|
|
79
|
+
requests += 1;
|
|
80
|
+
let result: ServeResponse;
|
|
81
|
+
try {
|
|
82
|
+
result = await handler(context, request.method, request.url, request.headers);
|
|
83
|
+
} catch {
|
|
84
|
+
return { result: textResponse(500, "Internal error") };
|
|
85
|
+
}
|
|
86
|
+
if (!result.stream || !live || request.method.toUpperCase() === "HEAD") return { result };
|
|
87
|
+
// Checked before the head is written: a refusal must not leave a stream the
|
|
88
|
+
// client would wait on. `204` is the one status an event source does not
|
|
89
|
+
// reconnect after, so a page beyond the cap goes quiet instead of retrying.
|
|
90
|
+
if (live.atCapacity()) {
|
|
91
|
+
return { result: { status: 204, headers: { "Cache-Control": "no-store" }, body: "" } };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
result,
|
|
95
|
+
openSink: (sink) => live.attach({ write: (frame) => sink.write(frame), end: () => sink.end() }),
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
url: `${server.origin}/`,
|
|
102
|
+
port: server.port,
|
|
103
|
+
root,
|
|
104
|
+
close: async () => {
|
|
105
|
+
// End the streams first, so a connected page does not hold the close open.
|
|
106
|
+
live?.close();
|
|
107
|
+
await server.close();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* it reaches the page. The page shell and the page bodies stay in `html.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { escapeHtml } from "
|
|
12
|
-
import { GLYPHS } from "
|
|
13
|
-
import type { GitStatus } from "
|
|
14
|
-
import type { ChangeIndex } from "
|
|
11
|
+
import { escapeHtml } from "../../../lib/escape.ts";
|
|
12
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
|
+
import type { GitStatus } from "../git/git.ts";
|
|
14
|
+
import type { ChangeIndex } from "../live/live.ts";
|
|
15
15
|
|
|
16
16
|
/** The header's git chip: the branch, or the detached head; changes are the changes chip's job. */
|
|
17
17
|
export function renderGit(git: GitStatus | undefined): string {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* and `bun build --no-bundle` needs no separate asset.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { keysDialogScript, THEME_TOGGLE_JS } from "
|
|
15
|
+
import { keysDialogScript, THEME_TOGGLE_JS } from "../../../lib/web/index.ts";
|
|
16
16
|
import { LIVE_CLIENT_JS } from "./live-client.ts";
|
|
17
17
|
import { PALETTE_CLIENT_JS } from "./palette.ts";
|
|
18
18
|
import { TREE_CLIENT_JS } from "./tree.ts";
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* hunk and never across the unchanged lines a hunk does not show.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { escapeHtml } from "
|
|
12
|
-
import { highlightLines, type HighlightLanguage } from "
|
|
13
|
-
import type { DiffLine } from "
|
|
11
|
+
import { escapeHtml } from "../../../lib/escape.ts";
|
|
12
|
+
import { highlightLines, type HighlightLanguage } from "../../../lib/highlight.ts";
|
|
13
|
+
import type { DiffLine } from "../text/diff.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The inner HTML per diff line. Hunk bodies are highlighted by rebuilding the
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
* the page-only `browse-*` rules from `styles.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "
|
|
12
|
-
import { escapeHtml } from "
|
|
11
|
+
import { brandLockup, docHead, type KeyHint, keysSheet, themeToggle } from "../../../lib/web/index.ts";
|
|
12
|
+
import { escapeHtml } from "../../../lib/escape.ts";
|
|
13
13
|
import { renderChanges, renderGit, renderLiveChrome, renderLiveStatus, type LivePage } from "./chrome.ts";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { type DirEntry, type FileView, type Listing, type TreeNode } from "../files/files.ts";
|
|
15
|
+
import { formatSize } from "../../../lib/format.ts";
|
|
16
|
+
import { DIFF_MODES, statusLetter, type ChangedFile, type DiffMode } from "../text/diff.ts";
|
|
16
17
|
import { renderDiffLines } from "./diff-html.ts";
|
|
17
|
-
import type { DiffFileView, GitStatus } from "
|
|
18
|
-
import type { ChangeIndex } from "
|
|
19
|
-
import { detectLanguage, highlightLines, type HighlightLanguage } from "
|
|
18
|
+
import type { DiffFileView, GitStatus } from "../git/git.ts";
|
|
19
|
+
import type { ChangeIndex } from "../live/live.ts";
|
|
20
|
+
import { detectLanguage, highlightLines, type HighlightLanguage } from "../../../lib/highlight.ts";
|
|
20
21
|
import { FOLDER_ICON, iconHref, languageOf, SPRITE, type IconSpec } from "./icons.ts";
|
|
21
22
|
import { renderMarkdown } from "./markdown.ts";
|
|
22
|
-
import { encodePath } from "
|
|
23
|
+
import { encodePath } from "../files/paths.ts";
|
|
23
24
|
import { BROWSE_CSS } from "./styles.ts";
|
|
24
25
|
|
|
25
26
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
@@ -286,8 +287,9 @@ function numberedLines(page: FilePage): boolean {
|
|
|
286
287
|
return page.file.kind === "text" || (page.file.kind === "markdown" && page.markdownRaw === true);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
/** The card-head controls: Markdown mode, line wrapping, and copy actions
|
|
290
|
-
|
|
290
|
+
/** The card-head controls: Markdown mode, line wrapping, and the copy actions,
|
|
291
|
+
* plus the download the diff head keeps in the same group. */
|
|
292
|
+
function renderFileActions(page: FilePage, download: string): string {
|
|
291
293
|
const isMarkdown = page.file.kind === "markdown";
|
|
292
294
|
const rawHref = `/raw/${encodePath(page.rel)}`;
|
|
293
295
|
const viewHref = `/view/${encodePath(page.rel)}`;
|
|
@@ -324,7 +326,7 @@ function renderFileActions(page: FilePage): string {
|
|
|
324
326
|
`<button type="button" class="bf-btn" id="copy-reference" disabled title="Select lines in the file first">copy reference</button>`,
|
|
325
327
|
);
|
|
326
328
|
}
|
|
327
|
-
return `<span class="bf-card__actions">${groups.join("")}</span>`;
|
|
329
|
+
return `<span class="bf-card__actions">${groups.join("")}${download}</span>`;
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
/** Render a file page (text, Markdown, image, binary, or over-size). */
|
|
@@ -336,7 +338,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
336
338
|
// about the file; the language label, size, and date are the useful metadata.
|
|
337
339
|
const meta = `${spec.label} · ${formatSize(page.file.size)} · ${formatDate(page.file.mtimeMs)}`;
|
|
338
340
|
const download = `<a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a>`;
|
|
339
|
-
const head = `<div class="bf-card__head">${iconMarkup(spec)}<h1 class="bf-card__title">${escapeHtml(name)}</h1><span class="bf-card__meta">${escapeHtml(meta)}</span><span class="bf-spacer"></span>${renderFileActions(page)}
|
|
341
|
+
const head = `<div class="bf-card__head">${iconMarkup(spec)}<h1 class="bf-card__title">${escapeHtml(name)}</h1><span class="bf-card__meta">${escapeHtml(meta)}</span><span class="bf-spacer"></span>${renderFileActions(page, download)}</div>`;
|
|
340
342
|
|
|
341
343
|
const isMarkdown = page.file.kind === "markdown";
|
|
342
344
|
const rendered = isMarkdown && !page.markdownRaw;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { extname } from "node:path";
|
|
8
|
-
import { IMAGE_EXTENSIONS } from "
|
|
8
|
+
import { IMAGE_EXTENSIONS } from "../files/files.ts";
|
|
9
9
|
|
|
10
10
|
export type IconName = "folder" | "file-code" | "file-text" | "file-image" | "file-json" | "file-archive" | "file";
|
|
11
11
|
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
* policy and raw HTML do not.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { decodeCodePoint, escapeHtml } from "
|
|
29
|
-
import { fenceLanguage, highlightLines } from "
|
|
28
|
+
import { decodeCodePoint, escapeHtml } from "../../../lib/escape.ts";
|
|
29
|
+
import { fenceLanguage, highlightLines } from "../../../lib/highlight.ts";
|
|
30
30
|
import {
|
|
31
31
|
closesSuppressed,
|
|
32
32
|
closeHtmlSink,
|
|
@@ -126,8 +126,8 @@ function createContext(baseDir: string, defs: Map<string, LinkDefinition>, html:
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/** Fold a reference label the way CommonMark does: trim, collapse, case-fold. */
|
|
130
|
-
function
|
|
129
|
+
/** Fold a reference label the way CommonMark does: trim, collapse, case-fold. Not `lib/session-label.ts`, which names a session. */
|
|
130
|
+
function referenceLabel(label: string): string {
|
|
131
131
|
return label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -180,7 +180,7 @@ function collectDefinitions(lines: string[], html: boolean): { defs: Map<string,
|
|
|
180
180
|
body.push(line);
|
|
181
181
|
continue;
|
|
182
182
|
}
|
|
183
|
-
const key =
|
|
183
|
+
const key = referenceLabel(match[1]);
|
|
184
184
|
if (!defs.has(key)) {
|
|
185
185
|
const dest = match[2].startsWith("<") && match[2].endsWith(">") ? match[2].slice(1, -1) : match[2];
|
|
186
186
|
defs.set(key, { dest, title: match[3] });
|
|
@@ -630,11 +630,11 @@ function parseReferenceLink(text: string, start: number, ctx: Context): ParsedLi
|
|
|
630
630
|
const end = matchBracket(text, close + 1);
|
|
631
631
|
if (end !== -1) {
|
|
632
632
|
const raw = text.slice(close + 2, end);
|
|
633
|
-
const def = ctx.defs.get(
|
|
633
|
+
const def = ctx.defs.get(referenceLabel(raw === "" ? label : raw));
|
|
634
634
|
if (def) return { label, dest: def.dest, title: def.title, end: end + 1 };
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
|
-
const def = ctx.defs.get(
|
|
637
|
+
const def = ctx.defs.get(referenceLabel(label));
|
|
638
638
|
if (def) return { label, dest: def.dest, title: def.title, end: close + 1 };
|
|
639
639
|
return undefined;
|
|
640
640
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* how `<details>` wraps Markdown.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { decodeCodePoint, escapeHtml } from "
|
|
28
|
+
import { decodeCodePoint, escapeHtml } from "../../../lib/escape.ts";
|
|
29
29
|
import { resolveImage, resolveLink } from "./urls.ts";
|
|
30
30
|
|
|
31
31
|
/** Elements re-emitted with sanitized attributes. */
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
WEB_COMPONENTS_CSS,
|
|
13
13
|
WEB_LAYOUT_CSS,
|
|
14
14
|
WEB_THEME_CSS,
|
|
15
|
-
} from "
|
|
15
|
+
} from "../../../lib/web/index.ts";
|
|
16
16
|
|
|
17
17
|
/** The shared shell, components, and the page-only `browse-*` rules. */
|
|
18
18
|
export const BROWSE_CSS = `${WEB_BASE_CSS}
|
|
@@ -37,7 +37,6 @@ ${WEB_COMPONENTS_CSS}
|
|
|
37
37
|
.browse-dotfile .bf-table__name a{opacity:.72}
|
|
38
38
|
.browse-broken{color:var(--muted);text-decoration:line-through}
|
|
39
39
|
.browse-link-mark{color:var(--muted);font-size:var(--fs-xs)}
|
|
40
|
-
.browse-when{display:none}
|
|
41
40
|
body.palette-open{overflow:hidden}
|
|
42
41
|
.browse-diff{font-family:var(--font-mono);gap:5px}
|
|
43
42
|
.browse-diff:hover{color:var(--accent);text-decoration:none}
|
|
@@ -93,8 +92,24 @@ body.palette-open{overflow:hidden}
|
|
|
93
92
|
.browse-measure .bf-md>*{max-width:var(--browse-measure);margin-inline:auto}
|
|
94
93
|
/* Tables start at the prose column and take only the gutter to its right, so a
|
|
95
94
|
wide window widens them without a dead band on their left. Code stays inside
|
|
96
|
-
the measure with the rest of the prose.
|
|
97
|
-
|
|
95
|
+
the measure with the rest of the prose. The frame is also the container a
|
|
96
|
+
squeezed table asks about: it is the pane, not the window, that narrows one. */
|
|
97
|
+
.browse-measure .bf-md>.md-table-frame{max-width:none;padding-left:var(--browse-gutter);container-type:inline-size}
|
|
98
|
+
/* A table spans the reading column rather than stopping at its own content: a
|
|
99
|
+
shrink-wrapped table leaves a ragged band beside itself — 254px on a 2560px
|
|
100
|
+
screen, and 300px beside the three-column tables at 1440 — which the reader
|
|
101
|
+
cannot tell from a column that failed to draw. The card is wider than the text
|
|
102
|
+
it holds, so the measure is the ceiling: a table ends where the prose does, and
|
|
103
|
+
below the floor's threshold the minimum still wins and the wrapper scrolls. */
|
|
104
|
+
.browse-measure .bf-md .md-table-frame table{width:100%;max-width:var(--browse-measure)}
|
|
105
|
+
/* In a pane this narrow, auto layout gives the prose cell whatever an unbreakable
|
|
106
|
+
code span leaves — one word per line, and a four-column table read as a
|
|
107
|
+
300px-tall row. A floor on the cells makes the table wider than the pane
|
|
108
|
+
instead, so every column keeps a readable width and the wrapper scrolls them;
|
|
109
|
+
a table that still fits is left as it was. */
|
|
110
|
+
@container (max-width:600px){
|
|
111
|
+
.browse-measure .bf-md .md-table-frame td{min-width:14em}
|
|
112
|
+
}
|
|
98
113
|
/* A table wider than the pane scrolls, but an overlay scrollbar is invisible
|
|
99
114
|
until it moves; a gradient on the frame marks the columns still out of view.
|
|
100
115
|
It has to be the frame: the scroller clips its own content, so a pseudo-element
|
|
@@ -116,10 +131,19 @@ body.palette-open{overflow:hidden}
|
|
|
116
131
|
.browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
|
|
117
132
|
}
|
|
118
133
|
@media (max-width:820px){
|
|
119
|
-
.browse-root{order:9;flex:1 1 100
|
|
134
|
+
.browse-root{order:9;flex:1 1 100%}
|
|
120
135
|
}
|
|
121
136
|
@media (max-width:520px){
|
|
122
137
|
.bf-toolbar .bf-theme-toggle{margin-left:auto}
|
|
138
|
+
/* A phone has no room for four button rows above the file: the actions keep one
|
|
139
|
+
line and scroll instead, so the document starts where it should. A control
|
|
140
|
+
keeps its own width — shrunk, a label wraps and a segmented control clips. */
|
|
141
|
+
.bf-card__actions{max-width:100%;flex-wrap:nowrap;overflow-x:auto}
|
|
142
|
+
.bf-card__actions>*{flex:0 0 auto}
|
|
143
|
+
/* A trailing date or one-word change state costs the column a reader is choosing
|
|
144
|
+
between: at 390px the listing gave the name 106px and every entry truncated,
|
|
145
|
+
and the diff gave a path 140px while "modified" held 130. */
|
|
146
|
+
.browse-listing .bf-table__when,.browse-diff .browse-diff-word{display:none}
|
|
123
147
|
}
|
|
124
148
|
/* A printed page scrolls nothing: the card and its body open up, the pinned hunk
|
|
125
149
|
header stops pinning, and the progress hairline belongs to the screen. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* intent, and the one row the keyboard is on.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { SIDEBAR_COLLAPSE_MAX } from "
|
|
11
|
+
import { SIDEBAR_COLLAPSE_MAX } from "../../../lib/web/index.ts";
|
|
12
12
|
|
|
13
13
|
export const TREE_CLIENT_JS = ` var tree = document.querySelector("nav.bf-tree");
|
|
14
14
|
var filter = document.getElementById("filter");
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type, type Static } from "typebox";
|
|
6
|
+
import { READ_DOC_TOOL } from "../../lib/tool-names.ts";
|
|
6
7
|
import type { FormatId } from "./formats.ts";
|
|
7
8
|
|
|
8
9
|
/** The tool's registered name, shared by the definition and its renderer. */
|
|
9
|
-
export const TOOL_NAME =
|
|
10
|
+
export const TOOL_NAME = READ_DOC_TOOL;
|
|
10
11
|
|
|
11
12
|
export const MIN_CHARS = 200;
|
|
12
13
|
export const MAX_CHARS = 100_000;
|
|
@@ -74,17 +74,24 @@ export function readMemoryFile(path: string): string {
|
|
|
74
74
|
return readFileSync(path, "utf-8");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Identity of a store file, for cheap change detection.
|
|
79
|
+
*
|
|
80
|
+
* The stamp is nanoseconds, not milliseconds: an edit that keeps the file's size
|
|
81
|
+
* is otherwise invisible for a whole millisecond, which is long enough for an
|
|
82
|
+
* editor that writes in place. `size` stays a number so the doctor's byte counts
|
|
83
|
+
* read the same.
|
|
84
|
+
*/
|
|
78
85
|
export interface MemoryFileStamp {
|
|
79
|
-
|
|
86
|
+
mtimeNs: bigint;
|
|
80
87
|
size: number;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
/** Freshness stamp for a store file; `undefined` when it is missing or unstattable. */
|
|
84
91
|
export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
|
|
85
92
|
try {
|
|
86
|
-
const info = statSync(path);
|
|
87
|
-
return {
|
|
93
|
+
const info = statSync(path, { bigint: true });
|
|
94
|
+
return { mtimeNs: info.mtimeNs, size: Number(info.size) };
|
|
88
95
|
} catch {
|
|
89
96
|
return undefined;
|
|
90
97
|
}
|
|
@@ -93,7 +100,7 @@ export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
|
|
|
93
100
|
/** Whether two stamps describe the same file state. */
|
|
94
101
|
export function sameStamp(a: MemoryFileStamp | undefined, b: MemoryFileStamp | undefined): boolean {
|
|
95
102
|
if (a === undefined || b === undefined) return a === b;
|
|
96
|
-
return a.
|
|
103
|
+
return a.mtimeNs === b.mtimeNs && a.size === b.size;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/** Note text of every bullet line, sanitized to one terminal-safe line. */
|