@gavin-hu/my-pi-agent 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -12
- package/README.zh-CN.md +15 -9
- package/docs/extensions/README.md +2 -1
- package/docs/extensions/board.md +329 -151
- package/docs/extensions/bytefly.md +427 -273
- package/docs/extensions/file-browse.md +46 -44
- package/docs/extensions/job.md +1 -2
- package/docs/extensions/plan.md +86 -24
- package/docs/extensions/serve.md +129 -0
- package/docs/extensions/session-bus.md +23 -10
- package/docs/extensions/team.md +56 -28
- package/docs/extensions/wechat.md +2 -1
- package/docs/extensions/worktree.md +30 -15
- package/extensions/board/commands.ts +48 -17
- package/extensions/board/format.ts +107 -9
- package/extensions/board/index.ts +14 -2
- package/extensions/board/join.ts +31 -10
- package/extensions/board/page.ts +202 -156
- package/extensions/board/result.ts +10 -24
- package/extensions/board/rows.ts +1 -1
- package/extensions/board/schema.ts +31 -10
- package/extensions/board/seat.ts +9 -10
- package/extensions/board/snapshot.ts +7 -5
- package/extensions/board/tools.ts +211 -34
- package/extensions/board/transport/router.ts +27 -110
- package/extensions/board/transport/server.ts +76 -56
- package/extensions/board/types.ts +57 -0
- package/extensions/board/view/client.ts +24 -13
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +28 -24
- package/extensions/board/view/live.ts +6 -178
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +60 -89
- package/extensions/board/view/render.ts +228 -200
- package/extensions/board/view/styles.ts +72 -49
- package/extensions/board/view/team.ts +63 -41
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -11
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +93 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/panel/commands.ts +28 -1
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings.ts +72 -23
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/check/commands.ts +2 -1
- package/extensions/check/render.ts +10 -1
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +8 -547
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/index.ts +27 -48
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -6
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/view/html.ts +78 -46
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +23 -26
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +27 -22
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/runtime.ts +4 -4
- package/extensions/job/tools.ts +19 -7
- package/extensions/job/tui.ts +2 -28
- package/extensions/plan/commands.ts +2 -2
- package/extensions/plan/feedback.ts +88 -18
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +24 -2
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +19 -23
- package/extensions/plan/policy.ts +10 -5
- package/extensions/plan/runtime.ts +43 -4
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +10 -1
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tools.ts +2 -2
- package/extensions/plan/types.ts +15 -0
- package/extensions/plan/web-blocks.ts +160 -0
- package/extensions/plan/web-chrome.ts +7 -5
- package/extensions/plan/web-client.ts +271 -325
- package/extensions/plan/web-collab.ts +7 -7
- package/extensions/plan/web-comments.ts +57 -16
- package/extensions/plan/web-composer.ts +91 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +258 -0
- package/extensions/plan/web-markdown.ts +1 -1
- package/extensions/plan/web-outline.ts +212 -0
- package/extensions/plan/web-page.ts +59 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +160 -0
- package/extensions/plan/web-router.ts +0 -1
- package/extensions/plan/web-settle.ts +201 -0
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +77 -130
- package/extensions/rewind/commands/review.ts +2 -1
- package/extensions/serve/config.ts +37 -0
- package/extensions/serve/index.ts +71 -0
- package/extensions/serve/registry.ts +251 -0
- package/extensions/session-bus/join-request.ts +16 -9
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/rows.ts +7 -3
- package/extensions/session-bus/runtime.ts +100 -38
- package/extensions/session-bus/store.ts +14 -2
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/tui.ts +1 -1
- package/extensions/session-bus/types.ts +10 -1
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/team/commands.ts +44 -9
- package/extensions/team/format.ts +14 -0
- package/extensions/team/index.ts +2 -1
- package/extensions/team/tools.ts +30 -9
- package/extensions/team/types.ts +17 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/todo/tui.ts +21 -3
- package/extensions/wechat/bridge.ts +43 -172
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +15 -285
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +9 -7
- package/lib/board/card.ts +192 -3
- package/lib/board/links.ts +26 -2
- package/lib/board/page-pointer.ts +111 -32
- package/lib/board/round-text.ts +11 -2
- package/lib/board/round.ts +25 -20
- package/lib/board/rules.ts +144 -11
- package/lib/board/session.ts +84 -18
- package/lib/board/store.ts +268 -55
- package/lib/board/view.ts +114 -24
- package/lib/bus-request.ts +83 -0
- package/lib/format.ts +22 -0
- package/lib/http/index.ts +11 -7
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +25 -0
- package/lib/http/serve.ts +170 -43
- package/lib/http/server.ts +3 -3
- package/lib/http/sink.ts +34 -9
- package/lib/plan-entry.ts +20 -13
- package/lib/policy.ts +14 -1
- package/lib/requirements.ts +2 -2
- package/lib/session-label.ts +23 -7
- package/lib/session-roster.ts +74 -3
- package/lib/session-tool.ts +37 -21
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +13 -0
- package/lib/tui/README.md +6 -3
- package/lib/tui/glyphs.ts +3 -6
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tool.ts +30 -1
- package/lib/web/README.md +52 -2
- package/lib/web/client-runtime.ts +149 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +106 -2
- package/lib/web/names.ts +42 -0
- package/package.json +3 -2
- package/extensions/board/config.ts +0 -36
- package/extensions/board/view/actions.ts +0 -306
- package/extensions/board/writes.ts +0 -99
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/job/process.ts +0 -35
|
@@ -1,44 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The browse page's mount: the prefix it holds on the session's one server, plus the
|
|
3
|
+
* live status channel it feeds.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `lib/http` owns the socket and `serve` owns the session's port, so this module owns
|
|
6
|
+
* only this page's own policy: the request handler, its request count, the cap that
|
|
7
|
+
* refuses a page rather than opening a stream for it, and how long a page with nobody
|
|
8
|
+
* reading it is worth keeping.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The page takes the namespace `/files`, beside `/board` and `/plan/<id>`: every URL it
|
|
11
|
+
* emits is built from that `base` — the links, the scripts, the destinations it rewrites
|
|
12
|
+
* out of Markdown, and the client's own fetches — so one prefix is all it needs, and the
|
|
13
|
+
* origin's root stays free. `BROWSE_BASE` is that claim, named where the mount and its
|
|
14
|
+
* suite can see it.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { realpathSync } from "node:fs";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type HttpMount,
|
|
20
|
+
type MountOutcome,
|
|
21
|
+
type MountSource,
|
|
22
|
+
type ServeResponse,
|
|
23
|
+
textResponse,
|
|
24
|
+
} from "../../../lib/http/index.ts";
|
|
17
25
|
import { IDLE_STOP_MS, type ServeConfig } from "../config/config.ts";
|
|
18
26
|
import { createFileIndex } from "../files/files.ts";
|
|
19
27
|
import type { GitDiffProvider, GitStatusProvider } from "../git/git.ts";
|
|
20
28
|
import { createLiveChannel, createLiveStatus } from "../live/live.ts";
|
|
21
29
|
import { handleRequest, type ServeContext } from "./router.ts";
|
|
22
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The prefix this page answers under: its own namespace on the session's origin.
|
|
33
|
+
*
|
|
34
|
+
* Route names inside the page (`/`, `/browse`, `/view`, `/raw`, `/diff`, `/api/*`,
|
|
35
|
+
* `/events`) are unchanged and relative to this prefix, so the same page is served under
|
|
36
|
+
* `/files` and nowhere else, and the origin's root answers nothing at all.
|
|
37
|
+
*/
|
|
38
|
+
export const BROWSE_BASE = "/files";
|
|
39
|
+
|
|
40
|
+
/** A mounted page. */
|
|
23
41
|
export interface FileServer {
|
|
24
|
-
/**
|
|
42
|
+
/** The page's URL, e.g. `http://127.0.0.1:4780/files/`. */
|
|
25
43
|
url: string;
|
|
26
|
-
/** The bound
|
|
44
|
+
/** The port the session's server bound. */
|
|
27
45
|
port: number;
|
|
28
46
|
/** Resolved absolute root that is actually served. */
|
|
29
47
|
root: string;
|
|
30
|
-
/**
|
|
48
|
+
/** Give the prefix back. Safe to call more than once. */
|
|
31
49
|
close(): Promise<void>;
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
/** The page, or the reason the session's server has no place for it. */
|
|
53
|
+
export type FileServerResult = { kind: "served"; server: FileServer } | { kind: "refused"; reason: string };
|
|
54
|
+
|
|
34
55
|
export interface FileServerOptions {
|
|
35
56
|
root: string;
|
|
36
57
|
config: ServeConfig;
|
|
58
|
+
/** Where the prefix comes from: the session's server, or a suite's own. */
|
|
59
|
+
mount: MountSource;
|
|
60
|
+
/** The Pi session asking, so the prefix lands on this session's socket. */
|
|
61
|
+
session: string;
|
|
37
62
|
/** Read-only git context for the page header; omitted when unavailable. */
|
|
38
63
|
git?: GitStatusProvider;
|
|
39
64
|
/** Read-only diff provider for the change and diff views; omitted when unavailable. */
|
|
40
65
|
diff?: GitDiffProvider;
|
|
41
|
-
/** Called once the last page has been gone for `idleStopMs`; omitted, the
|
|
66
|
+
/** Called once the last page has been gone for `idleStopMs`; omitted, the mount waits for `close()`. */
|
|
42
67
|
onIdle?: () => void;
|
|
43
68
|
/** Grace before `onIdle`, in ms; defaults to `IDLE_STOP_MS`. */
|
|
44
69
|
idleStopMs?: number;
|
|
@@ -46,8 +71,8 @@ export interface FileServerOptions {
|
|
|
46
71
|
handler?: typeof handleRequest;
|
|
47
72
|
}
|
|
48
73
|
|
|
49
|
-
/**
|
|
50
|
-
export async function createFileServer(options: FileServerOptions): Promise<
|
|
74
|
+
/** Mount a read-only page rooted at `options.root` on the session's server. */
|
|
75
|
+
export async function createFileServer(options: FileServerOptions): Promise<FileServerResult> {
|
|
51
76
|
const root = realpathSync.native(options.root);
|
|
52
77
|
const { config } = options;
|
|
53
78
|
// The status feed's two server-owned facts: how long it has been up, and how
|
|
@@ -64,6 +89,7 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
64
89
|
const live = status ? createLiveChannel({ status, intervalMs: config.liveIntervalMs }) : undefined;
|
|
65
90
|
const context: ServeContext = {
|
|
66
91
|
root,
|
|
92
|
+
base: BROWSE_BASE,
|
|
67
93
|
config,
|
|
68
94
|
git: options.git,
|
|
69
95
|
diff: options.diff,
|
|
@@ -72,10 +98,13 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
72
98
|
};
|
|
73
99
|
const handler = options.handler ?? handleRequest;
|
|
74
100
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
const outcome: MountOutcome = await options.mount({
|
|
102
|
+
session: options.session,
|
|
103
|
+
prefix: BROWSE_BASE,
|
|
104
|
+
...(options.onIdle === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { idle: { ms: options.idleStopMs ?? IDLE_STOP_MS, onIdle: options.onIdle } }),
|
|
107
|
+
async handler(request) {
|
|
79
108
|
requests += 1;
|
|
80
109
|
let result: ServeResponse;
|
|
81
110
|
try {
|
|
@@ -96,15 +125,19 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
96
125
|
};
|
|
97
126
|
},
|
|
98
127
|
});
|
|
99
|
-
|
|
128
|
+
if (outcome.kind === "refused") return { kind: "refused", reason: outcome.reason };
|
|
129
|
+
const mount: HttpMount = outcome.mount;
|
|
100
130
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
kind: "served",
|
|
132
|
+
server: {
|
|
133
|
+
url: `${mount.origin}${mount.base}/`,
|
|
134
|
+
port: mount.port,
|
|
135
|
+
root,
|
|
136
|
+
close: async () => {
|
|
137
|
+
// End the streams first, so a connected page does not hold the mount open.
|
|
138
|
+
live?.close();
|
|
139
|
+
await mount.release();
|
|
140
|
+
},
|
|
108
141
|
},
|
|
109
142
|
};
|
|
110
143
|
}
|
|
@@ -46,7 +46,7 @@ export function renderLiveChrome(live: LivePage | undefined): string {
|
|
|
46
46
|
if (!live) return "";
|
|
47
47
|
const rev =
|
|
48
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"
|
|
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">◉</span><span class="browse-live__text">connecting</span></span>`;
|
|
50
50
|
if (live.rev === undefined) return chip;
|
|
51
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
52
|
}
|
|
@@ -64,11 +64,11 @@ export function renderLiveStatus(): string {
|
|
|
64
64
|
* a hidden shell is always rendered so the chip can appear and disappear as the
|
|
65
65
|
* working tree changes, without the client inserting elements into the header.
|
|
66
66
|
*/
|
|
67
|
-
export function renderChanges(summary: ChangeIndex | undefined, live: LivePage | undefined): string {
|
|
67
|
+
export function renderChanges(summary: ChangeIndex | undefined, live: LivePage | undefined, base: string): string {
|
|
68
68
|
const count = summary?.count ?? 0;
|
|
69
69
|
if (count <= 0) {
|
|
70
70
|
return live
|
|
71
|
-
? `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" hidden></a>`
|
|
71
|
+
? `<a class="bf-chip browse-diff" id="browse-changes" href="${base}/diff" title="View uncommitted changes" hidden></a>`
|
|
72
72
|
: "";
|
|
73
73
|
}
|
|
74
74
|
const label = `${count} change${count === 1 ? "" : "s"}`;
|
|
@@ -79,5 +79,5 @@ export function renderChanges(summary: ChangeIndex | undefined, live: LivePage |
|
|
|
79
79
|
? ` <span class="browse-diff-totals"><span class="browse-diff-add">+${insertions}</span> <span class="browse-diff-del">−${deletions}</span></span>`
|
|
80
80
|
: "";
|
|
81
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>`;
|
|
82
|
+
return `<a class="bf-chip browse-diff" id="browse-changes" href="${base}/diff" title="View uncommitted changes" aria-label="${escapeHtml(aria)}"><span class="browse-diff__dot" aria-hidden="true">●</span>${escapeHtml(label)}${totals}</a>`;
|
|
83
83
|
}
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
* and `bun build --no-bundle` needs no separate asset.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { keysDialogScript, THEME_TOGGLE_JS } from "../../../lib/web/index.ts";
|
|
15
|
+
import { CLIENT_BASE_JS, 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";
|
|
19
19
|
|
|
20
20
|
export const CLIENT_JS = `(function () {
|
|
21
21
|
"use strict";
|
|
22
|
+
/* Spliced at the outermost scope: every fragment below builds its URLs from it. */
|
|
23
|
+
${CLIENT_BASE_JS}
|
|
22
24
|
var rows = Array.prototype.slice.call(document.querySelectorAll("table.bf-table tbody tr"));
|
|
23
25
|
${TREE_CLIENT_JS}
|
|
24
26
|
Array.prototype.forEach.call(document.querySelectorAll("img.bf-table__thumb"), function (img) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The elements a page emits for a resolved Markdown link or image.
|
|
3
|
+
*
|
|
4
|
+
* Two renderers reach a link: the Markdown renderer (`markdown.ts`) and the raw-HTML
|
|
5
|
+
* sanitizer (`raw-html.ts`). Both ask `urls.ts` what a destination resolves to, and both
|
|
6
|
+
* then build the same anchor and the same image — the external-rel attributes, the
|
|
7
|
+
* `md-img` class, and the lazy decoding — so those live here rather than in a fourth and
|
|
8
|
+
* fifth copy. Values are escaped here; the callers hand in what the document said.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { escapeHtml } from "../../../lib/escape.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The attributes an external link carries, for a caller that is building the element
|
|
15
|
+
* itself: the raw-HTML sanitizer rewrites an existing tag's attribute list and has no
|
|
16
|
+
* content string to hand an emitter.
|
|
17
|
+
*
|
|
18
|
+
* `noopener` because the target is another origin, and `noreferrer` because the page is a
|
|
19
|
+
* local view of the reader's own repository and should not tell anyone where they read it.
|
|
20
|
+
*/
|
|
21
|
+
export const EXTERNAL_ATTRS = ' target="_blank" rel="noopener noreferrer"';
|
|
22
|
+
|
|
23
|
+
/** An `<a>` for one destination; `title` and the rel attributes are omitted when absent. */
|
|
24
|
+
export function emitAnchor(input: { href: string; content: string; title?: string; external: boolean }): string {
|
|
25
|
+
const title = input.title === undefined ? "" : ` title="${escapeHtml(input.title)}"`;
|
|
26
|
+
return `<a href="${escapeHtml(input.href)}"${title}${input.external ? EXTERNAL_ATTRS : ""}>${input.content}</a>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A link standing in for an image the page may not show: an external `src` is blocked by
|
|
31
|
+
* the CSP, so the reader gets something to click, labelled by the alt text.
|
|
32
|
+
*/
|
|
33
|
+
export function emitImageLink(href: string, label: string): string {
|
|
34
|
+
return `<a href="${escapeHtml(href)}"${EXTERNAL_ATTRS}>${escapeHtml(label)}</a>`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** An `<img>`: block-level spacing class, alt text, and the sizes a document may pin. */
|
|
38
|
+
export function emitImage(input: {
|
|
39
|
+
href: string;
|
|
40
|
+
alt: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
/** A document may pin a pixel size; escaped like every other value the page emits. */
|
|
43
|
+
width?: string | number;
|
|
44
|
+
height?: string | number;
|
|
45
|
+
}): string {
|
|
46
|
+
const size = `${input.width === undefined ? "" : ` width="${escapeHtml(String(input.width))}"`}${
|
|
47
|
+
input.height === undefined ? "" : ` height="${escapeHtml(String(input.height))}"`
|
|
48
|
+
}`;
|
|
49
|
+
const title = input.title === undefined ? "" : ` title="${escapeHtml(input.title)}"`;
|
|
50
|
+
return `<img class="md-img" src="${escapeHtml(input.href)}" alt="${escapeHtml(input.alt)}"${size}${title} loading="lazy" decoding="async">`;
|
|
51
|
+
}
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* the page-only `browse-*` rules from `styles.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
baseAttribute,
|
|
13
|
+
brandLockup,
|
|
14
|
+
docHead,
|
|
15
|
+
type KeyHint,
|
|
16
|
+
keysSheet,
|
|
17
|
+
tabTitle,
|
|
18
|
+
themeToggle,
|
|
19
|
+
} from "../../../lib/web/index.ts";
|
|
12
20
|
import { escapeHtml } from "../../../lib/escape.ts";
|
|
13
21
|
import { renderChanges, renderGit, renderLiveChrome, renderLiveStatus, type LivePage } from "./chrome.ts";
|
|
14
22
|
import { type DirEntry, type FileView, type Listing, type TreeNode } from "../files/files.ts";
|
|
@@ -58,11 +66,19 @@ function changeMark(summary: ChangeSummary | undefined, rel: string, isDir: bool
|
|
|
58
66
|
: "";
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The URL one tree or table entry links to, written under the page's `base`.
|
|
71
|
+
*
|
|
72
|
+
* Exported because the JSON `/api/tree` route answers with the same hrefs, so the rendered
|
|
73
|
+
* tree and the client's refresh cannot disagree about where an entry lives.
|
|
74
|
+
*/
|
|
75
|
+
export function hrefFor(entry: { rel: string; isDir: boolean }, base: string): string {
|
|
76
|
+
return entry.isDir ? `${base}/browse/${encodePath(entry.rel)}` : `${base}/view/${encodePath(entry.rel)}`;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
interface Layout {
|
|
80
|
+
/** The mount prefix this page answers under, e.g. `/files`; `""` for the origin's root. */
|
|
81
|
+
base: string;
|
|
66
82
|
title: string;
|
|
67
83
|
rootLabel: string;
|
|
68
84
|
tree: TreeNode;
|
|
@@ -84,10 +100,9 @@ interface Layout {
|
|
|
84
100
|
/** A full two-pane page. */
|
|
85
101
|
function renderLayout(layout: Layout): string {
|
|
86
102
|
return `<!doctype html>
|
|
87
|
-
<html lang="en">
|
|
103
|
+
<html lang="en"${baseAttribute(layout.base)}>
|
|
88
104
|
<head>
|
|
89
|
-
${docHead({ title: layout.title, css: BROWSE_CSS })}
|
|
90
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%2358a6ff'/%3E%3C/svg%3E">
|
|
105
|
+
${docHead({ title: layout.title, css: BROWSE_CSS, base: layout.base })}
|
|
91
106
|
</head>
|
|
92
107
|
<body class="bf-app">
|
|
93
108
|
<a class="skip" href="#main">Skip to content</a>
|
|
@@ -98,7 +113,7 @@ ${SPRITE}
|
|
|
98
113
|
${brandLockup("browse")}
|
|
99
114
|
<span class="bf-context browse-root">${escapeHtml(layout.rootLabel)}</span>
|
|
100
115
|
${renderGit(layout.git)}
|
|
101
|
-
${renderChanges(layout.summary, layout.live)}
|
|
116
|
+
${renderChanges(layout.summary, layout.live, layout.base)}
|
|
102
117
|
<span class="bf-chip bf-chip--accent">read-only</span>
|
|
103
118
|
${renderLiveChrome(layout.live)}
|
|
104
119
|
<button type="button" class="bf-btn bf-btn--icon browse-keys-btn" id="keys-open" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>
|
|
@@ -113,7 +128,7 @@ ${SPRITE}
|
|
|
113
128
|
<label class="bf-field">⌕ <input class="bf-input" id="filter" type="search" placeholder="filter paths…" aria-label="Filter paths"></label>
|
|
114
129
|
<button type="button" class="bf-btn" id="fold-all" title="Fold or expand every folder">fold all</button>
|
|
115
130
|
</div>
|
|
116
|
-
<nav class="bf-tree" aria-label="File tree">${renderTree(layout.tree, layout.currentRel, layout.summary)}</nav>
|
|
131
|
+
<nav class="bf-tree" aria-label="File tree">${renderTree(layout.tree, layout.currentRel, layout.base, layout.summary)}</nav>
|
|
117
132
|
<p class="bf-note" id="tree-empty" hidden>No matches.</p>
|
|
118
133
|
</aside>
|
|
119
134
|
</details>
|
|
@@ -124,36 +139,36 @@ ${layout.main}
|
|
|
124
139
|
</div>
|
|
125
140
|
<footer class="bf-app__footer">${layout.live ? renderLiveStatus() : ""}served from ${escapeHtml(layout.rootLabel)} · read-only · ${escapeHtml(layout.footerNote)}</footer>
|
|
126
141
|
${layout.keys && layout.keys.length > 0 ? keysSheet(layout.keys) : ""}
|
|
127
|
-
<script src="/app.js" defer></script>
|
|
142
|
+
<script src="${layout.base}/app.js" defer></script>
|
|
128
143
|
</body>
|
|
129
144
|
</html>
|
|
130
145
|
`;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
/** The breadcrumb trail for a root-relative path, up to (not including) the current page. */
|
|
134
|
-
export function renderBreadcrumbs(rel: string): string {
|
|
149
|
+
export function renderBreadcrumbs(rel: string, base: string): string {
|
|
135
150
|
const segments = rel.split("/").filter(Boolean);
|
|
136
151
|
if (segments.length === 0) return `<span aria-current="page">home</span>`;
|
|
137
152
|
// The last segment is the current page, which the heading or card title names;
|
|
138
153
|
// the trail stops at its parent so the name is not shown twice.
|
|
139
|
-
const parts = [`<a href="/browse/">home</a>`];
|
|
154
|
+
const parts = [`<a href="${base}/browse/">home</a>`];
|
|
140
155
|
let accumulated = "";
|
|
141
156
|
for (const segment of segments.slice(0, -1)) {
|
|
142
157
|
accumulated = accumulated ? `${accumulated}/${segment}` : segment;
|
|
143
|
-
parts.push(`<a href="/browse/${encodePath(accumulated)}">${escapeHtml(segment)}</a>`);
|
|
158
|
+
parts.push(`<a href="${base}/browse/${encodePath(accumulated)}">${escapeHtml(segment)}</a>`);
|
|
144
159
|
}
|
|
145
160
|
return parts.join(`<span class="bf-crumbs__sep">/</span>`);
|
|
146
161
|
}
|
|
147
162
|
|
|
148
|
-
function renderTree(node: TreeNode, currentRel: string, summary?: ChangeSummary): string {
|
|
149
|
-
return `<ul>${renderTreeNode(node, currentRel, summary)}</ul>`;
|
|
163
|
+
function renderTree(node: TreeNode, currentRel: string, base: string, summary?: ChangeSummary): string {
|
|
164
|
+
return `<ul>${renderTreeNode(node, currentRel, base, summary)}</ul>`;
|
|
150
165
|
}
|
|
151
166
|
|
|
152
|
-
function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSummary): string {
|
|
167
|
+
function renderTreeNode(node: TreeNode, currentRel: string, base: string, summary?: ChangeSummary): string {
|
|
153
168
|
const expanded = node.children.length > 0;
|
|
154
169
|
const isDir = node.isDir;
|
|
155
170
|
const spec = isDir ? FOLDER_ICON : languageOf(node.name);
|
|
156
|
-
const href = hrefFor(node);
|
|
171
|
+
const href = hrefFor(node, base);
|
|
157
172
|
const current = node.rel === currentRel ? ` aria-current="page"` : "";
|
|
158
173
|
const twisty = isDir
|
|
159
174
|
? `<button type="button" class="bf-tree__tw bf-tree__tw--toggle" tabindex="-1" aria-hidden="true">${expanded ? "▾" : "▸"}</button>`
|
|
@@ -161,7 +176,7 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
|
|
|
161
176
|
const dataPath = ` data-path="${escapeHtml(node.rel)}"`;
|
|
162
177
|
const name = isDir ? `${escapeHtml(node.name)}/` : escapeHtml(node.name);
|
|
163
178
|
const children = expanded
|
|
164
|
-
? `<ul>${node.children.map((child) => renderTreeNode(child, currentRel, summary)).join("")}</ul>`
|
|
179
|
+
? `<ul>${node.children.map((child) => renderTreeNode(child, currentRel, base, summary)).join("")}</ul>`
|
|
165
180
|
: "";
|
|
166
181
|
const label = node.broken
|
|
167
182
|
? `<span class="browse-broken" title="broken symbolic link">${name}</span><span class="browse-link-mark">↩</span>`
|
|
@@ -171,11 +186,17 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
|
|
|
171
186
|
return `<li data-open="${expanded}"><div class="bf-tree__row"${current}${dataPath}>${twisty}${iconMarkup(spec)}${label}${changeMark(summary, node.rel, isDir)}</div>${children}</li>`;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
function renderRow(
|
|
189
|
+
function renderRow(
|
|
190
|
+
entry: DirEntry,
|
|
191
|
+
maxThumbBytes: number,
|
|
192
|
+
thumbnails: boolean,
|
|
193
|
+
base: string,
|
|
194
|
+
summary?: ChangeSummary,
|
|
195
|
+
): string {
|
|
175
196
|
const spec = entry.isDir ? FOLDER_ICON : languageOf(entry.name);
|
|
176
197
|
const showThumb = thumbnails && entry.isImage && !entry.broken && entry.size <= maxThumbBytes;
|
|
177
198
|
const visual = showThumb
|
|
178
|
-
? `<img class="bf-table__thumb" alt="" loading="lazy" decoding="async" src="/raw/${encodePath(entry.rel)}">${iconMarkup(spec, true)}`
|
|
199
|
+
? `<img class="bf-table__thumb" alt="" loading="lazy" decoding="async" src="${base}/raw/${encodePath(entry.rel)}">${iconMarkup(spec, true)}`
|
|
179
200
|
: iconMarkup(spec);
|
|
180
201
|
const name = entry.isDir ? `${escapeHtml(entry.name)}/` : escapeHtml(entry.name);
|
|
181
202
|
const classes = [entry.name.startsWith(".") ? "browse-dotfile" : "", entry.isSymlink ? "browse-symlink" : ""]
|
|
@@ -183,7 +204,7 @@ function renderRow(entry: DirEntry, maxThumbBytes: number, thumbnails: boolean,
|
|
|
183
204
|
.join(" ");
|
|
184
205
|
const link = entry.broken
|
|
185
206
|
? `<span class="browse-broken" title="broken symbolic link">${name}</span>`
|
|
186
|
-
: `<a href="${hrefFor(entry)}" title="${escapeHtml(entry.rel)}">${name}</a>`;
|
|
207
|
+
: `<a href="${hrefFor(entry, base)}" title="${escapeHtml(entry.rel)}">${name}</a>`;
|
|
187
208
|
const mark = entry.isSymlink
|
|
188
209
|
? `<span class="browse-link-mark" title="${entry.broken ? "broken symbolic link" : "symbolic link"}">↩</span>`
|
|
189
210
|
: "";
|
|
@@ -192,6 +213,7 @@ function renderRow(entry: DirEntry, maxThumbBytes: number, thumbnails: boolean,
|
|
|
192
213
|
}
|
|
193
214
|
|
|
194
215
|
export interface DirectoryPage {
|
|
216
|
+
base: string;
|
|
195
217
|
rootLabel: string;
|
|
196
218
|
rel: string;
|
|
197
219
|
listing: Listing;
|
|
@@ -213,7 +235,7 @@ export function renderDirectoryPage(page: DirectoryPage): string {
|
|
|
213
235
|
let body = `<table class="bf-table browse-listing" aria-labelledby="page-title">
|
|
214
236
|
<thead><tr><th scope="col" aria-sort="ascending"><button type="button" class="bf-sort" data-sort="name" title="Sort by name">Name</button></th><th scope="col" class="bf-table__size"><button type="button" class="bf-sort" data-sort="size" title="Sort by size">Size</button></th><th scope="col" class="bf-table__when"><button type="button" class="bf-sort" data-sort="mtime" title="Sort by modified time">Modified</button></th></tr></thead>
|
|
215
237
|
<tbody>
|
|
216
|
-
${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.thumbnails, page.summary)).join("\n")}
|
|
238
|
+
${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.thumbnails, page.base, page.summary)).join("\n")}
|
|
217
239
|
</tbody>
|
|
218
240
|
</table>`;
|
|
219
241
|
if (page.listing.entries.length === 0) body = `<div class="bf-empty">This folder is empty.</div>`;
|
|
@@ -221,11 +243,12 @@ ${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.
|
|
|
221
243
|
body += `<p class="bf-note">Showing the first ${page.listing.entries.length} of ${page.listing.total} entries.</p>`;
|
|
222
244
|
|
|
223
245
|
return renderLayout({
|
|
224
|
-
|
|
246
|
+
base: page.base,
|
|
247
|
+
title: tabTitle("browse", page.rel || folderName),
|
|
225
248
|
rootLabel: page.rootLabel,
|
|
226
249
|
tree: page.tree,
|
|
227
250
|
currentRel: page.rel,
|
|
228
|
-
breadcrumbs: renderBreadcrumbs(page.rel),
|
|
251
|
+
breadcrumbs: renderBreadcrumbs(page.rel, page.base),
|
|
229
252
|
keys: pageKeys(),
|
|
230
253
|
main: ` ${heading}\n ${body}`,
|
|
231
254
|
footerNote: `${page.listing.total} entr${page.listing.total === 1 ? "y" : "ies"}`,
|
|
@@ -236,6 +259,7 @@ ${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.
|
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
export interface FilePage {
|
|
262
|
+
base: string;
|
|
239
263
|
rootLabel: string;
|
|
240
264
|
rel: string;
|
|
241
265
|
file: FileView;
|
|
@@ -291,12 +315,12 @@ function numberedLines(page: FilePage): boolean {
|
|
|
291
315
|
* plus the download the diff head keeps in the same group. */
|
|
292
316
|
function renderFileActions(page: FilePage, download: string): string {
|
|
293
317
|
const isMarkdown = page.file.kind === "markdown";
|
|
294
|
-
const rawHref =
|
|
295
|
-
const viewHref =
|
|
318
|
+
const rawHref = `${page.base}/raw/${encodePath(page.rel)}`;
|
|
319
|
+
const viewHref = `${page.base}/view/${encodePath(page.rel)}`;
|
|
296
320
|
const groups: string[] = [];
|
|
297
321
|
if (page.changed) {
|
|
298
322
|
groups.push(
|
|
299
|
-
`<a class="bf-btn" href="/diff/${encodePath(page.rel)}" title="View this file's uncommitted changes">changes</a>`,
|
|
323
|
+
`<a class="bf-btn" href="${page.base}/diff/${encodePath(page.rel)}" title="View this file's uncommitted changes">changes</a>`,
|
|
300
324
|
);
|
|
301
325
|
}
|
|
302
326
|
if (isMarkdown) {
|
|
@@ -337,7 +361,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
337
361
|
// Text is always served as octet-stream, so the response type says nothing
|
|
338
362
|
// about the file; the language label, size, and date are the useful metadata.
|
|
339
363
|
const meta = `${spec.label} · ${formatSize(page.file.size)} · ${formatDate(page.file.mtimeMs)}`;
|
|
340
|
-
const download = `<a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a>`;
|
|
364
|
+
const download = `<a class="bf-btn" href="${page.base}/raw/${encodePath(page.rel)}" download>download</a>`;
|
|
341
365
|
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>`;
|
|
342
366
|
|
|
343
367
|
const isMarkdown = page.file.kind === "markdown";
|
|
@@ -355,6 +379,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
355
379
|
if (page.file.kind === "markdown" && !page.markdownRaw) {
|
|
356
380
|
const html = renderMarkdown(page.file.lines.join("\n"), markdownBaseDir(page.rel), {
|
|
357
381
|
html: page.markdownHtml !== false,
|
|
382
|
+
base: page.base,
|
|
358
383
|
});
|
|
359
384
|
content = `<article class="bf-md">${html}</article>`;
|
|
360
385
|
} else {
|
|
@@ -363,7 +388,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
363
388
|
if (page.file.truncated)
|
|
364
389
|
note = `<p class="bf-note">Showing the first ${page.file.lines.length} of ${page.file.totalLines} lines.</p>`;
|
|
365
390
|
} else if (page.file.kind === "image") {
|
|
366
|
-
content = `<div class="bf-media"><img src="/raw/${encodePath(page.rel)}" alt="${escapeHtml(name)}"></div>`;
|
|
391
|
+
content = `<div class="bf-media"><img src="${page.base}/raw/${encodePath(page.rel)}" alt="${escapeHtml(name)}"></div>`;
|
|
367
392
|
} else if (page.file.kind === "tooLarge") {
|
|
368
393
|
content = `<div class="bf-empty"><p class="bf-empty__title">File is ${escapeHtml(formatSize(page.file.size))}.</p><p class="bf-empty__sub">Larger than the ${escapeHtml(formatSize(page.maxFileBytes))} inline limit.</p>${download}</div>`;
|
|
369
394
|
} else {
|
|
@@ -371,11 +396,12 @@ export function renderFilePage(page: FilePage): string {
|
|
|
371
396
|
}
|
|
372
397
|
|
|
373
398
|
return renderLayout({
|
|
374
|
-
|
|
399
|
+
base: page.base,
|
|
400
|
+
title: tabTitle("browse", page.rel),
|
|
375
401
|
rootLabel: page.rootLabel,
|
|
376
402
|
tree: page.tree,
|
|
377
403
|
currentRel: page.rel,
|
|
378
|
-
breadcrumbs: renderBreadcrumbs(page.rel),
|
|
404
|
+
breadcrumbs: renderBreadcrumbs(page.rel, page.base),
|
|
379
405
|
mainClass: rendered ? "browse-page browse-measure" : "browse-page",
|
|
380
406
|
keys,
|
|
381
407
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
@@ -419,6 +445,7 @@ function renderModeControl(mode: DiffMode, build: (mode: DiffMode) => string): s
|
|
|
419
445
|
}
|
|
420
446
|
|
|
421
447
|
export interface DiffPage {
|
|
448
|
+
base: string;
|
|
422
449
|
rootLabel: string;
|
|
423
450
|
mode: DiffMode;
|
|
424
451
|
files: ChangedFile[];
|
|
@@ -432,23 +459,24 @@ export interface DiffPage {
|
|
|
432
459
|
|
|
433
460
|
/** The change overview: `git diff --stat` plus every changed path. */
|
|
434
461
|
export function renderDiffPage(page: DiffPage): string {
|
|
435
|
-
const modeHref = (mode: DiffMode) => (mode === "head" ?
|
|
462
|
+
const modeHref = (mode: DiffMode) => (mode === "head" ? `${page.base}/diff` : `${page.base}/diff?mode=${mode}`);
|
|
436
463
|
const body =
|
|
437
464
|
page.files.length === 0
|
|
438
465
|
? `<div class="bf-empty"><p class="bf-empty__title">No changes ${escapeHtml(modeScope(page.mode))}.</p><p class="bf-empty__sub">The working tree matches this scope.</p></div>`
|
|
439
466
|
: `<table class="bf-table browse-diff" aria-labelledby="page-title">
|
|
440
467
|
<thead><tr><th scope="col" class="browse-diff-status">Status</th><th scope="col">Path</th><th scope="col" class="browse-diff-word">Changes</th></tr></thead>
|
|
441
468
|
<tbody>
|
|
442
|
-
${page.files.map((file) => renderChangedRow(file, page.mode)).join("\n")}
|
|
469
|
+
${page.files.map((file) => renderChangedRow(file, page.mode, page.base)).join("\n")}
|
|
443
470
|
</tbody>
|
|
444
471
|
</table>`;
|
|
445
472
|
const stat = page.stat ? `<pre class="browse-diff-stat">${escapeHtml(page.stat)}</pre>` : "";
|
|
446
473
|
return renderLayout({
|
|
447
|
-
|
|
474
|
+
base: page.base,
|
|
475
|
+
title: tabTitle("browse", "changes"),
|
|
448
476
|
rootLabel: page.rootLabel,
|
|
449
477
|
tree: page.tree,
|
|
450
478
|
currentRel: "",
|
|
451
|
-
breadcrumbs: renderBreadcrumbs(""),
|
|
479
|
+
breadcrumbs: renderBreadcrumbs("", page.base),
|
|
452
480
|
mainClass: "browse-page",
|
|
453
481
|
keys: pageKeys(),
|
|
454
482
|
main: ` <h1 class="bf-title browse-heading" id="page-title">Changes <span class="browse-diff-scope">${escapeHtml(modeScope(page.mode))}</span></h1>\n <div class="browse-diff-toolbar">${renderModeControl(page.mode, modeHref)}</div>\n ${stat}\n ${body}`,
|
|
@@ -459,16 +487,17 @@ ${page.files.map((file) => renderChangedRow(file, page.mode)).join("\n")}
|
|
|
459
487
|
});
|
|
460
488
|
}
|
|
461
489
|
|
|
462
|
-
function renderChangedRow(file: ChangedFile, mode: DiffMode): string {
|
|
490
|
+
function renderChangedRow(file: ChangedFile, mode: DiffMode, base: string): string {
|
|
463
491
|
const letter = statusLetter(file);
|
|
464
492
|
const label = statusWord(letter);
|
|
465
493
|
const badge = `browse-diff-badge browse-diff-badge--${letter === "?" ? "Q" : letter}`;
|
|
466
494
|
const orig = file.origRel ? ` <span class="browse-diff-orig">from ${escapeHtml(file.origRel)}</span>` : "";
|
|
467
|
-
const href =
|
|
495
|
+
const href = `${base}/diff/${encodePath(file.rel)}${mode === "head" ? "" : `?mode=${mode}`}`;
|
|
468
496
|
return `<tr data-path="${escapeHtml(file.rel)}"><td><span class="${badge}" aria-label="${escapeHtml(label)}">${escapeHtml(letter)}</span></td><td><span class="bf-table__name">${iconMarkup(languageOf(file.rel))}<a href="${href}">${escapeHtml(file.rel)}</a>${orig}</span></td><td class="browse-diff-word">${escapeHtml(label)}</td></tr>`;
|
|
469
497
|
}
|
|
470
498
|
|
|
471
499
|
export interface FileDiffPage {
|
|
500
|
+
base: string;
|
|
472
501
|
rootLabel: string;
|
|
473
502
|
rel: string;
|
|
474
503
|
mode: DiffMode;
|
|
@@ -484,12 +513,12 @@ export interface FileDiffPage {
|
|
|
484
513
|
export function renderFileDiffPage(page: FileDiffPage): string {
|
|
485
514
|
const name = page.rel.slice(page.rel.lastIndexOf("/") + 1);
|
|
486
515
|
const spec = languageOf(name);
|
|
487
|
-
const
|
|
516
|
+
const diffBase = `${page.base}/diff/${encodePath(page.rel)}`;
|
|
488
517
|
const actions = [
|
|
489
|
-
renderModeControl(page.mode, (mode) => (mode === "head" ?
|
|
490
|
-
`<a class="bf-btn" href="/view/${encodePath(page.rel)}" title="Open the file">view file</a>`,
|
|
518
|
+
renderModeControl(page.mode, (mode) => (mode === "head" ? diffBase : `${diffBase}?mode=${mode}`)),
|
|
519
|
+
`<a class="bf-btn" href="${page.base}/view/${encodePath(page.rel)}" title="Open the file">view file</a>`,
|
|
491
520
|
`<button type="button" class="bf-btn" id="wrap-toggle" aria-pressed="false" title="Toggle line wrapping">wrap</button>`,
|
|
492
|
-
`<a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a>`,
|
|
521
|
+
`<a class="bf-btn" href="${page.base}/raw/${encodePath(page.rel)}" download>download</a>`,
|
|
493
522
|
`<button type="button" class="bf-btn" data-copy="path" data-value="${escapeHtml(page.rel)}" title="Copy the root-relative path">copy path</button>`,
|
|
494
523
|
];
|
|
495
524
|
// A per-file change summary and hunk count, so the scope is visible before scrolling.
|
|
@@ -508,7 +537,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
508
537
|
let content: string;
|
|
509
538
|
let note = "";
|
|
510
539
|
if (page.diff.binary) {
|
|
511
|
-
content = `<div class="bf-empty"><p class="bf-empty__title">Binary or unreadable change.</p><p class="bf-empty__sub">No text diff is shown for this file.</p><a class="bf-btn" href="/raw/${encodePath(page.rel)}" download>download</a></div>`;
|
|
540
|
+
content = `<div class="bf-empty"><p class="bf-empty__title">Binary or unreadable change.</p><p class="bf-empty__sub">No text diff is shown for this file.</p><a class="bf-btn" href="${page.base}/raw/${encodePath(page.rel)}" download>download</a></div>`;
|
|
512
541
|
} else if (page.diff.lines.length === 0) {
|
|
513
542
|
content = `<div class="bf-empty"><p class="bf-empty__title">No changes for this file.</p><p class="bf-empty__sub">It is unchanged in the selected scope.</p></div>`;
|
|
514
543
|
} else {
|
|
@@ -516,11 +545,12 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
516
545
|
if (page.diff.truncated) note = `<p class="bf-note">Showing the first ${page.diff.lines.length} diff lines.</p>`;
|
|
517
546
|
}
|
|
518
547
|
return renderLayout({
|
|
519
|
-
|
|
548
|
+
base: page.base,
|
|
549
|
+
title: tabTitle("browse", page.rel),
|
|
520
550
|
rootLabel: page.rootLabel,
|
|
521
551
|
tree: page.tree,
|
|
522
552
|
currentRel: page.rel,
|
|
523
|
-
breadcrumbs: renderBreadcrumbs(page.rel),
|
|
553
|
+
breadcrumbs: renderBreadcrumbs(page.rel, page.base),
|
|
524
554
|
mainClass: "browse-page",
|
|
525
555
|
keys,
|
|
526
556
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
@@ -532,6 +562,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
532
562
|
}
|
|
533
563
|
|
|
534
564
|
export interface ErrorPage {
|
|
565
|
+
base: string;
|
|
535
566
|
rootLabel: string;
|
|
536
567
|
status: number;
|
|
537
568
|
message: string;
|
|
@@ -544,12 +575,13 @@ export interface ErrorPage {
|
|
|
544
575
|
/** Render a status/error page with the same chrome. */
|
|
545
576
|
export function renderErrorPage(page: ErrorPage): string {
|
|
546
577
|
return renderLayout({
|
|
547
|
-
|
|
578
|
+
base: page.base,
|
|
579
|
+
title: tabTitle("browse", String(page.status)),
|
|
548
580
|
rootLabel: page.rootLabel,
|
|
549
581
|
tree: page.tree,
|
|
550
582
|
currentRel: "",
|
|
551
|
-
breadcrumbs: renderBreadcrumbs(""),
|
|
552
|
-
main: ` <div class="bf-empty"><div class="bf-empty__code">${page.status}</div><p class="bf-empty__title">${escapeHtml(page.message)}</p><p class="bf-empty__sub"><a href="/browse/">back to the served folder</a></p></div>`,
|
|
583
|
+
breadcrumbs: renderBreadcrumbs("", page.base),
|
|
584
|
+
main: ` <div class="bf-empty"><div class="bf-empty__code">${page.status}</div><p class="bf-empty__title">${escapeHtml(page.message)}</p><p class="bf-empty__sub"><a href="${page.base}/browse/">back to the served folder</a></p></div>`,
|
|
553
585
|
footerNote: "error",
|
|
554
586
|
git: page.git,
|
|
555
587
|
live: page.live,
|
|
@@ -166,7 +166,7 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
|
|
|
166
166
|
|
|
167
167
|
function checkLiveDir() {
|
|
168
168
|
if (document.hidden || liveTone !== "success") return;
|
|
169
|
-
fetch("/api/dir-rev?path=" + encodeURIComponent(liveDir))
|
|
169
|
+
fetch(pageBase() + "/api/dir-rev?path=" + encodeURIComponent(liveDir))
|
|
170
170
|
.then(function (response) {
|
|
171
171
|
if (!response.ok) throw new Error("check failed");
|
|
172
172
|
return response.json();
|
|
@@ -179,7 +179,7 @@ export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the heade
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
if (window.EventSource) {
|
|
182
|
-
var liveStream = new EventSource("/events");
|
|
182
|
+
var liveStream = new EventSource(pageBase() + "/events");
|
|
183
183
|
liveStream.addEventListener("open", function () {
|
|
184
184
|
if (liveTone !== "success") setLiveTone("accent", "connecting");
|
|
185
185
|
});
|