@gavin-hu/my-pi-agent 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -14,21 +14,15 @@ import { realpathSync } from "node:fs";
|
|
|
14
14
|
import { stat } from "node:fs/promises";
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { isExtensionEnabled, resolveEffectiveCwd } from "../../lib/env.ts";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { createGitDiff, createGitStatus } from "./git/git.ts";
|
|
21
|
-
import { openInBrowser } from "../../lib/browser.ts";
|
|
22
|
-
import { HttpError, resolveRequestPath } from "./files/paths.ts";
|
|
23
|
-
import { createFileServer, type FileServer } from "./transport/server.ts";
|
|
17
|
+
import type { openInBrowser } from "../../lib/browser.ts";
|
|
18
|
+
import type { ServeConfig } from "./config/config.ts";
|
|
19
|
+
import type { FileServer } from "./transport/server.ts";
|
|
24
20
|
|
|
25
21
|
export interface FileBrowserDeps {
|
|
26
22
|
/** Override the server factory (tests). */
|
|
27
|
-
createServer?: typeof createFileServer;
|
|
23
|
+
createServer?: typeof import("./transport/server.ts").createFileServer;
|
|
28
24
|
/** Override the browser opener (tests). */
|
|
29
25
|
open?: typeof openInBrowser;
|
|
30
|
-
/** Override session-port selection (tests). */
|
|
31
|
-
pickPort?: () => number;
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
interface RunningServer {
|
|
@@ -40,94 +34,76 @@ interface RunningServer {
|
|
|
40
34
|
export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}): void {
|
|
41
35
|
if (!isExtensionEnabled("file-browse")) return;
|
|
42
36
|
let running: RunningServer | undefined;
|
|
43
|
-
/**
|
|
44
|
-
let
|
|
37
|
+
/** The session this page serves as; the mount lands on its one server. */
|
|
38
|
+
let sessionId = "";
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
const setBrowseStatus = (ctx: ExtensionContext, port: number | undefined): void => {
|
|
48
|
-
try {
|
|
49
|
-
const value =
|
|
50
|
-
port === undefined
|
|
51
|
-
? undefined
|
|
52
|
-
: `${paint(ctx.ui.theme, "ok", GLYPHS.browse)} ${paint(ctx.ui.theme, "accent", String(port))}`;
|
|
53
|
-
ctx.ui.setStatus(STATUS_KEYS.browse, value);
|
|
54
|
-
} catch {
|
|
55
|
-
// A UI without a theme or setStatus must not break the command.
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const stop = async (ctx?: ExtensionContext): Promise<boolean> => {
|
|
40
|
+
const stop = async (): Promise<boolean> => {
|
|
60
41
|
const current = running;
|
|
61
42
|
if (!current) return false;
|
|
62
43
|
running = undefined;
|
|
63
44
|
await current.server.close();
|
|
64
|
-
if (ctx) setBrowseStatus(ctx, undefined);
|
|
65
45
|
return true;
|
|
66
46
|
};
|
|
67
47
|
|
|
68
48
|
/**
|
|
69
|
-
*
|
|
70
|
-
* left to serve
|
|
71
|
-
* one. `/browse` starts it again on the same session port.
|
|
49
|
+
* Give the prefix back once the page that was its only reader has gone: nothing is
|
|
50
|
+
* left to serve. `/browse` starts it again on the session's port.
|
|
72
51
|
*/
|
|
73
52
|
const stopWhenPageLeft = async (): Promise<void> => {
|
|
74
53
|
const current = running;
|
|
75
54
|
if (!current) return;
|
|
76
|
-
if (await stop(
|
|
55
|
+
if (await stop()) current.ctx.ui.notify("browse: stopped — the last page was closed", "info");
|
|
77
56
|
};
|
|
78
57
|
|
|
79
|
-
|
|
80
|
-
const target = args.trim();
|
|
81
|
-
if (target === "status") {
|
|
82
|
-
ctx.ui.notify(
|
|
83
|
-
running ? `browse: ${running.server.root} at ${running.server.url}` : "browse: not running",
|
|
84
|
-
"info",
|
|
85
|
-
);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (target === "stop") {
|
|
89
|
-
ctx.ui.notify((await stop(ctx)) ? "browse: stopped" : "browse: not running", "info");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
58
|
+
type ResolvedRoot = { root: string; config: ServeConfig };
|
|
92
59
|
|
|
93
|
-
|
|
60
|
+
/** The folder `/browse [path]` names, or undefined after telling the user why. */
|
|
61
|
+
const resolveRoot = async (target: string, cwd: string, ctx: ExtensionContext): Promise<ResolvedRoot | undefined> => {
|
|
62
|
+
const { loadConfig } = await import("./config/config.ts");
|
|
94
63
|
const config = loadConfig(cwd);
|
|
95
|
-
// A pinned config port wins; otherwise one random port is fixed for the session.
|
|
96
|
-
const unpinned = config.port <= 0;
|
|
97
|
-
const port = unpinned ? (sessionPort ?? (deps.pickPort ?? randomPort)()) : config.port;
|
|
98
64
|
let root = cwd;
|
|
99
65
|
if (target) {
|
|
66
|
+
const { HttpError, resolveRequestPath } = await import("./files/paths.ts");
|
|
100
67
|
try {
|
|
101
68
|
root = await resolveRequestPath(cwd, target);
|
|
102
69
|
} catch (error) {
|
|
103
70
|
const message = error instanceof HttpError ? error.message : String(error);
|
|
104
71
|
ctx.ui.notify(`browse: ${message}`, "error");
|
|
105
|
-
return;
|
|
72
|
+
return undefined;
|
|
106
73
|
}
|
|
107
74
|
const info = await stat(root).catch(() => undefined);
|
|
108
75
|
if (!info?.isDirectory()) {
|
|
109
76
|
ctx.ui.notify(`browse: ${target} is not a folder inside the working directory`, "error");
|
|
110
|
-
return;
|
|
77
|
+
return undefined;
|
|
111
78
|
}
|
|
112
79
|
}
|
|
80
|
+
return { root, config };
|
|
81
|
+
};
|
|
113
82
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Put the page in front of the user, unless the project asked to stay headless.
|
|
85
|
+
*
|
|
86
|
+
* Every `/browse` opens it, including one that finds the page already served: the
|
|
87
|
+
* command's job is showing the page, and a URL printed over a running server leaves
|
|
88
|
+
* the user to paste it.
|
|
89
|
+
*/
|
|
90
|
+
const openPage = async (url: string, config: ServeConfig): Promise<void> => {
|
|
91
|
+
if (!config.autoOpen) return;
|
|
92
|
+
const open = deps.open ?? (await import("../../lib/browser.ts")).openInBrowser;
|
|
93
|
+
open(url);
|
|
94
|
+
};
|
|
124
95
|
|
|
125
|
-
|
|
126
|
-
|
|
96
|
+
/** Open the page's server; the server, git, HTTP, and browser modules live here. */
|
|
97
|
+
const openServer = async (root: string, config: ServeConfig, ctx: ExtensionContext): Promise<void> => {
|
|
98
|
+
const create = deps.createServer ?? (await import("./transport/server.ts")).createFileServer;
|
|
99
|
+
const { createGitDiff, createGitStatus } = await import("./git/git.ts");
|
|
100
|
+
const { requestMount } = await import("../../lib/http/index.ts");
|
|
127
101
|
try {
|
|
128
|
-
const
|
|
102
|
+
const outcome = await create({
|
|
129
103
|
root,
|
|
130
|
-
config
|
|
104
|
+
config,
|
|
105
|
+
mount: (spec) => requestMount(pi, spec),
|
|
106
|
+
session: sessionId,
|
|
131
107
|
git: createGitStatus(pi, root),
|
|
132
108
|
diff: createGitDiff(pi, root, {
|
|
133
109
|
context: config.diffContext,
|
|
@@ -137,22 +113,53 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
|
|
|
137
113
|
}),
|
|
138
114
|
onIdle: () => void stopWhenPageLeft(),
|
|
139
115
|
});
|
|
140
|
-
if (
|
|
116
|
+
if (outcome.kind === "refused") {
|
|
117
|
+
ctx.ui.notify(`browse: ${outcome.reason}`, "error");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const server = outcome.server;
|
|
141
121
|
running = { server, ctx };
|
|
142
|
-
setBrowseStatus(ctx, server.port);
|
|
143
122
|
ctx.ui.notify(`browse: ${server.root} at ${server.url}`, "info");
|
|
144
|
-
|
|
123
|
+
await openPage(server.url, config);
|
|
145
124
|
} catch (error) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
125
|
+
ctx.ui.notify(`browse: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const start = async (args: string, ctx: ExtensionContext): Promise<void> => {
|
|
130
|
+
const target = args.trim();
|
|
131
|
+
if (target === "status") {
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
running ? `browse: ${running.server.root} at ${running.server.url}` : "browse: not running",
|
|
134
|
+
"info",
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (target === "stop") {
|
|
139
|
+
ctx.ui.notify((await stop()) ? "browse: stopped" : "browse: not running", "info");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cwd = resolveEffectiveCwd(ctx.cwd);
|
|
144
|
+
const resolved = await resolveRoot(target, cwd, ctx);
|
|
145
|
+
if (resolved === undefined) return;
|
|
146
|
+
const { root, config } = resolved;
|
|
147
|
+
|
|
148
|
+
let realRoot = root;
|
|
149
|
+
try {
|
|
150
|
+
realRoot = realpathSync.native(root);
|
|
151
|
+
} catch {
|
|
152
|
+
// Keep the unresolved root; the server factory reports a real failure.
|
|
153
|
+
}
|
|
154
|
+
if (running && running.server.root === realRoot) {
|
|
155
|
+
ctx.ui.notify(`browse: ${running.server.url} (already running)`, "info");
|
|
156
|
+
// The page is already served, so this is an open and not a restart.
|
|
157
|
+
await openPage(running.server.url, config);
|
|
158
|
+
return;
|
|
155
159
|
}
|
|
160
|
+
|
|
161
|
+
await stop();
|
|
162
|
+
await openServer(root, config, ctx);
|
|
156
163
|
};
|
|
157
164
|
|
|
158
165
|
pi.registerCommand("browse", {
|
|
@@ -168,8 +175,11 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
|
|
|
168
175
|
},
|
|
169
176
|
});
|
|
170
177
|
|
|
171
|
-
pi.on("
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
179
|
+
sessionId = ctx.sessionManager.getSessionId();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
pi.on("session_shutdown", async () => {
|
|
183
|
+
await stop();
|
|
174
184
|
});
|
|
175
185
|
}
|
|
@@ -174,7 +174,7 @@ export interface LiveChannel {
|
|
|
174
174
|
* attached, so an idle server does no status work at all; the last page leaving
|
|
175
175
|
* parks it and the next page starts it again. What happens to a server nobody is
|
|
176
176
|
* reading — the idle grace and the close it leads to — is the server's policy, so
|
|
177
|
-
* the caller hands it to
|
|
177
|
+
* the caller hands it to its own mount instead of configuring it
|
|
178
178
|
* here.
|
|
179
179
|
*/
|
|
180
180
|
export function createLiveChannel(options: LiveChannelOptions): LiveChannel {
|
|
@@ -46,13 +46,16 @@ import {
|
|
|
46
46
|
renderErrorPage,
|
|
47
47
|
renderFileDiffPage,
|
|
48
48
|
renderFilePage,
|
|
49
|
+
hrefFor,
|
|
49
50
|
} from "../view/html.ts";
|
|
50
51
|
import type { ChangeSummary } from "../view/html.ts";
|
|
51
|
-
import {
|
|
52
|
+
import { HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "../files/paths.ts";
|
|
52
53
|
import { listingRevision } from "../live/rev.ts";
|
|
53
54
|
|
|
54
55
|
export interface ServeContext {
|
|
55
56
|
root: string;
|
|
57
|
+
/** The prefix this page answers under, e.g. `/files`; `""` for the origin's root. */
|
|
58
|
+
base: string;
|
|
56
59
|
config: ServeConfig;
|
|
57
60
|
/** Read-only git context for the page header; omitted when unavailable. */
|
|
58
61
|
git?: GitStatusProvider;
|
|
@@ -152,6 +155,7 @@ async function errorPage(
|
|
|
152
155
|
// Keep the minimal tree.
|
|
153
156
|
}
|
|
154
157
|
const body = renderErrorPage({
|
|
158
|
+
base: ctx.base,
|
|
155
159
|
rootLabel: ctx.root,
|
|
156
160
|
status,
|
|
157
161
|
message,
|
|
@@ -187,6 +191,7 @@ async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeR
|
|
|
187
191
|
return htmlResponse(
|
|
188
192
|
200,
|
|
189
193
|
renderDirectoryPage({
|
|
194
|
+
base: ctx.base,
|
|
190
195
|
rootLabel: ctx.root,
|
|
191
196
|
rel,
|
|
192
197
|
listing,
|
|
@@ -211,6 +216,7 @@ async function fileResponse(ctx: ServeContext, rel: string, raw: boolean): Promi
|
|
|
211
216
|
return htmlResponse(
|
|
212
217
|
200,
|
|
213
218
|
renderFilePage({
|
|
219
|
+
base: ctx.base,
|
|
214
220
|
rootLabel: ctx.root,
|
|
215
221
|
rel,
|
|
216
222
|
file,
|
|
@@ -235,6 +241,7 @@ async function diffOverview(ctx: ServeContext, mode: DiffMode): Promise<ServeRes
|
|
|
235
241
|
return htmlResponse(
|
|
236
242
|
200,
|
|
237
243
|
renderDiffPage({
|
|
244
|
+
base: ctx.base,
|
|
238
245
|
rootLabel: ctx.root,
|
|
239
246
|
mode,
|
|
240
247
|
files: list?.files ?? [],
|
|
@@ -267,6 +274,7 @@ async function diffFile(ctx: ServeContext, rawRel: string, mode: DiffMode): Prom
|
|
|
267
274
|
return htmlResponse(
|
|
268
275
|
200,
|
|
269
276
|
renderFileDiffPage({
|
|
277
|
+
base: ctx.base,
|
|
270
278
|
rootLabel: ctx.root,
|
|
271
279
|
rel,
|
|
272
280
|
mode,
|
|
@@ -303,12 +311,12 @@ async function raw(ctx: ServeContext, rawRel: string): Promise<ServeResponse> {
|
|
|
303
311
|
};
|
|
304
312
|
}
|
|
305
313
|
|
|
306
|
-
function treeApiEntry(entry: DirEntry): Record<string, unknown> {
|
|
314
|
+
function treeApiEntry(entry: DirEntry, base: string): Record<string, unknown> {
|
|
307
315
|
const spec = entry.isDir ? FOLDER_ICON : languageOf(entry.name);
|
|
308
316
|
return {
|
|
309
317
|
name: entry.name,
|
|
310
318
|
path: entry.rel,
|
|
311
|
-
href:
|
|
319
|
+
href: hrefFor(entry, base),
|
|
312
320
|
isDir: entry.isDir,
|
|
313
321
|
broken: entry.broken,
|
|
314
322
|
iconHref: iconHref(spec.icon),
|
|
@@ -323,7 +331,7 @@ async function treeApi(ctx: ServeContext, decodedPath: string): Promise<ServeRes
|
|
|
323
331
|
const info = await stat(abs).catch(() => undefined);
|
|
324
332
|
if (info && !info.isDirectory()) throw new HttpError(400, "That path is not a folder.");
|
|
325
333
|
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
326
|
-
return jsonResponse(200, { path: rel, entries: listing.entries.map(treeApiEntry) });
|
|
334
|
+
return jsonResponse(200, { path: rel, entries: listing.entries.map((entry) => treeApiEntry(entry, ctx.base)) });
|
|
327
335
|
}
|
|
328
336
|
|
|
329
337
|
/** The revision of one folder's listing: what a page's reload control compares against. */
|
|
@@ -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) {
|