@gavin-hu/my-pi-agent 0.15.0 → 0.16.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 +12 -11
- package/README.zh-CN.md +9 -7
- package/extensions/bytefly/README.md +98 -116
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/format.ts +3 -8
- package/extensions/bytefly/panel/join.ts +1 -6
- package/extensions/bytefly/panel/settings.ts +74 -377
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
- package/extensions/bytefly/panel/tui/frame.ts +2 -2
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui.ts +4 -6
- package/extensions/bytefly/picker/resources.ts +7 -3
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/context/README.md +162 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +82 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +319 -0
- package/extensions/context/tui.ts +268 -0
- package/extensions/context/types.ts +122 -0
- package/extensions/file-browse/README.md +83 -8
- package/extensions/file-browse/client.ts +3 -1
- package/extensions/file-browse/config.ts +23 -0
- package/extensions/file-browse/files.ts +183 -48
- package/extensions/file-browse/fuzzy.ts +65 -11
- package/extensions/file-browse/html.ts +78 -133
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +158 -0
- package/extensions/file-browse/live.ts +256 -0
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +33 -8
- package/extensions/file-browse/server.ts +78 -13
- package/extensions/file-browse/styles.ts +124 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +72 -56
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +3 -3
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +10 -14
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +7 -26
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +6 -14
- package/extensions/plan/tools/enter.ts +6 -7
- package/extensions/plan/tools/exit.ts +6 -7
- package/extensions/plan/tools/steps.ts +6 -7
- package/extensions/plan/tools/write.ts +8 -9
- package/extensions/plan/web-router.ts +2 -12
- package/extensions/plan/web.ts +30 -15
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +10 -3
- package/extensions/rewind/git.ts +27 -14
- package/extensions/rewind/hunk-picker.ts +1 -1
- package/extensions/rewind/path-picker.ts +1 -1
- package/extensions/rewind/review-tui.ts +3 -3
- package/extensions/rewind/snapshot.ts +13 -4
- package/extensions/subagent/background-tui.ts +1 -1
- package/extensions/subagent/background.ts +1 -7
- package/extensions/todo/tools.ts +10 -14
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +7 -18
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +4 -7
- package/extensions/worktree/status.ts +14 -9
- package/lib/README.md +12 -8
- package/lib/escape.ts +15 -2
- package/lib/git/read.ts +20 -0
- package/lib/http/headers.ts +20 -0
- package/lib/http/server.ts +22 -4
- package/lib/pdf.ts +2 -2
- package/lib/pool.ts +62 -0
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tui/README.md +3 -3
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +12 -4
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/package.json +8 -4
- package/skills/changelog/SKILL.md +136 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* touched: all staging happens in a temporary index.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
commitTree,
|
|
13
|
+
currentBranch,
|
|
14
|
+
treeFromWorkingTree,
|
|
15
|
+
updateRef,
|
|
16
|
+
GitError,
|
|
17
|
+
revParseMany,
|
|
18
|
+
type RunGit,
|
|
19
|
+
} from "./git.ts";
|
|
12
20
|
import { encodeMessage, refFor, META_VERSION, type SnapshotMeta } from "./store.ts";
|
|
13
21
|
import type { Snapshot, SnapshotReason } from "./types.ts";
|
|
14
22
|
|
|
@@ -44,11 +52,12 @@ function defaultId(now: number): string {
|
|
|
44
52
|
/** Point a ref at the current working tree and return the recorded snapshot. */
|
|
45
53
|
export async function createSnapshot(deps: SnapshotDeps, input: SnapshotInput): Promise<Snapshot> {
|
|
46
54
|
const { runGit } = deps;
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
// One `rev-parse` for both ids: the snapshot needs HEAD to parent the commit
|
|
56
|
+
// and HEAD's tree to know whether the working tree is clean.
|
|
57
|
+
const [head, headTree] = await revParseMany(runGit, input.root, ["HEAD", "HEAD^{tree}"]);
|
|
58
|
+
if (!head || !headTree) throw new GitError("the repository has no commits to snapshot.", 1);
|
|
49
59
|
|
|
50
60
|
const tree = await treeFromWorkingTree(runGit, input.root, input.indexFile, input.includeUntracked);
|
|
51
|
-
const headTree = await revParse(runGit, input.root, "HEAD^{tree}");
|
|
52
61
|
const clean = tree === headTree;
|
|
53
62
|
|
|
54
63
|
const now = deps.now?.() ?? Date.now();
|
|
@@ -161,7 +161,7 @@ export class AgentsBoardComponent extends DockListScreen<BackgroundRun> {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
protected listHints(): ScreenHint[] {
|
|
164
|
-
const focused = this.
|
|
164
|
+
const focused = this.focusedRow;
|
|
165
165
|
const hints: ScreenHint[] = ["Esc close", { text: "Enter detail", color: "accent" }];
|
|
166
166
|
if (focused && canCollect(focused)) hints.push({ text: "c collect", color: "accent" });
|
|
167
167
|
if (focused?.status === "running") hints.push({ text: "x cancel", color: "warning" });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { sanitizeLine, stripControlChars } from "../../lib/format.ts";
|
|
13
13
|
import type { AgentConfig } from "./agents.ts";
|
|
14
14
|
import { formatUsageStats } from "./format.ts";
|
|
15
15
|
import type { RunOptions } from "./run.ts";
|
|
@@ -458,12 +458,6 @@ export function createBackgroundRuntime(deps: BackgroundDeps): BackgroundRuntime
|
|
|
458
458
|
return { start, list, collect, cancel, shutdown, reset, subscribe };
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
/** A human duration for a run; kept here so the board and collect agree. */
|
|
462
|
-
export function backgroundDuration(run: BackgroundRun, now = Date.now()): string {
|
|
463
|
-
const end = run.finishedAt ?? now;
|
|
464
|
-
return formatDuration(Math.max(0, end - run.startedAt));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
461
|
/** Whether a run can be collected. */
|
|
468
462
|
export function canCollect(run: BackgroundRun): boolean {
|
|
469
463
|
return run.status !== "running" && run.status !== "interrupted";
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
15
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
17
16
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
18
17
|
import { normalizeTodos } from "../../lib/todo-state.ts";
|
|
18
|
+
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
19
19
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
20
|
-
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
21
20
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
22
21
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
23
22
|
import type { TodoRuntime } from "./runtime.ts";
|
|
@@ -98,24 +97,21 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
98
97
|
},
|
|
99
98
|
|
|
100
99
|
renderCall(args, theme, context) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
104
|
-
paint(theme, "meta", formatCallText(args.todos as Todo[] | undefined, context.argsComplete)),
|
|
105
|
-
);
|
|
106
|
-
return text;
|
|
100
|
+
const body = formatCallText(args.todos as Todo[] | undefined, context.argsComplete);
|
|
101
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, body);
|
|
107
102
|
},
|
|
108
103
|
|
|
109
104
|
renderResult(result, { expanded }, theme, context) {
|
|
110
105
|
const text = reuseText(context.lastComponent);
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
text.setText(
|
|
106
|
+
const outcome = detailsOrLine(theme, result, expanded, toolDetails<TodoDetails>(result.details));
|
|
107
|
+
if ("line" in outcome) {
|
|
108
|
+
text.setText(outcome.line);
|
|
114
109
|
return text;
|
|
115
110
|
}
|
|
116
|
-
|
|
117
|
-
// block
|
|
118
|
-
|
|
111
|
+
const details = outcome.details;
|
|
112
|
+
// A details block can still arrive without the list: the host rejected the
|
|
113
|
+
// arguments, so its message is the whole result.
|
|
114
|
+
if (!Array.isArray(details.todos)) {
|
|
119
115
|
text.setText(rejectionLine(theme, result, expanded));
|
|
120
116
|
return text;
|
|
121
117
|
}
|
|
@@ -236,8 +236,8 @@ outlive the session.
|
|
|
236
236
|
|
|
237
237
|
## Testing
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
`runtime.test.ts` covers config, schema, the child supervisor, and the tailer
|
|
240
|
+
with a fake clock and fake children; `tools.test.ts` covers the tool and command;
|
|
241
|
+
`ui.test.ts` covers registration, formatting, the status chips, and the screen
|
|
242
|
+
through the shared `createFakePi`/`fakeCtx` doubles. The test harness is
|
|
243
|
+
`test/helpers/fixtures/watch.ts`.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
11
|
+
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
12
12
|
import { formatWatchList, shortLabel } from "./format.ts";
|
|
13
13
|
import type { WatchRuntime } from "./runtime.ts";
|
|
14
14
|
import { WatchListComponent } from "./tui.ts";
|
|
@@ -92,54 +92,40 @@ export function registerCommands(pi: ExtensionAPI, runtime: WatchRuntime): void
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
() => runtime.list(),
|
|
107
|
-
theme,
|
|
108
|
-
{
|
|
109
|
-
lines: (watchId) => runtime.matches(watchId, 500),
|
|
110
|
-
stop: (watchId) => {
|
|
111
|
-
runtime.stop(watchId);
|
|
112
|
-
},
|
|
113
|
-
clear: () => {
|
|
114
|
-
const result = runtime.clear(undefined, false);
|
|
115
|
-
if (result.cleared > 0) {
|
|
116
|
-
ctx.ui.notify(`Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
117
|
-
}
|
|
118
|
-
},
|
|
95
|
+
await withPolledScreen(
|
|
96
|
+
pi,
|
|
97
|
+
ctx,
|
|
98
|
+
(screen) =>
|
|
99
|
+
new WatchListComponent(
|
|
100
|
+
() => runtime.list(),
|
|
101
|
+
screen.theme,
|
|
102
|
+
{
|
|
103
|
+
lines: (watchId) => runtime.matches(watchId, 500),
|
|
104
|
+
stop: (watchId) => {
|
|
105
|
+
runtime.stop(watchId);
|
|
119
106
|
},
|
|
120
|
-
() => {
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
clear: () => {
|
|
108
|
+
const result = runtime.clear(undefined, false);
|
|
109
|
+
if (result.cleared > 0) {
|
|
110
|
+
ctx.ui.notify(`Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
111
|
+
}
|
|
123
112
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} finally {
|
|
141
|
-
stopTimer();
|
|
142
|
-
}
|
|
113
|
+
},
|
|
114
|
+
screen.close,
|
|
115
|
+
screen.requestRender,
|
|
116
|
+
screen.viewportRows,
|
|
117
|
+
),
|
|
118
|
+
{
|
|
119
|
+
pollMs: REFRESH_MS,
|
|
120
|
+
// The runtime's own clock owns file polling; this only refreshes what
|
|
121
|
+
// the screen shows, so opening it cannot change the poll cadence.
|
|
122
|
+
poll: (component, handles) => {
|
|
123
|
+
const open = component.currentLineId();
|
|
124
|
+
if (open) component.refreshLines(open);
|
|
125
|
+
handles.requestRender();
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
143
129
|
},
|
|
144
130
|
});
|
|
145
131
|
}
|
|
@@ -17,6 +17,7 @@ import type { KillTreeFn, SpawnFn, SpawnedProcess } from "../../lib/spawn.ts";
|
|
|
17
17
|
import { createChildSupervisor } from "./child.ts";
|
|
18
18
|
import { DEFAULT_CONFIG, loadConfig, type WatchConfig } from "./config.ts";
|
|
19
19
|
import { sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
20
|
+
import { defaultInterval } from "../../lib/timer.ts";
|
|
20
21
|
import { compileRegex, sourceLabel } from "./format.ts";
|
|
21
22
|
import { createUiController, type UiController } from "./ui.ts";
|
|
22
23
|
import type { PendingMatch, WatchMatch, WatchRecord, WatchSource } from "./types.ts";
|
|
@@ -118,7 +119,7 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
118
119
|
const watches = new Map<string, Internal>();
|
|
119
120
|
let counter = 0;
|
|
120
121
|
let disposed = false;
|
|
121
|
-
let clock:
|
|
122
|
+
let clock: (() => void) | undefined;
|
|
122
123
|
let onMatchHandler: ((matches: PendingMatch[]) => void) | undefined;
|
|
123
124
|
|
|
124
125
|
const record = (w: Internal): WatchRecord => ({
|
|
@@ -163,16 +164,13 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
163
164
|
});
|
|
164
165
|
|
|
165
166
|
const stopClock = (): void => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
clock = undefined;
|
|
169
|
-
}
|
|
167
|
+
clock?.();
|
|
168
|
+
clock = undefined;
|
|
170
169
|
};
|
|
171
170
|
|
|
172
171
|
const ensureClock = (): void => {
|
|
173
172
|
if (disposed || !autoClock || clock) return;
|
|
174
|
-
clock =
|
|
175
|
-
clock.unref?.();
|
|
173
|
+
clock = defaultInterval(config.pollMs, () => tick());
|
|
176
174
|
};
|
|
177
175
|
|
|
178
176
|
const pendingMatches = (): PendingMatch[] => {
|
|
@@ -11,7 +11,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
12
|
import { errorResult } from "../../lib/tool-result.ts";
|
|
13
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
15
15
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
16
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
17
|
import {
|
|
@@ -182,29 +182,18 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
182
182
|
|
|
183
183
|
renderCall(args, theme, context) {
|
|
184
184
|
const action = typeof args.action === "string" ? args.action : "watch";
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
188
|
-
paint(
|
|
189
|
-
theme,
|
|
190
|
-
"meta",
|
|
191
|
-
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
192
|
-
),
|
|
193
|
-
);
|
|
194
|
-
return text;
|
|
185
|
+
const subject = formatCallText(action, args as Record<string, unknown>, context.argsComplete);
|
|
186
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, subject, "middle");
|
|
195
187
|
},
|
|
196
188
|
|
|
197
189
|
renderResult(result, options, theme, context) {
|
|
198
190
|
const text = reuseText(context.lastComponent);
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
text.setText(
|
|
202
|
-
return text;
|
|
203
|
-
}
|
|
204
|
-
if (details.error) {
|
|
205
|
-
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
191
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<WatchDetails>(result.details));
|
|
192
|
+
if ("line" in outcome) {
|
|
193
|
+
text.setText(outcome.line);
|
|
206
194
|
return text;
|
|
207
195
|
}
|
|
196
|
+
const details = outcome.details;
|
|
208
197
|
|
|
209
198
|
if (details.action === "list" && details.watches) {
|
|
210
199
|
if (details.watches.length === 0) {
|
|
@@ -378,9 +378,9 @@ the content type and size.
|
|
|
378
378
|
|
|
379
379
|
## Testing
|
|
380
380
|
|
|
381
|
-
`extensions/web-access/` mirrors the halves (`search/`, `fetch/`) plus
|
|
382
|
-
`
|
|
383
|
-
`fetch`
|
|
381
|
+
`extensions/web-access/` mirrors the halves (`search/`, `fetch/`) plus the
|
|
382
|
+
`http.test.ts` suite for the shared runner, charset, config, and registration.
|
|
383
|
+
It covers the injected `fetch` runner, the provider registry and resolution, each provider's URL builder and
|
|
384
384
|
parser (DuckDuckGo, SearXNG, Brave), the orchestration pipeline and throttle,
|
|
385
385
|
charset decoding (declared, `<meta>`, and BOM precedence), a streamed body over
|
|
386
386
|
the byte cap, redirect re-validation (per-hop SSRF with literal IPs),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { messageOf } from "../../../lib/errors.ts";
|
|
11
|
+
import { abortableSleep } from "../../../lib/timer.ts";
|
|
11
12
|
import { createFetchRunner, type HttpRunner } from "../http.ts";
|
|
12
13
|
import { resolveSearchApiKey, type WebSearchConfig } from "./config.ts";
|
|
13
14
|
import { resolveProvider } from "./registry.ts";
|
|
@@ -40,24 +41,14 @@ export interface SearchDeps {
|
|
|
40
41
|
/** A sleeper, injectable so throttle scheduling is testable without wall time. */
|
|
41
42
|
type ThrottleSleep = (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
reject(new WebSearchError("Search aborted."));
|
|
52
|
-
};
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
finish();
|
|
55
|
-
resolve();
|
|
56
|
-
}, ms);
|
|
57
|
-
if (signal?.aborted) return onAbort();
|
|
58
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
44
|
+
/** The shared abortable sleep, mapped to the one error this extension throws. */
|
|
45
|
+
const delay: ThrottleSleep = async (ms, signal) => {
|
|
46
|
+
try {
|
|
47
|
+
await abortableSleep(ms, signal);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new WebSearchError("Search aborted.");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
61
52
|
|
|
62
53
|
/**
|
|
63
54
|
* A stateful throttle that spaces requests by `minIntervalMs`. `now` and `sleep`
|
|
@@ -3,20 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The bridge and the channel need a cancellable one-shot timer that a test can
|
|
5
5
|
* drive without waiting (the delayed acknowledgement, the typing keep-alive, the
|
|
6
|
-
* prompt timeout)
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
6
|
+
* prompt timeout), and login needs an abortable sleep between polls; both seams
|
|
7
|
+
* and their production defaults are in [`lib/timer.ts`](../../lib/timer.ts),
|
|
8
|
+
* because job, watch, file-browse, plan, and web-access arm the same two. This
|
|
9
|
+
* module adds the per-key map those timers are held in.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
export type ArmTimer = (ms: number, fn: () => void) => () => void;
|
|
12
|
+
import { abortableSleep, type ArmTimer } from "../../lib/timer.ts";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return () => clearTimeout(timer);
|
|
19
|
-
};
|
|
14
|
+
export { type ArmTimer, defaultTimer } from "../../lib/timer.ts";
|
|
15
|
+
|
|
16
|
+
/** The shared abortable sleep, under the name the bridge and login already use. */
|
|
17
|
+
export const defaultSleep = abortableSleep;
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
20
|
* A set of one-shot timers keyed by a caller's id (a peer, a watcher).
|
|
@@ -74,22 +72,3 @@ export function createTimerMap(arm: ArmTimer): TimerMap {
|
|
|
74
72
|
};
|
|
75
73
|
return map;
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
/** An abortable sleep: resolves after `ms`, rejects when `signal` aborts. */
|
|
79
|
-
export function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
if (signal?.aborted) {
|
|
82
|
-
reject(new Error("aborted"));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const timer = setTimeout(() => {
|
|
86
|
-
signal?.removeEventListener("abort", onAbort);
|
|
87
|
-
resolve();
|
|
88
|
-
}, ms);
|
|
89
|
-
const onAbort = () => {
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
reject(new Error("aborted"));
|
|
92
|
-
};
|
|
93
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
94
|
-
});
|
|
95
|
-
}
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Type, type Static } from "typebox";
|
|
13
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
-
import {
|
|
14
|
+
import { callLine, errorLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
16
15
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
16
|
import type { Bridge } from "./bridge.ts";
|
|
18
17
|
|
|
@@ -82,11 +81,9 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
82
81
|
},
|
|
83
82
|
|
|
84
83
|
renderCall(args, theme, context) {
|
|
85
|
-
const text = reuseCallText(context.lastComponent);
|
|
86
84
|
const body = (args as { text?: unknown }).text;
|
|
87
|
-
const label = context.argsComplete && typeof body === "string" ?
|
|
88
|
-
|
|
89
|
-
return text;
|
|
85
|
+
const label = context.argsComplete && typeof body === "string" ? body : "…";
|
|
86
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, label);
|
|
90
87
|
},
|
|
91
88
|
|
|
92
89
|
renderResult(result, options, theme, context) {
|
|
@@ -97,7 +94,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
97
94
|
return text;
|
|
98
95
|
}
|
|
99
96
|
if (details?.error) {
|
|
100
|
-
text.setText(
|
|
97
|
+
text.setText(errorLine(theme, details.error));
|
|
101
98
|
return text;
|
|
102
99
|
}
|
|
103
100
|
// Pi's rejection result carries `{}`; show its sanitized message.
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { realPathOfNearest } from "../../lib/path.ts";
|
|
9
|
+
import { mapConcurrent } from "../../lib/pool.ts";
|
|
9
10
|
import { loadConfig } from "./config.ts";
|
|
10
11
|
import { gapLines, isolationGaps } from "./gaps.ts";
|
|
11
|
-
import { type CheckoutCheck, defaultBranch, listManagedWorktrees, repoRoot } from "./git.ts";
|
|
12
|
+
import { type CheckoutCheck, defaultBranch, listManagedWorktrees, type ManagedWorktree, repoRoot } from "./git.ts";
|
|
12
13
|
import { reconcileRegistry, statusOf, type CheckoutStatus } from "./registry.ts";
|
|
13
14
|
import { configFor, getActive, getInactiveOverrides, getUnverifiableCommands } from "./runtime.ts";
|
|
14
15
|
import type { WorktreeState } from "./state.ts";
|
|
@@ -115,15 +116,19 @@ export async function worktreeStatus(pi: ExtensionAPI, ctx: ExtensionContext): P
|
|
|
115
116
|
const { registry } = reconcileRegistry(root, config, managed, { persist: false });
|
|
116
117
|
const baseBranch = await defaultBranch(pi, root);
|
|
117
118
|
const baseRef = baseBranch ? `origin/${baseBranch}` : undefined;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Keyed by the canonical path the registry stores, so a row's record is one
|
|
120
|
+
// lookup instead of a scan of every record.
|
|
121
|
+
const records = new Map(registry.worktrees.map((record) => [record.path, record]));
|
|
122
|
+
const row = async (entry: ManagedWorktree): Promise<string> => {
|
|
123
|
+
const path = realPathOfNearest(entry.path);
|
|
124
|
+
const status = await statusOf(pi, entry, { baseRef, record: records.get(path) });
|
|
122
125
|
const marks = statusMarks(status, current?.path === entry.path);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
return ` ${entry.path} ${entry.branch ?? ""}${marks.length > 0 ? ` [${marks.join(", ")}]` : ""}`.trimEnd();
|
|
127
|
+
};
|
|
128
|
+
// Each row is up to four git reads; four at a time keeps the list in the order
|
|
129
|
+
// git reports while overlapping the waits. `statusOf` passes no `env`, which is
|
|
130
|
+
// what makes concurrent git calls safe here (see `RunGitOptions.env`).
|
|
131
|
+
lines.push("", `Managed worktrees (${managed.length}):`, ...(await mapConcurrent(managed, 4, row)));
|
|
127
132
|
}
|
|
128
133
|
return lines.join("\n");
|
|
129
134
|
}
|
package/lib/README.md
CHANGED
|
@@ -20,25 +20,28 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
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
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
|
-
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
|
|
23
|
+
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters, plus `decodeCodePoint`, the other half of the same boundary — one numeric character reference as text, with a surrogate or out-of-range value decoded to U+FFFD. |
|
|
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
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
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
|
+
| [`timer.ts`](./timer.ts) | The three timer seams and their production defaults, each cancellable and each built so a pending timer never holds the process open: `ArmTimer` / `defaultTimer` (one shot), `RepeatTimer` / `defaultInterval` (repeating), and `abortableSleep` (resolve after a delay, reject `Error("aborted")` when a signal wins). Shared by `wechat`'s bridge and prompt channel, plan's review page, `job`'s and `watch`'s repaint clocks, `bytefly`'s working tick, the browse live channel, and the search throttle — so a test drives a deadline instead of waiting on one. |
|
|
29
30
|
| [`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`. |
|
|
31
|
+
| [`pool.ts`](./pool.ts) | `mapConcurrent`: one async read per entry with at most `limit` calls in flight and results in input order, so a directory listing or a status sweep fans out without an unbounded burst or a per-entry round trip. Shared by `file-browse` and `worktree`. |
|
|
30
32
|
| [`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. |
|
|
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. |
|
|
33
|
+
| [`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`, `revParseMany` for several revisions in one call, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
32
34
|
| [`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. |
|
|
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
|
|
35
|
+
| [`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, plus `eventHeaders` for the one long-lived stream), [`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, `openEventStream` for the head and reconnect hint of an SSE response, and the idempotent `createCloser`), and the [`http/index.ts`](./http/index.ts) barrel. |
|
|
36
|
+
| [`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` with its `align` option for a value held beside its label, `statusRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`, `byRunningFirst` for a live list's order), [`tui/columns.ts`](./tui/columns.ts) (`splitRow` for two cells at the two edges, `inlineRow` for two cells held together, `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`, `callLine` — the one headline every renderer draws — `detailsOrLine` (a result's details, or the one `errorLine` / `rejectionLine` a failure shows), 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
37
|
| [`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 `
|
|
38
|
+
| [`pdf.ts`](./pdf.ts) | The optional `unpdf` seam shared by `web-access` and `file-read`: `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. |
|
|
37
39
|
| [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
|
|
38
40
|
| [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
|
|
39
41
|
| [`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
42
|
| [`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. |
|
|
41
43
|
| [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
|
|
44
|
+
| [`screen-session.ts`](./screen-session.ts) | `withPolledScreen`: mount one `ctx.ui.custom` dock screen — rails suppressed for its lifetime, an optional repaint tick through an injected `RepeatTimer`, and one teardown — handing the mount the host's `tui`, `theme`, `requestRender`, `close`, and `viewportRows`. Shared by `/jobs`, `/watch`, and `/memory`, whose rails window and timer window were the same one. |
|
|
42
45
|
| [`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. |
|
|
43
46
|
| [`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`. |
|
|
44
47
|
| [`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`. |
|
|
@@ -61,9 +64,10 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
61
64
|
one extension probes on another — live in `tool-names.ts`.
|
|
62
65
|
- **A helper that exists here must be imported, not redefined.**
|
|
63
66
|
`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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
name is also exported by `lib/`, and a body copied from `lib/` or from any
|
|
68
|
+
other file — a second extension, a sibling module, or `lib/` itself. A
|
|
69
|
+
deliberate wrapper goes on that test's reasoned exception list; a copy belongs
|
|
70
|
+
here with its declared capability suite.
|
|
67
71
|
- **A color is a role, not a token.** The rules and the primitives are in
|
|
68
72
|
[`tui/README.md`](./tui/README.md#color-is-a-role): draw through
|
|
69
73
|
[`tui/tokens.ts`](./tui/tokens.ts) (`paint(theme, role, text)`) so a theme swap
|
package/lib/escape.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTML text escaping
|
|
2
|
+
* HTML text escaping, and the inverse for one character reference.
|
|
3
3
|
*
|
|
4
4
|
* The single escape used by every page renderer and by the Markdown renderer:
|
|
5
5
|
* model- and file-authored text becomes safe text nodes and double-quoted
|
|
6
6
|
* attributes. Keeping it here (rather than in `html.ts`) lets `markdown.ts`
|
|
7
7
|
* escape without importing the page renderer, so the module graph stays
|
|
8
|
-
* acyclic.
|
|
8
|
+
* acyclic. `decodeCodePoint` is the other half of the same boundary and lives
|
|
9
|
+
* beside it for the same reason: the Markdown inline scan and the raw-HTML
|
|
10
|
+
* attribute decoder both resolve `&#N;` and `&#xN;` references, and one copy
|
|
11
|
+
* cannot drift from the other.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
/** Escape text for an HTML text node or double-quoted attribute. */
|
|
@@ -25,3 +28,13 @@ export function escapeHtml(value: string): string {
|
|
|
25
28
|
}
|
|
26
29
|
});
|
|
27
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One numeric character reference as text: U+FFFD when it is a surrogate, zero,
|
|
34
|
+
* or out of range, so an entity cannot inject an unpaired half of a pair.
|
|
35
|
+
*/
|
|
36
|
+
export function decodeCodePoint(value: number): string {
|
|
37
|
+
if (!Number.isFinite(value) || value <= 0 || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff))
|
|
38
|
+
return "\uFFFD";
|
|
39
|
+
return String.fromCodePoint(value);
|
|
40
|
+
}
|
package/lib/git/read.ts
CHANGED
|
@@ -70,6 +70,26 @@ export async function revParse(runGit: RunGit, cwd: string, rev: string): Promis
|
|
|
70
70
|
return id || undefined;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Resolve several revisions in one `git rev-parse`, in the order given.
|
|
75
|
+
*
|
|
76
|
+
* A snapshot needs `HEAD` and its tree, and two calls pay two spawns for one
|
|
77
|
+
* answer; `rev-parse` prints one line per argument, so one call is enough. A
|
|
78
|
+
* revision that does not resolve is not a shorter list — it is no answer, since
|
|
79
|
+
* the caller's indices would shift — so the result is `[]` unless every one
|
|
80
|
+
* resolved.
|
|
81
|
+
*/
|
|
82
|
+
export async function revParseMany(runGit: RunGit, cwd: string, revs: readonly string[]): Promise<string[]> {
|
|
83
|
+
if (revs.length === 0) return [];
|
|
84
|
+
const result = await runGit(["rev-parse", ...revs], { cwd });
|
|
85
|
+
if (result.code !== 0) return [];
|
|
86
|
+
const ids = result.stdout
|
|
87
|
+
.split("\n")
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter((line) => line !== "");
|
|
90
|
+
return ids.length === revs.length ? ids : [];
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
/** Whether the repository has at least one commit. */
|
|
74
94
|
export async function hasCommits(runGit: RunGit, cwd: string): Promise<boolean> {
|
|
75
95
|
return (await revParse(runGit, cwd, "HEAD")) !== undefined;
|