@gavin-hu/my-pi-agent 0.13.1 → 0.15.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 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/config.ts +3 -2
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +118 -8
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +16 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +3 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +23 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { cleanString, clampInteger, loadConfigSection } from "../../lib/config.ts";
|
|
10
|
+
import { asBoolean, cleanString, clampInteger, loadConfigSection, stringList } from "../../lib/config.ts";
|
|
11
11
|
|
|
12
12
|
export interface WorktreeGuardConfig {
|
|
13
13
|
/** Block write/edit whose resolved path leaves the active worktree. Default: true */
|
|
@@ -70,28 +70,16 @@ export const DEFAULT_CONFIG: WorktreeConfig = {
|
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
function booleanOr(value: unknown, fallback: boolean): boolean {
|
|
74
|
-
return typeof value === "boolean" ? value : fallback;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Keep only trimmed, non-empty strings from an untrusted array. */
|
|
78
|
-
function stringList(value: unknown, fallback: string[]): string[] {
|
|
79
|
-
if (!Array.isArray(value)) return fallback;
|
|
80
|
-
return value
|
|
81
|
-
.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
82
|
-
.map((item) => item.trim());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
73
|
function normalizeGuard(raw: unknown, base: WorktreeGuardConfig): WorktreeGuardConfig {
|
|
86
74
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return base;
|
|
87
75
|
const value = raw as Record<string, unknown>;
|
|
88
76
|
return {
|
|
89
|
-
blockFileEscapes:
|
|
90
|
-
blockReadEscapes:
|
|
91
|
-
blockGitRedirects:
|
|
92
|
-
blockSymlinkEscapes:
|
|
93
|
-
blockUnparsableCommands:
|
|
94
|
-
blockPowershell:
|
|
77
|
+
blockFileEscapes: asBoolean(value.blockFileEscapes, base.blockFileEscapes),
|
|
78
|
+
blockReadEscapes: asBoolean(value.blockReadEscapes, base.blockReadEscapes),
|
|
79
|
+
blockGitRedirects: asBoolean(value.blockGitRedirects, base.blockGitRedirects),
|
|
80
|
+
blockSymlinkEscapes: asBoolean(value.blockSymlinkEscapes, base.blockSymlinkEscapes),
|
|
81
|
+
blockUnparsableCommands: asBoolean(value.blockUnparsableCommands, base.blockUnparsableCommands),
|
|
82
|
+
blockPowershell: asBoolean(value.blockPowershell, base.blockPowershell),
|
|
95
83
|
};
|
|
96
84
|
}
|
|
97
85
|
|
|
@@ -102,7 +90,7 @@ export function normalizeConfig(raw: Record<string, unknown> | undefined, base:
|
|
|
102
90
|
dir: cleanString(raw.dir, base.dir),
|
|
103
91
|
baseRef: raw.baseRef === "head" || raw.baseRef === "fresh" ? raw.baseRef : base.baseRef,
|
|
104
92
|
branchPrefix: cleanString(raw.branchPrefix, base.branchPrefix),
|
|
105
|
-
fetchRemote:
|
|
93
|
+
fetchRemote: asBoolean(raw.fetchRemote, base.fetchRemote),
|
|
106
94
|
fetchTimeoutMs: clampInteger(raw.fetchTimeoutMs, base.fetchTimeoutMs, 0, 600_000),
|
|
107
95
|
onExit: raw.onExit === "ask" || raw.onExit === "keep" || raw.onExit === "remove" ? raw.onExit : base.onExit,
|
|
108
96
|
pruneAfterDays: clampInteger(raw.pruneAfterDays, base.pruneAfterDays, 0, 3650),
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
9
|
+
import { createExecRunner, revParse } from "../../lib/git/index.ts";
|
|
10
|
+
import { isInside, realPathOfNearest } from "../../lib/path.ts";
|
|
10
11
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
11
12
|
import { loadConfig, type WorktreeConfig } from "./config.ts";
|
|
12
13
|
import { WorktreeCancelledError } from "./errors.ts";
|
|
13
14
|
import {
|
|
14
15
|
branchExists,
|
|
15
|
-
canonicalize,
|
|
16
16
|
commitsAhead,
|
|
17
17
|
currentBranch,
|
|
18
18
|
fetchPrRef,
|
|
@@ -95,12 +95,7 @@ export interface EnterOptions {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
async function resolveCommit(pi: ExtensionAPI, dir: string, ref: string): Promise<string | undefined> {
|
|
98
|
-
|
|
99
|
-
const result = await pi.exec("git", ["rev-parse", ref], { cwd: dir });
|
|
100
|
-
return result.code === 0 ? result.stdout.trim() : undefined;
|
|
101
|
-
} catch {
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
98
|
+
return revParse(createExecRunner(pi), dir, ref);
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
/** Create or enter a worktree and rebind the session's effective root. */
|
|
@@ -139,11 +134,11 @@ export async function enterWorktree(
|
|
|
139
134
|
let createdByUs = true;
|
|
140
135
|
|
|
141
136
|
if (pathArg) {
|
|
142
|
-
target =
|
|
143
|
-
if (target ===
|
|
137
|
+
target = realPathOfNearest(resolve(ctx.cwd, pathArg));
|
|
138
|
+
if (target === realPathOfNearest(root)) {
|
|
144
139
|
throw new Error("The main checkout is not a worktree; enter a separate worktree path.");
|
|
145
140
|
}
|
|
146
|
-
const managedRoot =
|
|
141
|
+
const managedRoot = realPathOfNearest(resolve(root, config.dir));
|
|
147
142
|
if (!isInside(managedRoot, target)) {
|
|
148
143
|
if (!ctx.hasUI) throw new Error(`Refusing to enter ${target} outside ${managedRoot} without confirmation.`);
|
|
149
144
|
const ok = await ctx.ui.confirm(
|
|
@@ -163,7 +158,7 @@ export async function enterWorktree(
|
|
|
163
158
|
target = worktreePath(root, config, name);
|
|
164
159
|
// `name` becomes both a directory and a branch, so an absolute name or one
|
|
165
160
|
// with `..` can otherwise place the worktree outside the managed root.
|
|
166
|
-
const managedRoot =
|
|
161
|
+
const managedRoot = realPathOfNearest(resolve(root, config.dir));
|
|
167
162
|
if (!isInside(managedRoot, target)) {
|
|
168
163
|
throw new Error(
|
|
169
164
|
`Refusing to create a worktree outside ${managedRoot}: ${target}. ` +
|
|
@@ -5,19 +5,26 @@
|
|
|
5
5
|
* configuration (and abort/timeout handling) as the rest of Pi.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync,
|
|
9
|
-
import {
|
|
8
|
+
import { existsSync, statSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
12
|
createExecRunner,
|
|
13
13
|
currentBranch as sharedCurrentBranch,
|
|
14
|
+
gitCommonDir,
|
|
14
15
|
hasCommits as sharedHasCommits,
|
|
15
16
|
repoRootFor,
|
|
17
|
+
revParse,
|
|
18
|
+
type RunGitOptions,
|
|
16
19
|
} from "../../lib/git/index.ts";
|
|
20
|
+
import { isInside, realPathOfNearest } from "../../lib/path.ts";
|
|
21
|
+
import { isPidAlive } from "../../lib/spawn.ts";
|
|
17
22
|
import type { WorktreeConfig } from "./config.ts";
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
type GitCallOptions = Omit<RunGitOptions, "cwd">;
|
|
25
|
+
|
|
26
|
+
async function git(pi: ExtensionAPI, args: string[], cwd: string, options: GitCallOptions = {}) {
|
|
27
|
+
return createExecRunner(pi)(args, { cwd, ...options });
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
type GitOutcome = { ok: true; stdout: string } | { ok: false; code: number; error: string };
|
|
@@ -30,46 +37,15 @@ async function gitOutcome(pi: ExtensionAPI, args: string[], cwd: string): Promis
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* The environment is restored afterwards.
|
|
40
|
+
* A network fetch: terminal prompting is disabled through the runner's env seam,
|
|
41
|
+
* so a credentials prompt fails fast instead of hanging until its timeout.
|
|
36
42
|
*/
|
|
37
|
-
|
|
38
|
-
const previous = process.env.GIT_TERMINAL_PROMPT;
|
|
39
|
-
process.env.GIT_TERMINAL_PROMPT = "0";
|
|
40
|
-
try {
|
|
41
|
-
return await fn();
|
|
42
|
-
} finally {
|
|
43
|
-
if (previous === undefined) delete process.env.GIT_TERMINAL_PROMPT;
|
|
44
|
-
else process.env.GIT_TERMINAL_PROMPT = previous;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Canonicalize a path, resolving symlinks. Handles paths that do not exist yet
|
|
50
|
-
* by canonicalizing the nearest existing ancestor, so `/tmp/x` and
|
|
51
|
-
* `/private/tmp/x` compare equal on macOS.
|
|
52
|
-
*/
|
|
53
|
-
export function canonicalize(path: string): string {
|
|
54
|
-
const absolute = resolve(path);
|
|
55
|
-
try {
|
|
56
|
-
// `.native` resolves Windows 8.3 short names (e.g. HSPCAD~1) to their
|
|
57
|
-
// long form, so a temp path from os.tmpdir() compares equal to git's
|
|
58
|
-
// expanded output. It is equivalent to realpathSync on other platforms.
|
|
59
|
-
return realpathSync.native(absolute);
|
|
60
|
-
} catch {
|
|
61
|
-
try {
|
|
62
|
-
return join(realpathSync.native(dirname(absolute)), basename(absolute));
|
|
63
|
-
} catch {
|
|
64
|
-
return absolute;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
43
|
+
const NO_TERMINAL_PROMPT: GitCallOptions = { env: { GIT_TERMINAL_PROMPT: "0" } };
|
|
68
44
|
|
|
69
45
|
/** Absolute path of the repository root containing `cwd`, or undefined when not a git repo. */
|
|
70
46
|
export async function repoRoot(pi: ExtensionAPI, cwd: string): Promise<string | undefined> {
|
|
71
47
|
const root = await repoRootFor(pi, cwd);
|
|
72
|
-
return root ?
|
|
48
|
+
return root ? realPathOfNearest(root) : undefined;
|
|
73
49
|
}
|
|
74
50
|
|
|
75
51
|
/** Whether the repository has at least one commit (a worktree needs a commit to branch from). */
|
|
@@ -101,11 +77,13 @@ export async function resolveBaseRef(
|
|
|
101
77
|
if (branch) {
|
|
102
78
|
if (config.fetchRemote && (await fetchStale(pi, repoRoot))) {
|
|
103
79
|
// Never block on credentials: treat an error or timeout as "keep using the cache".
|
|
104
|
-
await
|
|
80
|
+
await git(pi, ["fetch", "--no-tags", "origin", branch], repoRoot, {
|
|
81
|
+
...NO_TERMINAL_PROMPT,
|
|
82
|
+
timeoutMs: config.fetchTimeoutMs,
|
|
83
|
+
});
|
|
105
84
|
}
|
|
106
85
|
const remoteRef = `origin/${branch}`;
|
|
107
|
-
|
|
108
|
-
if (exists.code === 0 && exists.stdout.trim()) return { ref: remoteRef, mode: "fresh" };
|
|
86
|
+
if (await refExists(pi, repoRoot, remoteRef)) return { ref: remoteRef, mode: "fresh" };
|
|
109
87
|
}
|
|
110
88
|
return { ref: "HEAD", mode: "head" };
|
|
111
89
|
}
|
|
@@ -121,9 +99,9 @@ const FETCH_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
|
121
99
|
|
|
122
100
|
/** Whether the repository has not been fetched in the last 24 hours. */
|
|
123
101
|
export async function fetchStale(pi: ExtensionAPI, repoRoot: string, maxAgeMs = FETCH_MAX_AGE_MS): Promise<boolean> {
|
|
124
|
-
const common = await
|
|
125
|
-
if (common
|
|
126
|
-
const fetchHead = resolve(common
|
|
102
|
+
const common = await gitCommonDir(createExecRunner(pi), repoRoot);
|
|
103
|
+
if (common === undefined) return true;
|
|
104
|
+
const fetchHead = resolve(common, "FETCH_HEAD");
|
|
127
105
|
try {
|
|
128
106
|
return Date.now() - statSync(fetchHead).mtimeMs > maxAgeMs;
|
|
129
107
|
} catch {
|
|
@@ -170,7 +148,10 @@ export async function fetchPrRef(
|
|
|
170
148
|
? [`merge-requests/${pr.number}/head`]
|
|
171
149
|
: [`pull/${pr.number}/head`, `merge-requests/${pr.number}/head`];
|
|
172
150
|
for (const spec of specs) {
|
|
173
|
-
const result = await
|
|
151
|
+
const result = await git(pi, ["fetch", "--no-tags", "origin", spec], repoRoot, {
|
|
152
|
+
...NO_TERMINAL_PROMPT,
|
|
153
|
+
timeoutMs,
|
|
154
|
+
});
|
|
174
155
|
if (result.code === 0) return { ok: true };
|
|
175
156
|
}
|
|
176
157
|
return { ok: false, error: `Failed to fetch PR/MR #${pr.number}` };
|
|
@@ -178,16 +159,16 @@ export async function fetchPrRef(
|
|
|
178
159
|
|
|
179
160
|
/** Path of a managed worktree directory from its name. */
|
|
180
161
|
export function worktreePath(repoRoot: string, config: WorktreeConfig, name: string): string {
|
|
181
|
-
return
|
|
162
|
+
return realPathOfNearest(resolve(repoRoot, config.dir, name));
|
|
182
163
|
}
|
|
183
164
|
|
|
184
165
|
export async function worktreeExists(pi: ExtensionAPI, dir: string): Promise<boolean> {
|
|
185
|
-
const want =
|
|
166
|
+
const want = realPathOfNearest(dir);
|
|
186
167
|
const result = await git(pi, ["worktree", "list", "--porcelain"], dir);
|
|
187
168
|
if (result.code !== 0) return false;
|
|
188
169
|
return result.stdout
|
|
189
170
|
.split("\n")
|
|
190
|
-
.some((line) => line.startsWith("worktree ") &&
|
|
171
|
+
.some((line) => line.startsWith("worktree ") && realPathOfNearest(line.slice("worktree ".length).trim()) === want);
|
|
191
172
|
}
|
|
192
173
|
|
|
193
174
|
/** Whether a local branch exists. */
|
|
@@ -206,8 +187,7 @@ export async function commitsAheadOf(pi: ExtensionAPI, dir: string, branch: stri
|
|
|
206
187
|
|
|
207
188
|
/** Whether `ref` resolves here: a branch, a remote-tracking ref, or a tag. */
|
|
208
189
|
export async function refExists(pi: ExtensionAPI, dir: string, ref: string): Promise<boolean> {
|
|
209
|
-
|
|
210
|
-
return result.code === 0 && result.stdout.trim() !== "";
|
|
190
|
+
return (await revParse(createExecRunner(pi), dir, ref)) !== undefined;
|
|
211
191
|
}
|
|
212
192
|
|
|
213
193
|
export async function branchExists(pi: ExtensionAPI, repoRoot: string, branch: string): Promise<boolean> {
|
|
@@ -317,15 +297,15 @@ export async function checkCheckout(pi: ExtensionAPI, dir: string, mainRoot: str
|
|
|
317
297
|
if (!top) {
|
|
318
298
|
return { ok: false, reason: "unsafe", detail: `${dir} is not a git worktree.` };
|
|
319
299
|
}
|
|
320
|
-
if (
|
|
300
|
+
if (realPathOfNearest(top) === realPathOfNearest(mainRoot)) {
|
|
321
301
|
return {
|
|
322
302
|
ok: false,
|
|
323
303
|
reason: "unsafe",
|
|
324
304
|
detail: `${dir} resolves its working tree to the main checkout ${mainRoot}.`,
|
|
325
305
|
};
|
|
326
306
|
}
|
|
327
|
-
const commonDir = await
|
|
328
|
-
if (commonDir
|
|
307
|
+
const commonDir = await gitCommonDir(createExecRunner(pi), dir);
|
|
308
|
+
if (commonDir === undefined) {
|
|
329
309
|
return { ok: false, reason: "unverified", detail: `Could not read git metadata for ${dir}.` };
|
|
330
310
|
}
|
|
331
311
|
return { ok: true };
|
|
@@ -390,13 +370,13 @@ export async function listManagedWorktrees(
|
|
|
390
370
|
): Promise<ManagedWorktree[]> {
|
|
391
371
|
const result = await git(pi, ["worktree", "list", "--porcelain"], repoRoot);
|
|
392
372
|
if (result.code !== 0) return [];
|
|
393
|
-
const managedRoot =
|
|
373
|
+
const managedRoot = realPathOfNearest(resolve(repoRoot, config.dir));
|
|
394
374
|
const entries: ManagedWorktree[] = [];
|
|
395
375
|
let current: ManagedWorktree = { path: "" };
|
|
396
376
|
const flush = () => {
|
|
397
377
|
if (!current.path) return;
|
|
398
|
-
const path =
|
|
399
|
-
if (
|
|
378
|
+
const path = realPathOfNearest(current.path);
|
|
379
|
+
if (isInside(managedRoot, path)) {
|
|
400
380
|
entries.push({ ...current, path });
|
|
401
381
|
}
|
|
402
382
|
current = { path: "" };
|
|
@@ -433,17 +413,6 @@ export async function unlockWorktree(pi: ExtensionAPI, repoRoot: string, dir: st
|
|
|
433
413
|
await git(pi, ["worktree", "unlock", dir], repoRoot);
|
|
434
414
|
}
|
|
435
415
|
|
|
436
|
-
/** Whether a process id is alive on this host. */
|
|
437
|
-
export function isProcessAlive(pid: number): boolean {
|
|
438
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
439
|
-
try {
|
|
440
|
-
process.kill(pid, 0);
|
|
441
|
-
return true;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
416
|
/** Owning pid from a `pi:<pid>:<session>` lock reason, when present. */
|
|
448
417
|
export function lockOwnerPid(reason: string): number | undefined {
|
|
449
418
|
const match = reason.match(/^pi:(\d+):/);
|
|
@@ -453,5 +422,5 @@ export function lockOwnerPid(reason: string): number | undefined {
|
|
|
453
422
|
/** Whether a lock reason names a `pi` process that is no longer alive. */
|
|
454
423
|
export function isStaleLock(reason: string): boolean {
|
|
455
424
|
const pid = lockOwnerPid(reason);
|
|
456
|
-
return pid !== undefined && !
|
|
425
|
+
return pid !== undefined && !isPidAlive(pid);
|
|
457
426
|
}
|
|
@@ -24,8 +24,9 @@ import { existsSync } from "node:fs";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
27
|
+
import { realPathOfNearest } from "../../lib/path.ts";
|
|
27
28
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
28
|
-
import {
|
|
29
|
+
import { checkCheckout, repoRoot, unlockWorktree } from "./git.ts";
|
|
29
30
|
import { analyzeBashCommand, guardFileTool, guardPowershellTool, isUnverifiable } from "./guard.ts";
|
|
30
31
|
import {
|
|
31
32
|
enterWorktree,
|
|
@@ -100,9 +101,9 @@ export function registerWorktree(pi: ExtensionAPI, { entryPath, createBuiltin }:
|
|
|
100
101
|
// factory inside the parent, which must restore its own recorded state.
|
|
101
102
|
const envRoot = event.reason === "startup" ? process.env[ENV_ROOT] : undefined;
|
|
102
103
|
if (envRoot) {
|
|
103
|
-
const borrowedPath =
|
|
104
|
+
const borrowedPath = realPathOfNearest(envRoot);
|
|
104
105
|
if (existsSync(borrowedPath) && (await repoRoot(pi, borrowedPath))) {
|
|
105
|
-
const borrowedMain = process.env[ENV_MAIN] ?
|
|
106
|
+
const borrowedMain = process.env[ENV_MAIN] ? realPathOfNearest(process.env[ENV_MAIN]) : borrowedPath;
|
|
106
107
|
const borrowed: WorktreeState = {
|
|
107
108
|
active: true,
|
|
108
109
|
borrowed: true,
|
|
@@ -229,5 +230,5 @@ export function registerWorktree(pi: ExtensionAPI, { entryPath, createBuiltin }:
|
|
|
229
230
|
/** Pi extension entrypoint. */
|
|
230
231
|
export default function worktreeExtension(pi: ExtensionAPI): void {
|
|
231
232
|
if (!isExtensionEnabled("worktree")) return;
|
|
232
|
-
registerWorktree(pi, { entryPath:
|
|
233
|
+
registerWorktree(pi, { entryPath: realPathOfNearest(fileURLToPath(import.meta.url)) });
|
|
233
234
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { realPathOfNearest } from "../../lib/path.ts";
|
|
16
17
|
import type { WorktreeConfig } from "./config.ts";
|
|
17
18
|
import {
|
|
18
|
-
canonicalize,
|
|
19
19
|
commitsAhead,
|
|
20
20
|
commitsBehind,
|
|
21
21
|
isBranchMerged,
|
|
@@ -102,7 +102,7 @@ function saveRegistry(repoRoot: string, config: WorktreeConfig, registry: Worktr
|
|
|
102
102
|
/** Insert or replace a record, preserving an existing `createdAt`. */
|
|
103
103
|
export function upsertRecord(repoRoot: string, config: WorktreeConfig, record: WorktreeRecord): void {
|
|
104
104
|
const registry = loadRegistry(repoRoot, config);
|
|
105
|
-
const path =
|
|
105
|
+
const path = realPathOfNearest(record.path);
|
|
106
106
|
const existing = registry.worktrees.find((candidate) => candidate.path === path);
|
|
107
107
|
const next: WorktreeRecord = existing
|
|
108
108
|
? { ...record, path, createdAt: existing.createdAt || record.createdAt }
|
|
@@ -116,7 +116,7 @@ export function upsertRecord(repoRoot: string, config: WorktreeConfig, record: W
|
|
|
116
116
|
/** Mark a worktree as just used. No-op when it is not registered. */
|
|
117
117
|
export function touchRecord(repoRoot: string, config: WorktreeConfig, path: string, at = Date.now()): void {
|
|
118
118
|
const registry = loadRegistry(repoRoot, config);
|
|
119
|
-
const wanted =
|
|
119
|
+
const wanted = realPathOfNearest(path);
|
|
120
120
|
const record = registry.worktrees.find((candidate) => candidate.path === wanted);
|
|
121
121
|
if (!record) return;
|
|
122
122
|
record.lastUsedAt = at;
|
|
@@ -125,7 +125,7 @@ export function touchRecord(repoRoot: string, config: WorktreeConfig, path: stri
|
|
|
125
125
|
|
|
126
126
|
export function removeRecord(repoRoot: string, config: WorktreeConfig, path: string): void {
|
|
127
127
|
const registry = loadRegistry(repoRoot, config);
|
|
128
|
-
const wanted =
|
|
128
|
+
const wanted = realPathOfNearest(path);
|
|
129
129
|
const next = registry.worktrees.filter((candidate) => candidate.path !== wanted);
|
|
130
130
|
if (next.length === registry.worktrees.length) return;
|
|
131
131
|
registry.worktrees = next;
|
|
@@ -145,7 +145,7 @@ export function reconcileRegistry(
|
|
|
145
145
|
options: { persist?: boolean } = {},
|
|
146
146
|
): { registry: WorktreeRegistry; changed: boolean } {
|
|
147
147
|
const registry = loadRegistry(repoRoot, config);
|
|
148
|
-
const live = new Set(managed.map((entry) =>
|
|
148
|
+
const live = new Set(managed.map((entry) => realPathOfNearest(entry.path)));
|
|
149
149
|
const byPath = new Map(registry.worktrees.map((record) => [record.path, record] as const));
|
|
150
150
|
|
|
151
151
|
let changed = false;
|
|
@@ -158,7 +158,7 @@ export function reconcileRegistry(
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
for (const entry of managed) {
|
|
161
|
-
const path =
|
|
161
|
+
const path = realPathOfNearest(entry.path);
|
|
162
162
|
if (byPath.has(path)) continue;
|
|
163
163
|
next.push({
|
|
164
164
|
path,
|
|
@@ -189,7 +189,7 @@ export async function statusOf(
|
|
|
189
189
|
entry: ManagedWorktree,
|
|
190
190
|
options: StatusOptions = {},
|
|
191
191
|
): Promise<CheckoutStatus> {
|
|
192
|
-
const path =
|
|
192
|
+
const path = realPathOfNearest(entry.path);
|
|
193
193
|
const locked = entry.locked !== undefined;
|
|
194
194
|
const staleLock = locked && entry.locked ? isStaleLock(entry.locked) : false;
|
|
195
195
|
const base: CheckoutStatus = {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
createReadToolDefinition,
|
|
31
31
|
createWriteToolDefinition,
|
|
32
32
|
} from "@earendil-works/pi-coding-agent";
|
|
33
|
-
import {
|
|
33
|
+
import { realPathOfNearest } from "../../lib/path.ts";
|
|
34
34
|
|
|
35
35
|
export const ROOT_TOOL_NAMES = ["read", "write", "edit", "bash", "grep", "find", "ls"] as const;
|
|
36
36
|
export type RootToolName = (typeof ROOT_TOOL_NAMES)[number];
|
|
@@ -155,7 +155,7 @@ export function findInactiveOverrides(pi: ExtensionAPI, self: string | undefined
|
|
|
155
155
|
const info = pi.getAllTools().find((entry) => entry.name === name);
|
|
156
156
|
if (!info) continue;
|
|
157
157
|
const path = info.sourceInfo?.path;
|
|
158
|
-
const isOurs = path && !path.startsWith("builtin:") && (!self ||
|
|
158
|
+
const isOurs = path && !path.startsWith("builtin:") && (!self || realPathOfNearest(path) === self);
|
|
159
159
|
if (!isOurs) inactive.push(name);
|
|
160
160
|
}
|
|
161
161
|
return inactive;
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { realPathOfNearest } from "../../lib/path.ts";
|
|
8
9
|
import { loadConfig } from "./config.ts";
|
|
9
10
|
import { gapLines, isolationGaps } from "./gaps.ts";
|
|
10
|
-
import {
|
|
11
|
+
import { type CheckoutCheck, defaultBranch, listManagedWorktrees, repoRoot } from "./git.ts";
|
|
11
12
|
import { reconcileRegistry, statusOf, type CheckoutStatus } from "./registry.ts";
|
|
12
13
|
import { configFor, getActive, getInactiveOverrides, getUnverifiableCommands } from "./runtime.ts";
|
|
13
14
|
import type { WorktreeState } from "./state.ts";
|
|
@@ -116,7 +117,7 @@ export async function worktreeStatus(pi: ExtensionAPI, ctx: ExtensionContext): P
|
|
|
116
117
|
const baseRef = baseBranch ? `origin/${baseBranch}` : undefined;
|
|
117
118
|
lines.push("", `Managed worktrees (${managed.length}):`);
|
|
118
119
|
for (const entry of managed) {
|
|
119
|
-
const record = registry.worktrees.find((candidate) => candidate.path ===
|
|
120
|
+
const record = registry.worktrees.find((candidate) => candidate.path === realPathOfNearest(entry.path));
|
|
120
121
|
const status = await statusOf(pi, entry, { baseRef, record });
|
|
121
122
|
const marks = statusMarks(status, current?.path === entry.path);
|
|
122
123
|
lines.push(
|
package/lib/README.md
CHANGED
|
@@ -18,26 +18,35 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
18
18
|
| Module | Responsibility |
|
|
19
19
|
|---|---|
|
|
20
20
|
| [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
|
|
21
|
-
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` (the
|
|
21
|
+
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` / `writeConfigValuesAt` / `unsetConfigValueAt` (the leaf writers preserve siblings and metadata, `writeConfigValuesAt` applies several leaves in one document pass, and the last removes one and prunes what it emptied). Includes `clampInteger` / `clampNumber` / `cleanString` / `optionalString` / `asBoolean` / `stringList` / `asRecord` / `readJson` / `readJsonResult`. |
|
|
22
22
|
| [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
|
|
23
23
|
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
|
|
24
24
|
| [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` / `JobFinishedEvent` (published by `job`, consumed by bytefly's notify capability), `SUBAGENT_FINISHED_CHANNEL` / `SubagentFinishedEvent` (published by `subagent`, consumed by bytefly's footer roll-up), and `WEB_SEARCH_PROVIDER_CHANNEL` / `WebSearchProviderEvent` (published by `web-access` at session start, consumed by bytefly's doctor so it can report the resolved provider without importing the extension — the key itself is never in the payload). |
|
|
25
25
|
| [`notice.ts`](./notice.ts) | The shared transcript notice: `NOTICE_TYPE`, the `NoticeDetails` / `NoticeItem` shape, `noticeMessage` (the `display: true` message both send paths accept), `isNotice`, and `pruneStaleNotices` — posted by `job` and `watch`, rendered by bytefly. |
|
|
26
|
-
| [`
|
|
26
|
+
| [`report.ts`](./report.ts) | The pending-report coordinator: `createPendingReporter` with `wake` / `pending` / `markReported` / `buildNotice` / `source` / `onWoke`, returning the lifecycle handlers that report at the next turn, raise at most one triggered turn per wake window, and prune only their own notices. Shared by `job` and `watch`. |
|
|
27
|
+
| [`errors.ts`](./errors.ts) | `messageOf`: the one boundary that turns an unknown thrown value into a user-facing line (an `Error` yields its message, anything else is stringified). |
|
|
28
|
+
| [`format.ts`](./format.ts) | Pure text/number formatting shared by renderers: `formatTokens`, `formatCount`, `formatDuration` (`450ms`, `3.2s`, `1m05s`, `2h03m`), `boundedTail` (the end of a joined tail with a `truncated` flag), `stripControlChars`, `sanitize`, `sanitizeLine` — the boundary for untrusted, single-line text — `lineOf` (a value read as one sanitized line), `sanitizeUntrustedLine` / `sanitizeUntrustedText` with `MAX_OUTPUT_LINE` for **program output** (OSC/CSI escapes removed whole, `\r` progress resolved, clipped), and `shortenPath`. |
|
|
29
|
+
| [`lines.ts`](./lines.ts) | `LineSplitter`, the byte-safe splitter for a stream of program output: buffers bytes and decodes only at a newline, so a UTF-8 character split across two reads survives, and caps an over-long line instead of dropping it. Shared by `job`, `watch`, `check`, and `subagent`. |
|
|
27
30
|
| [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote transport marks on the UI context it owns (`INTERACTION_TURN_KEY`), the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file, fallback text, and digest preface) plus the shared “Other” label. |
|
|
28
|
-
| [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
31
|
+
| [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `gitCommonDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
29
32
|
| [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
|
|
30
|
-
| [`
|
|
31
|
-
| [`
|
|
33
|
+
| [`http/`](./http/) | The loopback HTTP layer shared by `file-browse` and `plan`: [`http/headers.ts`](./http/headers.ts) (the page policy `PAGE_CSP` and the `pageHeaders` / `scriptHeaders` / `jsonHeaders` sets), [`http/response.ts`](./http/response.ts) (`ServeResponse` — a buffered body, a `filePath` stream, or an SSE `stream` — plus `htmlResponse` / `jsonResponse` / `jsonError` / `textResponse`), [`http/router.ts`](./http/router.ts) (`parseRequestUrl`, and `isLocalHostHeader` with the `required` mode each server chooses), [`http/server.ts`](./http/server.ts) (`bindLoopback` on `127.0.0.1` with the accept-error listener, `respond` for HEAD / streamed file / body, and the idempotent `createCloser`), and the [`http/index.ts`](./http/index.ts) barrel. |
|
|
34
|
+
| [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface, whose conventions are its own doc, [`tui/README.md`](./tui/README.md). Modules: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame) with `detailBlockRows` (the rows its detail block costs), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `statusRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`, `byRunningFirst` for a live list's order), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`, `clipMiddle`, `labelLine` / `clipLabel` with `LABEL_WIDTH`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job`, `watch`, and `subagent` share, with the attached-context `notify` and a `detach` that clears its keys), [`tui/dock-screen.ts`](./tui/dock-screen.ts) (`DockListScreen`, the two-mode docked list + detail-pane screen with navigation, wheel, range, and confirm), [`tui/confirm.ts`](./tui/confirm.ts) (`ConfirmState`, the y/N layer behind every destructive screen action), [`tui/tool.ts`](./tui/tool.ts) (`toolCallTitle`, the width-safe `ToolCallText` / `reuseCallText`, sanitized `errorLine` / `rejectionLine`, `moreHint` / `overflowHint` / `earlierHint`, the `TailResult` transcript block, `syncElapsedTimer`, `elapsedSuffix` — the tool-transcript kit), [`tui/render-cache.ts`](./tui/render-cache.ts) (`RenderCache`: the per-instance line memo a component that derives its lines from another component's output renders through, so a repeat frame is one comparison), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
|
|
35
|
+
| [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, doc, and plan guards share, and the canonical-path helper the worktree registry keys records by. |
|
|
36
|
+
| [`pdf.ts`](./pdf.ts) | The optional `unpdf` seam shared by `web-access` and `document`: `pdfGuidance`, `PdfUnavailableError`, `pdfTextFromResult`, and the lazy `loadPdfExtractor` / `extractPdfText` (an injected extractor wins), so a missing package and a merged-pages result read the same way through either surface. |
|
|
32
37
|
| [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
|
|
33
38
|
| [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
|
|
39
|
+
| [`plan-state.ts`](./plan-state.ts) | The plan-mode flag replayed from the session branch: `PLAN_MODE_ENTRY_TYPE` and `isPlanModeEnabled(entries)`, so `rewind` can refuse a write while planning without importing the plan extension. |
|
|
40
|
+
| [`plan-entry.ts`](./plan-entry.ts) | Plan-mode entry shared by the extension that owns the mode and the one that offers it: `READ_ONLY_SUMMARY` and `PLAN_CONSENT` (one wording for the read-only guarantee), the `PLAN_ENTER_REQUEST` channel with its `PlanEnterRequest` payload, and `requestPlanMode(pi, ctx)` — a request/response over `pi.events` that resolves `false` instead of hanging when `plan` is not loaded, so a command context (which has no `executeTool`) can still enter the mode after asking the user once. |
|
|
34
41
|
| [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
|
|
35
42
|
| [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports, the published search provider, env, and a refresh-only reachability probe): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. A check kind is either a prerequisite (`binary`, `module`, `state`, `invocation`, `notify`, `provider`) or a `safety` switch whose off value trades a protection away, read from `bytefly.json` global-then-project. |
|
|
36
43
|
| [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
|
|
37
|
-
| [`
|
|
38
|
-
| [`
|
|
44
|
+
| [`spawn.ts`](./spawn.ts) | The process seam for extensions that run a shell command: `SpawnedProcess`, `SpawnFn`, `KillTreeFn`, the pure `spawnOptions` / `killPlan`, `applyKillPlan` over injected seams, `isPidAlive`, and the `defaultSpawn` / `defaultKillTree` that execute them (process group on POSIX, `taskkill /T` on Windows). Shared by `job`, `watch`, `check`, `worktree`, and `wechat`. |
|
|
45
|
+
| [`goal-state.ts`](./goal-state.ts) | The published `goal` state replayed from the session branch: `PublishedGoal`, `PublishedGoalStatus` (`planning` / `active` / `achieved`), `GOAL_ENTRY_TYPE`, `MAX_GOAL_OBJECTIVE`, `isPursuedGoal(goal)` and `lastGoal(entries)` (the last valid goal, skipping a rejected call and honouring an explicit clear), so `plan` can restate a pursued goal while planning, promote a `planning` one at approval, and offer to record one when a plan is approved with none, and `goal` itself reads the branch through the same replay — with no extension importing another's modules. |
|
|
46
|
+
| [`todo-state.ts`](./todo-state.ts) | The todo item contract, its parser, and the published state replayed from the session branch: `TODO_STATUSES` / `TodoStatus`, `ExistingTodo`, `MAX_TODOS` / `MAX_CONTENT`, `normalizeTodos`, and `lastTodos(entries)` (the last write that fully validates, skipping a rejected call; a malformed write leaves the previous list). `plan` merges steps into that list, `goal` counts the items still open, and `todo` itself replays through it — so one rule decides which writes are state writes, with no extension importing another's modules. |
|
|
39
47
|
| [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
|
|
40
|
-
| [`
|
|
48
|
+
| [`tool-result.ts`](./tool-result.ts) | The rejected-tool-call shape: `toolError` builds the `isError: true` content plus the caller's `details`, and `errorResult` is the action-and-message form `job` and `watch` share. |
|
|
49
|
+
| [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts) (the `bf-*` kit, including the dialog, shortcut-sheet, and `bf-kbd` rules), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`, and `keysDialogScript(triggerId)` — the shared shortcut-sheet wiring, whose contract is `window.bfKeys`), [`web/markup.ts`](./web/markup.ts) (the HTML atoms, including `keysSheet` and its `KeyHint` rows), and the [`web/index.ts`](./web/index.ts) barrel. |
|
|
41
50
|
|
|
42
51
|
## Conventions
|
|
43
52
|
|
|
@@ -50,6 +59,11 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
50
59
|
glyph and the `setStatus` key are one contract, not two literals. Tool names
|
|
51
60
|
shared by an orchestrating tool and the tool it calls — and parameter names
|
|
52
61
|
one extension probes on another — live in `tool-names.ts`.
|
|
62
|
+
- **A helper that exists here must be imported, not redefined.**
|
|
63
|
+
`test/shared-helpers.test.ts` fails a top-level extension declaration whose
|
|
64
|
+
name is also exported by `lib/`, and a body copied from `lib/` or from another
|
|
65
|
+
extension. A deliberate wrapper goes on that test's reasoned exception list; a
|
|
66
|
+
copy belongs here with one co-located test.
|
|
53
67
|
- **A color is a role, not a token.** The rules and the primitives are in
|
|
54
68
|
[`tui/README.md`](./tui/README.md#color-is-a-role): draw through
|
|
55
69
|
[`tui/tokens.ts`](./tui/tokens.ts) (`paint(theme, role, text)`) so a theme swap
|