@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- 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 +561 -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} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- 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 +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -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} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -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 +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- 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 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- 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-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- 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/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/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- 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 -17
- 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/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- 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/job/runtime.ts +4 -4
- 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 +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- 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} +15 -14
- 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 +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- 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 +106 -251
- 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/intake.ts +245 -0
- 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/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- 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 +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -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/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- 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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory listings and single-file reads: what a folder holds, capped and sorted, and
|
|
3
|
+
* what one file's bytes are.
|
|
4
|
+
*
|
|
5
|
+
* The filesystem reads are injected (`DirectoryFs`) so a test can count them: the property
|
|
6
|
+
* worth holding is that a directory is read once per entry, not once per row drawn.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { lstat, readdir, readFile, stat } from "node:fs/promises";
|
|
11
|
+
import type { Stats } from "node:fs";
|
|
12
|
+
import { mapConcurrent } from "../../../lib/pool.ts";
|
|
13
|
+
import { HttpError } from "./paths.ts";
|
|
14
|
+
import {
|
|
15
|
+
classifyByName,
|
|
16
|
+
contentTypeFor,
|
|
17
|
+
type EntryKind,
|
|
18
|
+
isImageName,
|
|
19
|
+
isMarkdownName,
|
|
20
|
+
isProbablyText,
|
|
21
|
+
} from "./classify.ts";
|
|
22
|
+
|
|
23
|
+
export interface DirEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
/** Root-relative posix path. */
|
|
26
|
+
rel: string;
|
|
27
|
+
isDir: boolean;
|
|
28
|
+
isSymlink: boolean;
|
|
29
|
+
broken: boolean;
|
|
30
|
+
size: number;
|
|
31
|
+
mtimeMs: number;
|
|
32
|
+
kind: EntryKind;
|
|
33
|
+
isImage: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Listing {
|
|
37
|
+
entries: DirEntry[];
|
|
38
|
+
total: number;
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FileMeta {
|
|
43
|
+
size: number;
|
|
44
|
+
mtimeMs: number;
|
|
45
|
+
mime: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type FileView =
|
|
49
|
+
| (FileMeta & { kind: "text"; lines: string[]; truncated: boolean; totalLines: number })
|
|
50
|
+
| (FileMeta & { kind: "markdown"; lines: string[]; truncated: boolean; totalLines: number })
|
|
51
|
+
| (FileMeta & { kind: "image" })
|
|
52
|
+
| (FileMeta & { kind: "binary" })
|
|
53
|
+
| (FileMeta & { kind: "tooLarge" });
|
|
54
|
+
|
|
55
|
+
/** Whether the name has an image extension. */
|
|
56
|
+
export function toAbs(root: string, rel: string): string {
|
|
57
|
+
return rel ? join(root, ...rel.split("/")) : root;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The `Dirent` fields a listing reads; a test's fake need not be a real `Dirent`. */
|
|
61
|
+
export interface DirectoryEntryInfo {
|
|
62
|
+
name: string;
|
|
63
|
+
isDirectory(): boolean;
|
|
64
|
+
isSymbolicLink(): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The `Stats` fields a listing reads. */
|
|
68
|
+
export interface DirectoryStats {
|
|
69
|
+
size: number;
|
|
70
|
+
mtimeMs: number;
|
|
71
|
+
isDirectory(): boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The three filesystem reads a listing makes.
|
|
76
|
+
*
|
|
77
|
+
* Injected so a test can observe them: the interesting property — a directory is
|
|
78
|
+
* read once per entry, not once per entry *and* once per row drawn — is a count,
|
|
79
|
+
* not a value, and a wall-clock assertion would be flaky on a loaded machine.
|
|
80
|
+
*/
|
|
81
|
+
export interface DirectoryFs {
|
|
82
|
+
readdir(path: string): Promise<DirectoryEntryInfo[]>;
|
|
83
|
+
lstat(path: string): Promise<DirectoryStats>;
|
|
84
|
+
stat(path: string): Promise<DirectoryStats>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Production reads; a symlink's size and kind come from the target. */
|
|
88
|
+
const NODE_DIRECTORY_FS: DirectoryFs = {
|
|
89
|
+
readdir: (path) => readdir(path, { withFileTypes: true }),
|
|
90
|
+
lstat: (path) => lstat(path),
|
|
91
|
+
stat: (path) => stat(path),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Reads in flight per listing: enough to hide latency, bounded against a burst. */
|
|
95
|
+
const LISTING_CONCURRENCY = 32;
|
|
96
|
+
|
|
97
|
+
/** A row while it is still missing its metadata. */
|
|
98
|
+
interface Row {
|
|
99
|
+
name: string;
|
|
100
|
+
rel: string;
|
|
101
|
+
isDir: boolean;
|
|
102
|
+
isSymlink: boolean;
|
|
103
|
+
broken: boolean;
|
|
104
|
+
size: number;
|
|
105
|
+
mtimeMs: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve one symlink: the link's own stat, then the target's.
|
|
110
|
+
*
|
|
111
|
+
* A symlink's directory-ness is the target's, so this cannot wait until after
|
|
112
|
+
* the sort; it is also the only entry whose classification needs a read.
|
|
113
|
+
*/
|
|
114
|
+
async function resolveSymlink(fs: DirectoryFs, absDir: string, row: Row): Promise<void> {
|
|
115
|
+
const abs = join(absDir, row.name);
|
|
116
|
+
try {
|
|
117
|
+
const info = await fs.lstat(abs);
|
|
118
|
+
row.size = info.size;
|
|
119
|
+
row.mtimeMs = info.mtimeMs;
|
|
120
|
+
} catch {
|
|
121
|
+
// Vanished between readdir and lstat: the target read decides below.
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const target = await fs.stat(abs);
|
|
125
|
+
row.isDir = target.isDirectory();
|
|
126
|
+
row.size = target.size;
|
|
127
|
+
row.mtimeMs = target.mtimeMs;
|
|
128
|
+
} catch {
|
|
129
|
+
row.broken = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List a directory, sorted (directories first, then case-insensitive name).
|
|
135
|
+
*
|
|
136
|
+
* A listing is latency per entry, not work per entry, so it is read in three
|
|
137
|
+
* passes instead of one entry at a time:
|
|
138
|
+
*
|
|
139
|
+
* 1. `readdir`, whose dirents already say whether each name is a directory;
|
|
140
|
+
* 2. the symlinks only, resolved through the pool, because their kind is the
|
|
141
|
+
* target's and the sort needs it;
|
|
142
|
+
* 3. the kept rows only, `stat`ed through the pool — so a directory of three
|
|
143
|
+
* thousand names pays for the two thousand it draws, not for every name, and
|
|
144
|
+
* pays them concurrently instead of in turn.
|
|
145
|
+
*
|
|
146
|
+
* `total` is still the directory's full entry count and `truncated` its `> limit`
|
|
147
|
+
* flip, so the page can say how many were dropped. Ties keep `readdir` order:
|
|
148
|
+
* `sort` is stable and the rows arrive unsorted.
|
|
149
|
+
*/
|
|
150
|
+
export async function listDirectory(
|
|
151
|
+
root: string,
|
|
152
|
+
relDir: string,
|
|
153
|
+
limit: number,
|
|
154
|
+
fs: DirectoryFs = NODE_DIRECTORY_FS,
|
|
155
|
+
): Promise<Listing> {
|
|
156
|
+
const absDir = toAbs(root, relDir);
|
|
157
|
+
let dirents: DirectoryEntryInfo[];
|
|
158
|
+
try {
|
|
159
|
+
dirents = await fs.readdir(absDir);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new HttpError(404, "That folder could not be read.");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const links: Row[] = [];
|
|
165
|
+
const rows: Row[] = dirents.map((dirent) => {
|
|
166
|
+
const row: Row = {
|
|
167
|
+
name: dirent.name,
|
|
168
|
+
rel: relDir ? `${relDir}/${dirent.name}` : dirent.name,
|
|
169
|
+
isDir: dirent.isDirectory(),
|
|
170
|
+
isSymlink: dirent.isSymbolicLink(),
|
|
171
|
+
broken: false,
|
|
172
|
+
size: 0,
|
|
173
|
+
mtimeMs: 0,
|
|
174
|
+
};
|
|
175
|
+
if (row.isSymlink) links.push(row);
|
|
176
|
+
return row;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await mapConcurrent(links, LISTING_CONCURRENCY, (row) => resolveSymlink(fs, absDir, row));
|
|
180
|
+
|
|
181
|
+
// Directories first, then case-insensitive name. The sort runs before the cap,
|
|
182
|
+
// so the rows that survive it are the rows that would have been drawn.
|
|
183
|
+
rows.sort((a, b) => {
|
|
184
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
185
|
+
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const total = rows.length;
|
|
189
|
+
const truncated = total > limit;
|
|
190
|
+
const kept = truncated ? rows.slice(0, limit) : rows;
|
|
191
|
+
|
|
192
|
+
await mapConcurrent(
|
|
193
|
+
kept.filter((row) => !row.isSymlink),
|
|
194
|
+
LISTING_CONCURRENCY,
|
|
195
|
+
async (row) => {
|
|
196
|
+
try {
|
|
197
|
+
const info = await fs.stat(join(absDir, row.name));
|
|
198
|
+
row.size = info.size;
|
|
199
|
+
row.mtimeMs = info.mtimeMs;
|
|
200
|
+
} catch {
|
|
201
|
+
// Directory entry vanished between readdir and stat.
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const entries: DirEntry[] = kept.map((row) => {
|
|
207
|
+
const kind: EntryKind = row.isDir ? "directory" : classifyByName(row.name);
|
|
208
|
+
return {
|
|
209
|
+
name: row.name,
|
|
210
|
+
rel: row.rel,
|
|
211
|
+
isDir: row.isDir,
|
|
212
|
+
isSymlink: row.isSymlink,
|
|
213
|
+
broken: row.broken,
|
|
214
|
+
size: row.size,
|
|
215
|
+
mtimeMs: row.mtimeMs,
|
|
216
|
+
kind,
|
|
217
|
+
isImage: !row.isDir && kind === "image",
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return { entries, total, truncated };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Read a file into a renderable view, honouring the size and line caps. */
|
|
225
|
+
export async function readFileView(
|
|
226
|
+
root: string,
|
|
227
|
+
rel: string,
|
|
228
|
+
maxFileBytes: number,
|
|
229
|
+
maxTextLines: number,
|
|
230
|
+
markdownEnabled = true,
|
|
231
|
+
): Promise<FileView> {
|
|
232
|
+
const abs = toAbs(root, rel);
|
|
233
|
+
let info: Stats;
|
|
234
|
+
try {
|
|
235
|
+
info = await stat(abs);
|
|
236
|
+
} catch {
|
|
237
|
+
throw new HttpError(404, "That file could not be read.");
|
|
238
|
+
}
|
|
239
|
+
if (info.isDirectory()) throw new HttpError(404, "That path is a folder.");
|
|
240
|
+
|
|
241
|
+
const meta: FileMeta = { size: info.size, mtimeMs: info.mtimeMs, mime: contentTypeFor(rel) };
|
|
242
|
+
if (isImageName(rel)) return { kind: "image", ...meta };
|
|
243
|
+
if (info.size > maxFileBytes) return { kind: "tooLarge", ...meta };
|
|
244
|
+
|
|
245
|
+
let buffer: Buffer;
|
|
246
|
+
try {
|
|
247
|
+
buffer = await readFile(abs);
|
|
248
|
+
} catch {
|
|
249
|
+
throw new HttpError(404, "That file could not be read.");
|
|
250
|
+
}
|
|
251
|
+
if (!isProbablyText(buffer)) return { kind: "binary", ...meta };
|
|
252
|
+
|
|
253
|
+
const allLines = buffer.toString("utf-8").split(/\r\n|\r|\n/);
|
|
254
|
+
const truncated = allLines.length > maxTextLines;
|
|
255
|
+
const lines = truncated ? allLines.slice(0, maxTextLines) : allLines;
|
|
256
|
+
const kind = markdownEnabled && isMarkdownName(rel) ? "markdown" : "text";
|
|
257
|
+
if (kind === "markdown") {
|
|
258
|
+
return { kind: "markdown", ...meta, lines, truncated, totalLines: allLines.length };
|
|
259
|
+
}
|
|
260
|
+
return { kind: "text", ...meta, lines, truncated, totalLines: allLines.length };
|
|
261
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { realpathSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
12
|
-
import { isInside } from "
|
|
12
|
+
import { isInside } from "../../../lib/path.ts";
|
|
13
13
|
|
|
14
14
|
/** An HTTP-shaped failure the router turns into a status page. */
|
|
15
15
|
export class HttpError extends Error {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar tree: the current path's ancestors expanded, siblings collapsed.
|
|
3
|
+
*
|
|
4
|
+
* Its rows are the same `DirEntry` a listing produces, so the tree and the listing cannot
|
|
5
|
+
* disagree about what one name is.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import type { EntryKind } from "./classify.ts";
|
|
10
|
+
import { type Listing, listDirectory } from "./list.ts";
|
|
11
|
+
|
|
12
|
+
export interface TreeNode {
|
|
13
|
+
name: string;
|
|
14
|
+
rel: string;
|
|
15
|
+
isDir: boolean;
|
|
16
|
+
isImage: boolean;
|
|
17
|
+
/** True for a symlink whose target cannot be resolved. */
|
|
18
|
+
broken: boolean;
|
|
19
|
+
kind: EntryKind;
|
|
20
|
+
children: TreeNode[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the sidebar tree: the current path's ancestors are expanded so the
|
|
25
|
+
* active item is always visible, and siblings are shown collapsed.
|
|
26
|
+
*/
|
|
27
|
+
export async function buildTree(root: string, currentRel: string, limit: number): Promise<TreeNode> {
|
|
28
|
+
return buildNode(root, "", currentRel, basename(root) || root, limit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A directory page's two views of one directory. */
|
|
32
|
+
export interface DirectoryView {
|
|
33
|
+
/** The current directory's own rows, for the page's listing. */
|
|
34
|
+
listing: Listing;
|
|
35
|
+
/** The sidebar tree, with the current path expanded. */
|
|
36
|
+
tree: TreeNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One walk for a directory page: the listing it draws, and the tree that shows
|
|
41
|
+
* where it sits.
|
|
42
|
+
*
|
|
43
|
+
* `listDirectory` is what decides whether the directory can be read at all, so it
|
|
44
|
+
* runs first and its 404 is the page's error. The walk then reuses that listing
|
|
45
|
+
* for the same directory instead of reading it a second time; the ancestors are
|
|
46
|
+
* still read once each, which is what the tree needs.
|
|
47
|
+
*/
|
|
48
|
+
export async function directoryView(root: string, currentRel: string, limit: number): Promise<DirectoryView> {
|
|
49
|
+
const listing = await listDirectory(root, currentRel, limit);
|
|
50
|
+
const tree = await buildNode(root, "", currentRel, basename(root) || root, limit, { rel: currentRel, listing });
|
|
51
|
+
return { listing, tree };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function buildNode(
|
|
55
|
+
root: string,
|
|
56
|
+
relDir: string,
|
|
57
|
+
currentRel: string,
|
|
58
|
+
label: string,
|
|
59
|
+
limit: number,
|
|
60
|
+
/** A listing the caller already read for this directory, so it is not re-read. */
|
|
61
|
+
known?: { rel: string; listing: Listing },
|
|
62
|
+
): Promise<TreeNode> {
|
|
63
|
+
const node: TreeNode = {
|
|
64
|
+
name: label,
|
|
65
|
+
rel: relDir,
|
|
66
|
+
isDir: true,
|
|
67
|
+
isImage: false,
|
|
68
|
+
broken: false,
|
|
69
|
+
kind: "directory",
|
|
70
|
+
children: [],
|
|
71
|
+
};
|
|
72
|
+
let listing: Listing;
|
|
73
|
+
if (known && known.rel === relDir) {
|
|
74
|
+
listing = known.listing;
|
|
75
|
+
} else {
|
|
76
|
+
try {
|
|
77
|
+
listing = await listDirectory(root, relDir, limit);
|
|
78
|
+
} catch {
|
|
79
|
+
return node;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const entry of listing.entries) {
|
|
83
|
+
const child: TreeNode = {
|
|
84
|
+
name: entry.name,
|
|
85
|
+
rel: entry.rel,
|
|
86
|
+
isDir: entry.isDir,
|
|
87
|
+
isImage: entry.isImage,
|
|
88
|
+
broken: entry.broken,
|
|
89
|
+
kind: entry.kind,
|
|
90
|
+
children: [],
|
|
91
|
+
};
|
|
92
|
+
if (entry.isDir) {
|
|
93
|
+
const onPath = currentRel === entry.rel || currentRel.startsWith(`${entry.rel}/`);
|
|
94
|
+
if (onPath) {
|
|
95
|
+
child.children = (await buildNode(root, entry.rel, currentRel, entry.name, limit, known)).children;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
node.children.push(child);
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createExecRunner,
|
|
14
|
+
currentBranch,
|
|
15
|
+
hasCommits,
|
|
16
|
+
repoRoot,
|
|
17
|
+
revParse,
|
|
18
|
+
type RunGit,
|
|
19
|
+
} from "../../../lib/git/index.ts";
|
|
13
20
|
import {
|
|
14
21
|
diffArgs,
|
|
15
22
|
inMode,
|
|
@@ -18,8 +25,8 @@ import {
|
|
|
18
25
|
type ChangedFile,
|
|
19
26
|
type DiffLine,
|
|
20
27
|
type DiffMode,
|
|
21
|
-
} from "
|
|
22
|
-
import { readFileView } from "
|
|
28
|
+
} from "../text/diff.ts";
|
|
29
|
+
import { readFileView } from "../files/files.ts";
|
|
23
30
|
|
|
24
31
|
export interface GitStatus {
|
|
25
32
|
/** Checked-out branch, when HEAD is attached. */
|
|
@@ -16,11 +16,11 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
16
16
|
import { isExtensionEnabled, resolveEffectiveCwd } from "../../lib/env.ts";
|
|
17
17
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
18
18
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
19
|
-
import { loadConfig, randomPort } from "./config.ts";
|
|
20
|
-
import { createGitDiff, createGitStatus } from "./git.ts";
|
|
19
|
+
import { loadConfig, randomPort } from "./config/config.ts";
|
|
20
|
+
import { createGitDiff, createGitStatus } from "./git/git.ts";
|
|
21
21
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
22
|
-
import { HttpError, resolveRequestPath } from "./paths.ts";
|
|
23
|
-
import { createFileServer, type FileServer } from "./server.ts";
|
|
22
|
+
import { HttpError, resolveRequestPath } from "./files/paths.ts";
|
|
23
|
+
import { createFileServer, type FileServer } from "./transport/server.ts";
|
|
24
24
|
|
|
25
25
|
export interface FileBrowserDeps {
|
|
26
26
|
/** Override the server factory (tests). */
|
|
@@ -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();
|
|
@@ -15,18 +15,17 @@ import {
|
|
|
15
15
|
isLocalHostHeader,
|
|
16
16
|
jsonError,
|
|
17
17
|
jsonResponse,
|
|
18
|
-
pageHeaders,
|
|
19
18
|
parseRequestUrl,
|
|
20
19
|
scriptHeaders,
|
|
21
20
|
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 "
|
|
21
|
+
} from "../../../lib/http/index.ts";
|
|
22
|
+
import { THEME_JS, THEME_JS_PATH } from "../../../lib/web/index.ts";
|
|
23
|
+
import { CLIENT_JS } from "../view/client.ts";
|
|
24
|
+
import type { LivePage } from "../view/chrome.ts";
|
|
25
|
+
import type { ServeConfig } from "../config/config.ts";
|
|
26
|
+
import { isDiffMode, type DiffMode } from "../text/diff.ts";
|
|
27
|
+
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "../git/git.ts";
|
|
28
|
+
import { changeIndexOf, type LiveStatusSource } from "../live/live.ts";
|
|
30
29
|
import {
|
|
31
30
|
buildFileIndex,
|
|
32
31
|
buildTree,
|
|
@@ -38,13 +37,19 @@ import {
|
|
|
38
37
|
type DirEntry,
|
|
39
38
|
type FileIndexProvider,
|
|
40
39
|
type TreeNode,
|
|
41
|
-
} from "
|
|
42
|
-
import { FOLDER_ICON, iconHref, languageOf } from "
|
|
43
|
-
import { fuzzySearch } from "
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
} from "../files/files.ts";
|
|
41
|
+
import { FOLDER_ICON, iconHref, languageOf } from "../view/icons.ts";
|
|
42
|
+
import { fuzzySearch } from "../text/fuzzy.ts";
|
|
43
|
+
import {
|
|
44
|
+
renderDirectoryPage,
|
|
45
|
+
renderDiffPage,
|
|
46
|
+
renderErrorPage,
|
|
47
|
+
renderFileDiffPage,
|
|
48
|
+
renderFilePage,
|
|
49
|
+
} from "../view/html.ts";
|
|
50
|
+
import type { ChangeSummary } from "../view/html.ts";
|
|
51
|
+
import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "../files/paths.ts";
|
|
52
|
+
import { listingRevision } from "../live/rev.ts";
|
|
48
53
|
|
|
49
54
|
export interface ServeContext {
|
|
50
55
|
root: string;
|
|
@@ -373,7 +378,6 @@ export async function handleRequest(
|
|
|
373
378
|
try {
|
|
374
379
|
if (pathname === "/app.js") return { status: 200, headers: scriptHeaders(), body: CLIENT_JS };
|
|
375
380
|
if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
376
|
-
if (pathname === "/favicon.ico") return { status: 204, headers: pageHeaders(), body: "" };
|
|
377
381
|
if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
|
|
378
382
|
if (pathname === "/api/dir-rev") return await dirRevApi(ctx, search.get("path") ?? "");
|
|
379
383
|
if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
|