@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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in per-run transcripts.
|
|
3
|
+
*
|
|
4
|
+
* The dispatching process writes these; a child never learns that one exists, so
|
|
5
|
+
* the read-only tool set and the project-agent trust gate stay exactly as strict
|
|
6
|
+
* as they are. Lines are buffered in memory up to {@link MAX_TRANSCRIPT_BYTES}
|
|
7
|
+
* and flushed once at the end of the run, which keeps the store ordered without a
|
|
8
|
+
* stream to close and still writes on the abort path.
|
|
9
|
+
*
|
|
10
|
+
* Every failure is reported, never thrown: an unwritable checkout must not fail a
|
|
11
|
+
* delegation that already did its work.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { repoRootFor } from "../../lib/git/index.ts";
|
|
19
|
+
import type { SingleResult } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
/** Ceiling for one run's buffered transcript, stdout and stderr together. */
|
|
22
|
+
export const MAX_TRANSCRIPT_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/** `YYYYMMDD-HHMMSS-abcdef`; a name sort is chronological, so pruning never parses one. */
|
|
25
|
+
const RUN_ID_PATTERN = /^\d{8}-\d{6}-[0-9a-f]{6}$/;
|
|
26
|
+
|
|
27
|
+
const EVENTS_FILE = "events.jsonl";
|
|
28
|
+
const STDERR_FILE = "stderr.log";
|
|
29
|
+
const META_FILE = "meta.json";
|
|
30
|
+
|
|
31
|
+
/** What a transcript needs from the run that produced it. */
|
|
32
|
+
export interface TranscriptContext {
|
|
33
|
+
/** Working directory the child ran in. */
|
|
34
|
+
cwd: string;
|
|
35
|
+
/** Whether the run forced a reader-only tool set. */
|
|
36
|
+
readOnly: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The outcome of writing one run directory. */
|
|
40
|
+
export interface TranscriptWritten {
|
|
41
|
+
/** Directory written, absent when the write failed. */
|
|
42
|
+
path?: string;
|
|
43
|
+
/** Bytes buffered for this run. */
|
|
44
|
+
bytes: number;
|
|
45
|
+
/** Whether the size cap cut the transcript short. */
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
/** Why the write failed, when it did. */
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A run's buffered transcript, written once the run has finished. */
|
|
52
|
+
export interface Transcript {
|
|
53
|
+
/** Append one raw stdout line. */
|
|
54
|
+
line(raw: string): void;
|
|
55
|
+
/** Append stderr text, which may arrive in partial chunks. */
|
|
56
|
+
stderr(text: string): void;
|
|
57
|
+
/** Write the run directory under `dir`. Never throws. */
|
|
58
|
+
write(dir: string, result: SingleResult, context: TranscriptContext): TranscriptWritten;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The directory holding every transcript for `cwd`'s repository. */
|
|
62
|
+
export async function transcriptsDirFor(pi: Pick<ExtensionAPI, "exec">, cwd: string): Promise<string> {
|
|
63
|
+
const root = await repoRootFor(pi, cwd, 5_000);
|
|
64
|
+
return root ? join(root, CONFIG_DIR_NAME, "subagents") : join(getAgentDir(), "subagents");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A run id for `at` (epoch ms, rendered in UTC) with a six-hex suffix. */
|
|
68
|
+
export function runIdFor(at: number, suffix: string): string {
|
|
69
|
+
const date = new Date(at);
|
|
70
|
+
const pad = (value: number, width = 2) => String(value).padStart(width, "0");
|
|
71
|
+
const day = `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}`;
|
|
72
|
+
const time = `${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;
|
|
73
|
+
return `${day}-${time}-${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The metadata written beside a run's stream; enough to audit it without one. */
|
|
77
|
+
function metaFor(runId: string, result: SingleResult, context: TranscriptContext, truncated: boolean) {
|
|
78
|
+
return {
|
|
79
|
+
version: 1,
|
|
80
|
+
runId,
|
|
81
|
+
agent: result.agent,
|
|
82
|
+
agentSource: result.agentSource,
|
|
83
|
+
task: result.task,
|
|
84
|
+
step: result.step,
|
|
85
|
+
cwd: context.cwd,
|
|
86
|
+
readOnly: context.readOnly,
|
|
87
|
+
provider: result.provider,
|
|
88
|
+
model: result.model,
|
|
89
|
+
startedAt: result.startedAt,
|
|
90
|
+
finishedAt: result.finishedAt,
|
|
91
|
+
exitCode: result.exitCode,
|
|
92
|
+
timedOut: result.timedOut === true,
|
|
93
|
+
stopReason: result.stopReason,
|
|
94
|
+
errorMessage: result.errorMessage,
|
|
95
|
+
toolErrors: result.toolErrors ?? 0,
|
|
96
|
+
usage: result.usage,
|
|
97
|
+
transcript: { truncated },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Buffer one run's stream until it finishes. */
|
|
102
|
+
export function createTranscript(options: { maxBytes?: number } = {}): Transcript {
|
|
103
|
+
const maxBytes = options.maxBytes ?? MAX_TRANSCRIPT_BYTES;
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
let stderr = "";
|
|
106
|
+
let bytes = 0;
|
|
107
|
+
let truncated = false;
|
|
108
|
+
|
|
109
|
+
const append = (text: string, kind: "line" | "stderr"): void => {
|
|
110
|
+
if (truncated || !text) return;
|
|
111
|
+
const size = Buffer.byteLength(text);
|
|
112
|
+
if (bytes + size > maxBytes) {
|
|
113
|
+
// Stop growing; the marker is added on write so the file stays parseable.
|
|
114
|
+
truncated = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
bytes += size;
|
|
118
|
+
if (kind === "line") lines.push(text);
|
|
119
|
+
else stderr += text;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
line: (raw) => append(raw, "line"),
|
|
124
|
+
stderr: (text) => append(text, "stderr"),
|
|
125
|
+
|
|
126
|
+
write(dir, result, context) {
|
|
127
|
+
const runId = runIdFor(result.startedAt ?? Date.now(), randomBytes(3).toString("hex"));
|
|
128
|
+
const runDir = join(dir, runId);
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(runDir, { recursive: true });
|
|
131
|
+
const body = truncated ? [...lines, JSON.stringify({ type: "transcript-truncated", bytes })] : lines;
|
|
132
|
+
if (body.length > 0) writeFileSync(join(runDir, EVENTS_FILE), `${body.join("\n")}\n`, { mode: 0o600 });
|
|
133
|
+
if (stderr) writeFileSync(join(runDir, STDERR_FILE), stderr, { mode: 0o600 });
|
|
134
|
+
// Written last: a directory without metadata is an interrupted write.
|
|
135
|
+
const meta = `${JSON.stringify(metaFor(runId, result, context, truncated), null, 2)}\n`;
|
|
136
|
+
writeFileSync(join(runDir, META_FILE), meta, { mode: 0o600 });
|
|
137
|
+
return { path: runDir, bytes, truncated };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
bytes,
|
|
141
|
+
truncated,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Which run directories a prune keeps and which it removes. */
|
|
150
|
+
export interface PruneOutcome {
|
|
151
|
+
removed: string[];
|
|
152
|
+
/** Runs still on disk after the prune. */
|
|
153
|
+
kept: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Remove aged and surplus run directories. Only names matching a run id are
|
|
158
|
+
* candidates, so an unrelated directory under the store is never touched.
|
|
159
|
+
*/
|
|
160
|
+
export function pruneTranscripts(
|
|
161
|
+
dir: string,
|
|
162
|
+
options: { retentionDays: number; maxRuns: number; now?: number },
|
|
163
|
+
): PruneOutcome {
|
|
164
|
+
const now = options.now ?? Date.now();
|
|
165
|
+
let names: string[];
|
|
166
|
+
try {
|
|
167
|
+
names = readdirSync(dir, { withFileTypes: true })
|
|
168
|
+
.filter((entry) => entry.isDirectory() && RUN_ID_PATTERN.test(entry.name))
|
|
169
|
+
.map((entry) => entry.name)
|
|
170
|
+
.sort();
|
|
171
|
+
} catch {
|
|
172
|
+
return { removed: [], kept: 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const kept: string[] = [];
|
|
176
|
+
const stale: string[] = [];
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
let modified: number;
|
|
179
|
+
try {
|
|
180
|
+
modified = statSync(join(dir, name)).mtimeMs;
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (now - modified > options.retentionDays * 86_400_000) stale.push(name);
|
|
185
|
+
else kept.push(name);
|
|
186
|
+
}
|
|
187
|
+
while (kept.length > options.maxRuns) {
|
|
188
|
+
const oldest = kept.shift();
|
|
189
|
+
if (oldest) stale.push(oldest);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const removed: string[] = [];
|
|
193
|
+
for (const name of stale) {
|
|
194
|
+
try {
|
|
195
|
+
rmSync(join(dir, name), { recursive: true, force: true });
|
|
196
|
+
removed.push(name);
|
|
197
|
+
} catch {
|
|
198
|
+
// Best-effort: a locked directory is retried at the next session start.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { removed, kept: kept.length };
|
|
202
|
+
}
|
|
@@ -50,6 +50,27 @@ export interface SingleResult {
|
|
|
50
50
|
startedAt?: number;
|
|
51
51
|
/** Epoch ms when the subprocess finished. */
|
|
52
52
|
finishedAt?: number;
|
|
53
|
+
/** Whether the run hit its wall-clock limit, which fails the run without aborting it. */
|
|
54
|
+
timedOut?: boolean;
|
|
55
|
+
/** The limit that applied to this run in ms; absent when it had none. */
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
/** Directory of the run's transcript, when one was written. */
|
|
58
|
+
artifactPath?: string;
|
|
59
|
+
/** Bytes buffered for the transcript. */
|
|
60
|
+
artifactBytes?: number;
|
|
61
|
+
/** Whether the transcript stopped at the size cap. */
|
|
62
|
+
artifactTruncated?: boolean;
|
|
63
|
+
/** Why the transcript could not be written, when it could not. */
|
|
64
|
+
artifactError?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A background run the tool just started. */
|
|
68
|
+
export interface BackgroundStart {
|
|
69
|
+
runId: string;
|
|
70
|
+
agent: string;
|
|
71
|
+
task: string;
|
|
72
|
+
cwd: string;
|
|
73
|
+
startedAt: number;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/** Structured result carried on the `subagent` tool result for rendering. */
|
|
@@ -62,6 +83,8 @@ export interface SubagentDetails {
|
|
|
62
83
|
projectAgentsDir?: string | null;
|
|
63
84
|
/** Whether the run forced a read-only tool set (plan mode). */
|
|
64
85
|
readOnly?: boolean;
|
|
86
|
+
/** Present when the tool started a background run instead of waiting. */
|
|
87
|
+
background?: BackgroundStart;
|
|
65
88
|
/**
|
|
66
89
|
* Total steps/tasks requested, which can exceed `results.length` when a chain
|
|
67
90
|
* stops early. Defaults to `results.length` when absent.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer status and finish notices for background subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* The runtime owns lifecycle and results; this owns the running-count chip and
|
|
5
|
+
* the notice that points back at `/agents`. A cancelled run stays silent because
|
|
6
|
+
* the user initiated it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
10
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
11
|
+
import { createStatusController, type StatusController } from "../../lib/tui/status-controller.ts";
|
|
12
|
+
import type { BackgroundRun } from "./background.ts";
|
|
13
|
+
|
|
14
|
+
export interface SubagentUiOptions {
|
|
15
|
+
/** The current runs, read fresh on every paint. */
|
|
16
|
+
runs(): readonly BackgroundRun[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SubagentUiController extends StatusController {
|
|
20
|
+
notifyFinished(run: BackgroundRun): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A status word a notice can read without exposing the type's spelling. */
|
|
24
|
+
function statusText(status: BackgroundRun["status"]): string {
|
|
25
|
+
return status === "timedout" ? "timed out" : status;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createSubagentUi(options: SubagentUiOptions): SubagentUiController {
|
|
29
|
+
const controller = createStatusController({
|
|
30
|
+
keys: [STATUS_KEYS.agents],
|
|
31
|
+
enabled: () => true,
|
|
32
|
+
chips: () => {
|
|
33
|
+
const running = options.runs().filter((run) => run.status === "running").length;
|
|
34
|
+
return [{ key: STATUS_KEYS.agents, glyph: GLYPHS.delegation, count: running, role: "accent" }];
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const notifyFinished = (run: BackgroundRun): void => {
|
|
39
|
+
// A cancelled run stays silent because the user initiated it.
|
|
40
|
+
if (run.status === "cancelled") return;
|
|
41
|
+
const failed = run.status === "failed" || run.status === "timedout";
|
|
42
|
+
const action = run.status === "finished" ? "collect" : "inspect";
|
|
43
|
+
controller.notify(
|
|
44
|
+
`Background subagent ${run.id} (${sanitizeLine(run.agent)}) ${statusText(run.status)}. Use /agents to ${action} it.`,
|
|
45
|
+
failed ? "error" : "info",
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return { ...controller, notifyFinished };
|
|
50
|
+
}
|
|
@@ -174,13 +174,17 @@ depends on config.
|
|
|
174
174
|
- **Whole-list replacement, not mutations.** Every call is a snapshot, so
|
|
175
175
|
`todos: []` is the natural clear and there is no "forgot to toggle" failure
|
|
176
176
|
mode. No ids or per-item update actions exist.
|
|
177
|
-
- **State in tool-result `details`.** `
|
|
178
|
-
branch and takes the last
|
|
179
|
-
the list for that point in history.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
- **State in tool-result `details`.** `lib/todo-state.ts`'s `lastTodos` replays
|
|
178
|
+
the active branch and takes the last write that fully validates, so `/resume`
|
|
179
|
+
and `/tree` reproduce the list for that point in history. The same replay is
|
|
180
|
+
what `plan` and `goal` read, so there is one rule for which write is a state
|
|
181
|
+
write. `pi.appendEntry()` was rejected because a side-channel list would not
|
|
182
|
+
branch; error results are skipped as non-writes.
|
|
183
|
+
- **Validation is pure and fail-safe.** The parser (`normalizeTodos` in
|
|
184
|
+
`lib/todo-state.ts`) throws a model-readable message, and the tool turns that
|
|
185
|
+
into an `isError` result that carries the *previous* list. A rejected call
|
|
186
|
+
never half-applies, and branch replay follows the same rule: a stored write
|
|
187
|
+
that fails validation is ignored whole rather than partially applied.
|
|
184
188
|
- **One `in_progress` item, with a label.** The rule is validated, not silently
|
|
185
189
|
coerced, so the model learns it from the error. The item must carry a
|
|
186
190
|
non-blank `activeForm`; branch replay passes `{ requireActiveForm: false }`
|
|
@@ -210,10 +214,10 @@ depends on config.
|
|
|
210
214
|
answer and never reopens that path. `nudged` caps it at one per user turn,
|
|
211
215
|
`setTodos` clears the counters, and `context` drops the hidden message on the
|
|
212
216
|
next user turn so it never accumulates.
|
|
213
|
-
- **Content is sanitized at the boundary.** `normalizeTodos`
|
|
214
|
-
characters and collapses whitespace
|
|
215
|
-
keeping `format.ts` free of terminal
|
|
216
|
-
per item.
|
|
217
|
+
- **Content is sanitized at the boundary.** `normalizeTodos` in
|
|
218
|
+
`lib/todo-state.ts` replaces control characters and collapses whitespace
|
|
219
|
+
before the length and duplicate checks, keeping `format.ts` free of terminal
|
|
220
|
+
concerns and every surface to one row per item.
|
|
217
221
|
- **The widget summarizes; full views keep the model's order.** The persistent
|
|
218
222
|
widget is a single line (`currentTodo` picks the `in_progress` item, else the
|
|
219
223
|
first pending); `/todos` keeps the submitted order, and the transcript result
|
|
@@ -234,8 +238,9 @@ depends on config.
|
|
|
234
238
|
|---|---|
|
|
235
239
|
| `index.ts` | Factory: wire the runtime, tool, command, reminder events, and session events. |
|
|
236
240
|
| `types.ts` | `Todo`, `TodoStatus`, `TodoDetails`. |
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
241
|
+
| `types.ts` | `Todo` and `TodoDetails`; re-exports `TodoStatus` / `TODO_STATUSES` from `lib/todo-state.ts`. |
|
|
242
|
+
| `schema.ts` | TypeBox parameters; the parser and limits live in `lib/todo-state.ts`. |
|
|
243
|
+
| `state.ts` | Status queries over the replayed list (pure). |
|
|
239
244
|
| `nudge.ts` | Lag detection: mutating and file-write classification, due check, reminder text (pure). |
|
|
240
245
|
| `format.ts` | Model-facing and transcript text (pure). |
|
|
241
246
|
| `tui.ts` | Widget and scrollable `/todos` components, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
|
-
import {
|
|
7
|
+
import { GLYPH_GAP, GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
8
|
+
import { overflowHint } from "../../lib/tui/tool.ts";
|
|
8
9
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
9
10
|
import { completedCount, currentTodo } from "./state.ts";
|
|
10
11
|
import type { Todo, TodoStatus } from "./types.ts";
|
|
@@ -149,7 +150,7 @@ export function todoRailLines(input: TodoRailInput, theme: Theme, width: number)
|
|
|
149
150
|
});
|
|
150
151
|
}
|
|
151
152
|
if (input.more && input.more > 0) {
|
|
152
|
-
lines.push(`${continuation}${
|
|
153
|
+
lines.push(`${continuation}${overflowHint(theme, input.more)}`);
|
|
153
154
|
}
|
|
154
155
|
return lines.map((line) => truncateToWidth(line, w, "…"));
|
|
155
156
|
}
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { onRailsSuppressed, onUpperRailChanged } from "../../lib/rails.ts";
|
|
15
|
+
import { lastTodos } from "../../lib/todo-state.ts";
|
|
15
16
|
import { DEFAULT_TODO_CONFIG, type TodoConfig } from "./config.ts";
|
|
16
17
|
import { shouldNudge } from "./nudge.ts";
|
|
17
|
-
import { hasOpenTodos
|
|
18
|
+
import { hasOpenTodos } from "./state.ts";
|
|
18
19
|
import { TodoWidget, WIDGET_KEY } from "./tui.ts";
|
|
19
20
|
import type { Todo } from "./types.ts";
|
|
20
21
|
|
|
@@ -115,7 +116,7 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
115
116
|
notify();
|
|
116
117
|
},
|
|
117
118
|
reconstruct: (ctx) => {
|
|
118
|
-
todos =
|
|
119
|
+
todos = lastTodos(ctx.sessionManager.getBranch());
|
|
119
120
|
mutations = 0;
|
|
120
121
|
writes = 0;
|
|
121
122
|
touched = false;
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parameter schema
|
|
2
|
+
* Parameter schema for the `todo` tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The schema is the host-facing shape; the semantic validation and the branch
|
|
5
|
+
* replay live together in `lib/todo-state.ts`, so the tool, `plan`, and `goal`
|
|
6
|
+
* read the same rules. Invalid input still throws a model-readable `Error`
|
|
7
|
+
* before any state changes.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
11
|
import { Type, type Static } from "typebox";
|
|
11
|
-
import {
|
|
12
|
-
import { TODO_STATUSES, type Todo, type TodoStatus } from "./types.ts";
|
|
13
|
-
|
|
14
|
-
/** Maximum number of items in one list. */
|
|
15
|
-
export const MAX_TODOS = 50;
|
|
16
|
-
/** Maximum length of a single item's text. */
|
|
17
|
-
export const MAX_CONTENT = 500;
|
|
12
|
+
import { MAX_TODOS, TODO_STATUSES } from "../../lib/todo-state.ts";
|
|
18
13
|
|
|
19
14
|
/**
|
|
20
15
|
* One item in the tool's input and output schemas.
|
|
21
16
|
*
|
|
22
17
|
* The input schema (`TodoParams`) enforces `maxItems` and the status enum
|
|
23
|
-
* before `execute` runs
|
|
24
|
-
*
|
|
25
|
-
* (one `in_progress`, unique non-empty content, content length).
|
|
18
|
+
* before `execute` runs; `normalizeTodos` in `lib/todo-state.ts` re-enforces
|
|
19
|
+
* both and owns the semantic rules the schema cannot express.
|
|
26
20
|
*/
|
|
27
21
|
export const TodoItem = Type.Object({
|
|
28
22
|
content: Type.String({ description: "Imperative description of the task" }),
|
|
@@ -53,73 +47,3 @@ export const TodoResult = Type.Object({
|
|
|
53
47
|
});
|
|
54
48
|
|
|
55
49
|
export type TodoArgs = Static<typeof TodoParams>;
|
|
56
|
-
|
|
57
|
-
const STATUS_SET: ReadonlySet<string> = new Set(TODO_STATUSES);
|
|
58
|
-
|
|
59
|
-
function normalizeStatus(raw: unknown, index: number): TodoStatus {
|
|
60
|
-
const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
61
|
-
if (STATUS_SET.has(value)) return value as TodoStatus;
|
|
62
|
-
throw new Error(`Todo ${index + 1}: status must be one of ${TODO_STATUSES.join(", ")}.`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Options for {@link normalizeTodos}. */
|
|
66
|
-
export interface NormalizeOptions {
|
|
67
|
-
/**
|
|
68
|
-
* Require a non-blank `activeForm` on `in_progress` items. Live tool input
|
|
69
|
-
* enforces this (the default); branch replay passes `false` so lists written
|
|
70
|
-
* before the rule was added still load.
|
|
71
|
-
*/
|
|
72
|
-
requireActiveForm?: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Validate and normalize the model's list.
|
|
77
|
-
*
|
|
78
|
-
* Content and `activeForm` are sanitized to a single safe line (see
|
|
79
|
-
* `sanitizeLine`). Rejects an empty description, an unknown status, duplicate
|
|
80
|
-
* content, an over-long field, more than one `in_progress` item, and — unless
|
|
81
|
-
* `options.requireActiveForm` is `false` — an `in_progress` item without a
|
|
82
|
-
* non-blank `activeForm`. `activeForm` is dropped when blank.
|
|
83
|
-
*/
|
|
84
|
-
export function normalizeTodos(raw: unknown, options: NormalizeOptions = {}): Todo[] {
|
|
85
|
-
const requireActiveForm = options.requireActiveForm ?? true;
|
|
86
|
-
if (raw === undefined || raw === null) return [];
|
|
87
|
-
if (!Array.isArray(raw)) throw new Error("todos must be an array.");
|
|
88
|
-
if (raw.length > MAX_TODOS) throw new Error(`At most ${MAX_TODOS} todos are allowed.`);
|
|
89
|
-
|
|
90
|
-
const todos: Todo[] = [];
|
|
91
|
-
const seen = new Set<string>();
|
|
92
|
-
let inProgress = 0;
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < raw.length; i++) {
|
|
95
|
-
const item = (raw[i] ?? {}) as Partial<Todo>;
|
|
96
|
-
const content = typeof item.content === "string" ? sanitizeLine(item.content) : "";
|
|
97
|
-
if (!content) throw new Error(`Todo ${i + 1}: content is required.`);
|
|
98
|
-
if (content.length > MAX_CONTENT) {
|
|
99
|
-
throw new Error(`Todo ${i + 1}: content is longer than ${MAX_CONTENT} characters.`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const key = content.toLowerCase();
|
|
103
|
-
if (seen.has(key)) throw new Error(`Todo ${i + 1}: duplicate content "${content}".`);
|
|
104
|
-
seen.add(key);
|
|
105
|
-
|
|
106
|
-
const status = normalizeStatus(item.status, i);
|
|
107
|
-
if (status === "in_progress") inProgress++;
|
|
108
|
-
|
|
109
|
-
const activeForm = typeof item.activeForm === "string" ? sanitizeLine(item.activeForm) : "";
|
|
110
|
-
if (activeForm.length > MAX_CONTENT) {
|
|
111
|
-
throw new Error(`Todo ${i + 1}: activeForm is longer than ${MAX_CONTENT} characters.`);
|
|
112
|
-
}
|
|
113
|
-
if (status === "in_progress" && requireActiveForm && !activeForm) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Todo ${i + 1}: activeForm is required for an in_progress item (present-continuous, e.g. "Running tests").`,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
todos.push(activeForm ? { content, status, activeForm } : { content, status });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (inProgress > 1) {
|
|
122
|
-
throw new Error("At most one todo may be in_progress; mark the rest pending or completed.");
|
|
123
|
-
}
|
|
124
|
-
return todos;
|
|
125
|
-
}
|
package/extensions/todo/state.ts
CHANGED
|
@@ -1,53 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Status queries over the list the branch published (pure).
|
|
3
3
|
*
|
|
4
|
-
* The list
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* values are re-validated and re-sanitized so corrupt or tampered branch data
|
|
8
|
-
* cannot inject escapes, crash reconstruction, or wipe a valid list.
|
|
4
|
+
* The list itself — the item contract, its validation, and the single strict
|
|
5
|
+
* replay — lives in `lib/todo-state.ts`, shared with `plan` and `goal`. These
|
|
6
|
+
* are the derived facts the widget and the transcript ask of it.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import { normalizeTodos } from "./schema.ts";
|
|
13
|
-
import type { Todo, TodoDetails } from "./types.ts";
|
|
14
|
-
|
|
15
|
-
/** Minimal shape of a session entry, used for runtime narrowing. */
|
|
16
|
-
interface BranchEntryLike {
|
|
17
|
-
type?: string;
|
|
18
|
-
message?: {
|
|
19
|
-
role?: string;
|
|
20
|
-
toolName?: string;
|
|
21
|
-
details?: unknown;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The last valid list written on the branch, or an empty list. */
|
|
26
|
-
export function reconstructTodos(entries: Iterable<unknown>): Todo[] {
|
|
27
|
-
let todos: Todo[] = [];
|
|
28
|
-
for (const raw of entries) {
|
|
29
|
-
const entry = raw as BranchEntryLike;
|
|
30
|
-
if (entry?.type !== "message") continue;
|
|
31
|
-
const message = entry.message;
|
|
32
|
-
if (message?.role !== "toolResult" || message.toolName !== TODO_TOOL) continue;
|
|
33
|
-
const details = message.details as TodoDetails | undefined;
|
|
34
|
-
if (!details || details.todos === undefined) continue;
|
|
35
|
-
// A rejected call carries the unchanged list for the model to read; it is
|
|
36
|
-
// not a state write and must not be replayed as one.
|
|
37
|
-
if (details.error) continue;
|
|
38
|
-
// Anything that does not re-validate as a todo list is ignored, so a
|
|
39
|
-
// malformed entry cannot wipe a valid one. Replay is lenient about
|
|
40
|
-
// `activeForm`: a list written before that field was required still loads
|
|
41
|
-
// instead of being dropped.
|
|
42
|
-
if (!Array.isArray(details.todos)) continue;
|
|
43
|
-
try {
|
|
44
|
-
todos = normalizeTodos(details.todos, { requireActiveForm: false });
|
|
45
|
-
} catch {
|
|
46
|
-
// Keep the previous list.
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return todos;
|
|
50
|
-
}
|
|
9
|
+
import type { Todo } from "./types.ts";
|
|
51
10
|
|
|
52
11
|
/** Number of completed items. */
|
|
53
12
|
export function completedCount(todos: Todo[]): number {
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -15,12 +15,13 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
15
15
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
16
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
17
17
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { normalizeTodos } from "../../lib/todo-state.ts";
|
|
18
19
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
19
20
|
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
20
21
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
21
22
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
22
23
|
import type { TodoRuntime } from "./runtime.ts";
|
|
23
|
-
import {
|
|
24
|
+
import { TodoParams, TodoResult, type TodoArgs } from "./schema.ts";
|
|
24
25
|
import { TodoResult as TodoResultView } from "./tui.ts";
|
|
25
26
|
import type { Todo, TodoDetails } from "./types.ts";
|
|
26
27
|
|
package/extensions/todo/types.ts
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the todo extension.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The item contract itself lives in `lib/todo-state.ts`, which owns the parser
|
|
5
|
+
* and the branch replay shared with `plan` and `goal`; this module re-exports it
|
|
6
|
+
* under the name the extension uses and adds the tool-result shape.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import type { ExistingTodo } from "../../lib/todo-state.ts";
|
|
10
10
|
|
|
11
|
-
export type TodoStatus
|
|
11
|
+
export { TODO_STATUSES, type TodoStatus } from "../../lib/todo-state.ts";
|
|
12
12
|
|
|
13
13
|
/** One item in the list. */
|
|
14
|
-
export type Todo =
|
|
15
|
-
/** Imperative description of the task, e.g. "Write the parser tests". */
|
|
16
|
-
content: string;
|
|
17
|
-
status: TodoStatus;
|
|
18
|
-
/**
|
|
19
|
-
* Present-continuous label shown while the item is in progress, e.g.
|
|
20
|
-
* "Writing the parser tests". Falls back to `content` when absent.
|
|
21
|
-
*/
|
|
22
|
-
activeForm?: string;
|
|
23
|
-
};
|
|
14
|
+
export type Todo = ExistingTodo;
|
|
24
15
|
|
|
25
16
|
/** Structured result carried in the tool's `details` and used for reconstruction. */
|
|
26
17
|
export interface TodoDetails {
|
|
@@ -103,6 +103,8 @@ render the tail of the sanitized lines, with the hidden count noted above and
|
|
|
103
103
|
|
|
104
104
|
### Command: /watch
|
|
105
105
|
|
|
106
|
+
<img src="../../assets/watch.png" alt="The /watch screen: a Watch title rule, a `2 watchers · 1 running · 3 matches` summary, a selected file watcher and a finished command watcher, the focused watcher's path and pattern with its line and match counts, and the key hints" width="100%">
|
|
107
|
+
|
|
106
108
|
A bare `/watch` opens a dock screen listing watchers. `j`/`k` (and the wheel)
|
|
107
109
|
move, `Enter`/`l` opens a watcher's matches, `x`/`K` stops a running watcher
|
|
108
110
|
behind a `y/N` confirm, `d` clears stopped ones behind a `y/N` confirm, `Esc`
|
|
@@ -195,6 +197,14 @@ other.
|
|
|
195
197
|
|
|
196
198
|
## Design notes
|
|
197
199
|
|
|
200
|
+
The shared seams are `lib/`'s: `lib/spawn.ts` (spawn, tree-kill), `lib/lines.ts`
|
|
201
|
+
(the byte-safe splitter), `lib/format.ts` (`sanitizeUntrustedLine`),
|
|
202
|
+
`lib/report.ts` (pending matches → notice → one wake turn),
|
|
203
|
+
`lib/tui/status-controller.ts` (chips and the alert), and
|
|
204
|
+
`lib/tui/dock-screen.ts` (the `/watch` frame, navigation, and confirm layer).
|
|
205
|
+
What stays here is the watcher policy: the tailer, the matcher, the source
|
|
206
|
+
model, the command-child lifecycle in `child.ts`, and the watcher vocabulary.
|
|
207
|
+
|
|
198
208
|
The file tailer polls and exposes `tick()` publicly; the interval is a thin
|
|
199
209
|
wrapper over `tick()`, so tests drive matching deterministically with no real
|
|
200
210
|
timer. A command tailer is event-driven and feeds both output streams through
|
|
@@ -212,15 +222,15 @@ outlive the session.
|
|
|
212
222
|
|
|
213
223
|
| Module | Responsibility |
|
|
214
224
|
|---|---|
|
|
215
|
-
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle and the shared notices. |
|
|
225
|
+
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle, the shared `lib/report.ts` match reporter, and the shared notices. |
|
|
216
226
|
| `tools.ts` | The `watch` tool definition, execution, and transcript rendering. |
|
|
217
227
|
| `schema.ts` | TypeBox parameters and pure `normalizeCall` validation. |
|
|
218
228
|
| `types.ts` | `WatchAction`/`WatchStatus`, `WatchRecord`, `WatchDetails`, `PendingMatch`. |
|
|
219
|
-
| `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown. |
|
|
220
|
-
| `
|
|
221
|
-
| `format.ts` | Pure sanitization
|
|
222
|
-
| `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts
|
|
223
|
-
| `tui.ts` | `WatchResult` transcript block and the `/watch`
|
|
229
|
+
| `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown; spawning and tree-kill come from `child.ts`, line splitting from `lib/lines.ts`. |
|
|
230
|
+
| `child.ts` | A command watcher's process lifecycle: spawn, the SIGTERM → SIGKILL grace, and the shutdown drain, over the injected `SpawnFn`/`KillTreeFn` seams. |
|
|
231
|
+
| `format.ts` | Pure text formatting; sanitization is `lib/format.ts`'s `sanitizeUntrustedLine`. |
|
|
232
|
+
| `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts` (chips, attached context, `notify`). |
|
|
233
|
+
| `tui.ts` | The `WatchResult` transcript block over the shared `TailResult`, and the `/watch` screen as a `DockListScreen` subclass: watcher rows, focus block, counts, verbs. |
|
|
224
234
|
| `commands.ts` | The `/watch` command and its completions. |
|
|
225
235
|
| `config.ts` | The `watch` config section load and normalization. |
|
|
226
236
|
|