@gavin-hu/my-pi-agent 0.14.0 → 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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -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/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 +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- 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 +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- 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/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 +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -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 +61 -6
- 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 +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -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 +14 -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 +1 -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 +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -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 +22 -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/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -197,6 +197,14 @@ other.
|
|
|
197
197
|
|
|
198
198
|
## Design notes
|
|
199
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
|
+
|
|
200
208
|
The file tailer polls and exposes `tick()` publicly; the interval is a thin
|
|
201
209
|
wrapper over `tick()`, so tests drive matching deterministically with no real
|
|
202
210
|
timer. A command tailer is event-driven and feeds both output streams through
|
|
@@ -214,15 +222,15 @@ outlive the session.
|
|
|
214
222
|
|
|
215
223
|
| Module | Responsibility |
|
|
216
224
|
|---|---|
|
|
217
|
-
| `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. |
|
|
218
226
|
| `tools.ts` | The `watch` tool definition, execution, and transcript rendering. |
|
|
219
227
|
| `schema.ts` | TypeBox parameters and pure `normalizeCall` validation. |
|
|
220
228
|
| `types.ts` | `WatchAction`/`WatchStatus`, `WatchRecord`, `WatchDetails`, `PendingMatch`. |
|
|
221
|
-
| `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown. |
|
|
222
|
-
| `
|
|
223
|
-
| `format.ts` | Pure sanitization
|
|
224
|
-
| `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts
|
|
225
|
-
| `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. |
|
|
226
234
|
| `commands.ts` | The `/watch` command and its completions. |
|
|
227
235
|
| `config.ts` | The `watch` config section load and normalization. |
|
|
228
236
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command watcher's process lifecycle: spawn, tree-kill, escalate, and drain.
|
|
3
|
+
*
|
|
4
|
+
* A command source is a spawned process whose merged stdout and stderr feed the
|
|
5
|
+
* matcher. This module owns the process side only — the spawn, the SIGTERM →
|
|
6
|
+
* SIGKILL grace, and the shutdown drain — over the injected `SpawnFn` and
|
|
7
|
+
* `KillTreeFn` seams, so the runtime keeps the watcher's status, its repaint,
|
|
8
|
+
* and its clock.
|
|
9
|
+
*
|
|
10
|
+
* Value-only: the child handle and its escalation timer live on the caller's
|
|
11
|
+
* slot, never here, because Pi loads each extension with its own module cache.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { defaultKillTree, defaultSpawn, type KillTreeFn, type SpawnFn, type SpawnedProcess } from "../../lib/spawn.ts";
|
|
15
|
+
|
|
16
|
+
/** Grace period before a stopped command is escalated to SIGKILL. */
|
|
17
|
+
export const KILL_GRACE_MS = 2000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The process fields a watcher record carries. The supervisor mutates them in
|
|
21
|
+
* place, so the runtime's record and the module's handle cannot disagree about
|
|
22
|
+
* whether a child is still live.
|
|
23
|
+
*/
|
|
24
|
+
export interface ChildSlot {
|
|
25
|
+
pid?: number;
|
|
26
|
+
child?: SpawnedProcess;
|
|
27
|
+
killTimer?: ReturnType<typeof setTimeout>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** How a child settled, for the sink that owns the watcher's status transition. */
|
|
31
|
+
export type ChildOutcome =
|
|
32
|
+
| { kind: "closed"; code: number | null; signal: string | null }
|
|
33
|
+
| { kind: "failed"; message: string };
|
|
34
|
+
|
|
35
|
+
export interface ChildSink {
|
|
36
|
+
/** One chunk of merged stdout/stderr. */
|
|
37
|
+
onData(chunk: Buffer): void;
|
|
38
|
+
/**
|
|
39
|
+
* The child closed or failed to spawn. The sink owns the status, because only
|
|
40
|
+
* it knows whether the watcher was still running when that happened.
|
|
41
|
+
*/
|
|
42
|
+
onSettled(outcome: ChildOutcome): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChildSupervisorOptions {
|
|
46
|
+
spawn?: SpawnFn;
|
|
47
|
+
killTree?: KillTreeFn;
|
|
48
|
+
/** Grace before SIGTERM escalates to SIGKILL; default {@link KILL_GRACE_MS}. */
|
|
49
|
+
graceMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChildSupervisor {
|
|
53
|
+
/** Spawn `command`, or return the spawn error and leave the slot's child unset. */
|
|
54
|
+
start(slot: ChildSlot, command: string, cwd: string, sink: ChildSink): string | undefined;
|
|
55
|
+
/** SIGTERM a command, escalating to SIGKILL when it has not closed in time. */
|
|
56
|
+
terminate(slot: ChildSlot): void;
|
|
57
|
+
/** Kill immediately, without arming an escalation. */
|
|
58
|
+
kill(slot: ChildSlot, signal: "SIGTERM" | "SIGKILL"): void;
|
|
59
|
+
/** Cancel a pending escalation. */
|
|
60
|
+
clearTimer(slot: ChildSlot): void;
|
|
61
|
+
/** Resolve when every slot's child has closed, or after `ms`, whichever comes first. */
|
|
62
|
+
wait(slots: readonly ChildSlot[], ms: number): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createChildSupervisor(options: ChildSupervisorOptions = {}): ChildSupervisor {
|
|
66
|
+
const spawnFn = options.spawn ?? defaultSpawn;
|
|
67
|
+
const killTree = options.killTree ?? defaultKillTree;
|
|
68
|
+
const graceMs = options.graceMs ?? KILL_GRACE_MS;
|
|
69
|
+
|
|
70
|
+
const clearTimer = (slot: ChildSlot): void => {
|
|
71
|
+
if (slot.killTimer) {
|
|
72
|
+
clearTimeout(slot.killTimer);
|
|
73
|
+
slot.killTimer = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const kill = (slot: ChildSlot, signal: "SIGTERM" | "SIGKILL"): void => {
|
|
78
|
+
const pid = slot.pid;
|
|
79
|
+
try {
|
|
80
|
+
if (pid !== undefined) killTree(pid, signal, signal === "SIGKILL");
|
|
81
|
+
else slot.child?.kill(signal);
|
|
82
|
+
} catch {
|
|
83
|
+
// Best-effort: the child may already be gone.
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const terminate = (slot: ChildSlot): void => {
|
|
88
|
+
clearTimer(slot);
|
|
89
|
+
kill(slot, "SIGTERM");
|
|
90
|
+
if (slot.pid === undefined && !slot.child) return;
|
|
91
|
+
slot.killTimer = setTimeout(() => {
|
|
92
|
+
slot.killTimer = undefined;
|
|
93
|
+
if (slot.child) kill(slot, "SIGKILL");
|
|
94
|
+
}, graceMs);
|
|
95
|
+
slot.killTimer.unref?.();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const start = (slot: ChildSlot, command: string, cwd: string, sink: ChildSink): string | undefined => {
|
|
99
|
+
let child: SpawnedProcess;
|
|
100
|
+
try {
|
|
101
|
+
child = spawnFn(command, { cwd });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return error instanceof Error ? error.message : String(error);
|
|
104
|
+
}
|
|
105
|
+
slot.child = child;
|
|
106
|
+
slot.pid = child.pid;
|
|
107
|
+
const onData = (chunk: Buffer): void => sink.onData(chunk);
|
|
108
|
+
child.stdout?.on("data", onData);
|
|
109
|
+
child.stderr?.on("data", onData);
|
|
110
|
+
child.on("close", (code, signal) => {
|
|
111
|
+
clearTimer(slot);
|
|
112
|
+
slot.child = undefined;
|
|
113
|
+
sink.onSettled({ kind: "closed", code, signal });
|
|
114
|
+
});
|
|
115
|
+
child.on("error", (error) => {
|
|
116
|
+
clearTimer(slot);
|
|
117
|
+
slot.child = undefined;
|
|
118
|
+
sink.onSettled({ kind: "failed", message: error.message });
|
|
119
|
+
});
|
|
120
|
+
return undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const wait = (slots: readonly ChildSlot[], ms: number): Promise<void> => {
|
|
124
|
+
const alive = slots.filter((slot) => slot.child !== undefined);
|
|
125
|
+
if (alive.length === 0) return Promise.resolve();
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
let remaining = alive.length;
|
|
128
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
129
|
+
const settle = (): void => {
|
|
130
|
+
if (--remaining > 0) return;
|
|
131
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
132
|
+
resolve();
|
|
133
|
+
};
|
|
134
|
+
for (const slot of alive) {
|
|
135
|
+
let done = false;
|
|
136
|
+
const once = (): void => {
|
|
137
|
+
if (done) return;
|
|
138
|
+
done = true;
|
|
139
|
+
settle();
|
|
140
|
+
};
|
|
141
|
+
slot.child?.on("close", once);
|
|
142
|
+
slot.child?.on("error", once);
|
|
143
|
+
}
|
|
144
|
+
timer = setTimeout(resolve, ms);
|
|
145
|
+
timer.unref?.();
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return { start, terminate, kill, clearTimer, wait };
|
|
150
|
+
}
|
|
@@ -6,50 +6,23 @@
|
|
|
6
6
|
* status chip, the `/watch` screen, the transcript, and the model.
|
|
7
7
|
*
|
|
8
8
|
* File and command output is untrusted, so every line that reaches a widget or
|
|
9
|
-
* the model goes through `
|
|
9
|
+
* the model goes through `sanitizeUntrustedLine` from `lib/format.ts` at the
|
|
10
|
+
* boundary; `sanitizeLine` covers the extension's own trusted text.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
|
-
import { clip, labelLine } from "../../lib/tui/columns.ts";
|
|
14
|
+
import { boundedTail, sanitizeLine } from "../../lib/format.ts";
|
|
15
|
+
import { clip, clipLabel, labelLine } from "../../lib/tui/columns.ts";
|
|
15
16
|
import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
17
|
+
import { byRunningFirst } from "../../lib/tui/rows.ts";
|
|
16
18
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
17
19
|
import type { PendingMatch, WatchRecord, WatchStatus } from "./types.ts";
|
|
18
20
|
|
|
19
|
-
/** Longest sanitized watched line kept for display; longer lines are clipped. */
|
|
20
|
-
const MAX_WATCH_LINE = 200;
|
|
21
|
-
|
|
22
|
-
/** ESC-introduced sequences: OSC, CSI, and single-character escapes. */
|
|
23
|
-
const OSC = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
24
|
-
const CSI = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
25
|
-
const OTHER_ESC = /\u001b[@-Z\\-_]/g;
|
|
26
|
-
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Make one line of untrusted file/command output safe to render.
|
|
30
|
-
*
|
|
31
|
-
* Resolves carriage returns the way a terminal would (keep the text after the
|
|
32
|
-
* last `\r`, so progress rewrites collapse to their final state), strips ANSI
|
|
33
|
-
* escapes and control characters, collapses whitespace, and clips the result.
|
|
34
|
-
* `sanitizeLine` alone would leave the printable tail of an ANSI sequence.
|
|
35
|
-
*/
|
|
36
|
-
export function sanitizeWatchLine(raw: string): string {
|
|
37
|
-
const withoutAnsi = raw.replace(OSC, "").replace(CSI, "").replace(OTHER_ESC, "");
|
|
38
|
-
const afterCr = withoutAnsi.slice(withoutAnsi.lastIndexOf("\r") + 1);
|
|
39
|
-
const cleaned = afterCr.replace(CONTROL, " ").replace(/\s+/g, " ").trim();
|
|
40
|
-
return cleaned.length > MAX_WATCH_LINE ? `${cleaned.slice(0, MAX_WATCH_LINE - 1)}…` : cleaned;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
/** Compile a pattern; case-insensitive unless `caseSensitive`. */
|
|
44
22
|
export function compileRegex(pattern: string, caseSensitive: boolean): RegExp {
|
|
45
23
|
return new RegExp(pattern, caseSensitive ? "" : "i");
|
|
46
24
|
}
|
|
47
25
|
|
|
48
|
-
/** A label clipped to one safe display line. */
|
|
49
|
-
export function clipLabel(text: string): string {
|
|
50
|
-
return labelLine(text, sanitizeWatchLine);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
26
|
/** What a watcher reads: a path or a command, clipped for display. */
|
|
54
27
|
export function sourceLabel(watch: WatchRecord): string {
|
|
55
28
|
if (watch.source.kind === "file") return clipLabel(watch.source.path ?? "");
|
|
@@ -89,11 +62,6 @@ export function statusGlyphChar(status: WatchStatus): string {
|
|
|
89
62
|
}
|
|
90
63
|
}
|
|
91
64
|
|
|
92
|
-
/** Width-1 themed glyph for a watcher status. */
|
|
93
|
-
export function statusGlyph(status: WatchStatus, theme: Theme): string {
|
|
94
|
-
return paint(theme, statusRole(status), statusGlyphChar(status));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
65
|
/** Short status word, including an exit code or error when present. */
|
|
98
66
|
export function statusWord(watch: WatchRecord): string {
|
|
99
67
|
if (watch.status === "error" && watch.error) return `error (${watch.error})`;
|
|
@@ -104,10 +72,7 @@ export function statusWord(watch: WatchRecord): string {
|
|
|
104
72
|
}
|
|
105
73
|
|
|
106
74
|
/** Order watchers for display: running first, then by most recent start. */
|
|
107
|
-
export
|
|
108
|
-
const rank = (watch: WatchRecord): number => (watch.status === "running" ? 0 : 1);
|
|
109
|
-
return rank(a) - rank(b) || b.startedAt - a.startedAt;
|
|
110
|
-
}
|
|
75
|
+
export const compareWatches = byRunningFirst<WatchRecord>((watch) => watch.status === "running");
|
|
111
76
|
|
|
112
77
|
/** One-line watcher summary shared by model output and the `/watch` screen. */
|
|
113
78
|
function watchSummary(watch: WatchRecord, now: number): string {
|
|
@@ -155,12 +120,7 @@ export function formatTail(
|
|
|
155
120
|
truncated: false,
|
|
156
121
|
};
|
|
157
122
|
}
|
|
158
|
-
|
|
159
|
-
let truncated = false;
|
|
160
|
-
if (body.length > maxChars) {
|
|
161
|
-
body = body.slice(body.length - maxChars);
|
|
162
|
-
truncated = true;
|
|
163
|
-
}
|
|
123
|
+
const { text: body, truncated } = boundedTail(lines, maxChars);
|
|
164
124
|
const suffix = truncated ? `\n… truncated (${lines.length} lines total)` : "";
|
|
165
125
|
return { text: `${watch.id} (${statusWord(watch)}) ${label}:\n${body}${suffix}`, truncated };
|
|
166
126
|
}
|
|
@@ -199,7 +159,7 @@ export function formatWatchOutcomeLine(watch: WatchRecord, theme: Theme, now = D
|
|
|
199
159
|
? paint(theme, "bad", statusWord(watch))
|
|
200
160
|
: paint(theme, "faint", `${watch.matchCount} match${watch.matchCount === 1 ? "" : "es"}`);
|
|
201
161
|
return (
|
|
202
|
-
|
|
162
|
+
paint(theme, statusRole(watch.status), statusGlyphChar(watch.status)) +
|
|
203
163
|
" " +
|
|
204
164
|
paint(theme, "accent", watch.id) +
|
|
205
165
|
" " +
|
|
@@ -14,10 +14,11 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
15
|
import { WATCH_MATCH_CHANNEL, type WatchMatchEvent } from "../../lib/events.ts";
|
|
16
16
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
17
|
-
import { noticeMessage, type NoticeMessage
|
|
18
|
-
import {
|
|
17
|
+
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
18
|
+
import { createPendingReporter } from "../../lib/report.ts";
|
|
19
19
|
import { registerCommands } from "./commands.ts";
|
|
20
|
-
import { clipLabel
|
|
20
|
+
import { clipLabel } from "../../lib/tui/columns.ts";
|
|
21
|
+
import { formatPendingMatch } from "./format.ts";
|
|
21
22
|
import { createWatchRuntime, type WatchRuntime } from "./runtime.ts";
|
|
22
23
|
import { registerTools } from "./tools.ts";
|
|
23
24
|
import type { PendingMatch } from "./types.ts";
|
|
@@ -62,53 +63,46 @@ export interface WatchDeps {
|
|
|
62
63
|
export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
63
64
|
if (!isExtensionEnabled("watch")) return;
|
|
64
65
|
const runtime = deps.runtime ?? createWatchRuntime();
|
|
65
|
-
let busy = false;
|
|
66
|
-
let waking = false;
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (pending.length === 0) return;
|
|
78
|
-
try {
|
|
79
|
-
pi.sendMessage(watchNotice(pending), { triggerTurn: true });
|
|
80
|
-
runtime.markReported(pending);
|
|
81
|
-
waking = true;
|
|
82
|
-
} catch {
|
|
83
|
-
// Waking is best-effort; the next turn still reports the matches.
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
67
|
+
// Opt-in wake: a `wake` watcher matched while the agent is idle. The reporter
|
|
68
|
+
// reports *all* pending matches, not just the waking one, so a non-wake match
|
|
69
|
+
// is not silently dropped.
|
|
70
|
+
const reporter = createPendingReporter<PendingMatch>({
|
|
71
|
+
wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
|
|
72
|
+
pending: () => runtime.pendingMatches(),
|
|
73
|
+
markReported: (matches) => runtime.markReported(matches),
|
|
74
|
+
buildNotice: watchNotice,
|
|
75
|
+
source: "watch",
|
|
86
76
|
// A wake means the user is away from the terminal, so the alert also leaves
|
|
87
77
|
// the process. bytefly's notify capability owns delivery; this publishes the
|
|
88
78
|
// fact and never learns how it is sent. The newest match is the one an alert
|
|
89
79
|
// is about, while the count covers the whole cycle.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
onWoke: (matches) => {
|
|
81
|
+
const newest = matches[matches.length - 1];
|
|
82
|
+
if (!newest) return;
|
|
83
|
+
try {
|
|
84
|
+
const event: WatchMatchEvent = {
|
|
85
|
+
count: matches.length,
|
|
86
|
+
watcherId: newest.watcherId,
|
|
87
|
+
label: newest.label,
|
|
88
|
+
line: newest.line,
|
|
89
|
+
};
|
|
90
|
+
pi.events.emit(WATCH_MATCH_CHANNEL, event);
|
|
91
|
+
} catch {
|
|
92
|
+
// A throwing listener must not cost the wake that already happened.
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
registerTools(pi, runtime);
|
|
98
|
+
registerCommands(pi, runtime);
|
|
104
99
|
|
|
105
100
|
pi.on("session_start", (_event, ctx) => {
|
|
106
|
-
|
|
107
|
-
waking = false;
|
|
101
|
+
reporter.sessionStart();
|
|
108
102
|
runtime.load(ctx);
|
|
109
103
|
// `shutdown` clears this; a reload or session replacement reuses the
|
|
110
104
|
// runtime, so re-arm it on every session start.
|
|
111
|
-
runtime.onMatch =
|
|
105
|
+
runtime.onMatch = () => reporter.report();
|
|
112
106
|
});
|
|
113
107
|
|
|
114
108
|
// Tree navigation does not change the watcher table; only repaint.
|
|
@@ -117,37 +111,27 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
117
111
|
});
|
|
118
112
|
|
|
119
113
|
pi.on("agent_start", () => {
|
|
120
|
-
|
|
114
|
+
reporter.agentStart();
|
|
121
115
|
});
|
|
122
116
|
pi.on("agent_settled", () => {
|
|
123
|
-
|
|
124
|
-
waking = false;
|
|
117
|
+
reporter.agentSettled();
|
|
125
118
|
});
|
|
126
119
|
|
|
127
120
|
// Report matches to the model at the start of the next turn.
|
|
128
121
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
runtime.markReported(pending);
|
|
122
|
+
const reported = reporter.beforeAgentStart();
|
|
123
|
+
if (!reported) return undefined;
|
|
132
124
|
runtime.setStatus(ctx);
|
|
133
|
-
return
|
|
125
|
+
return reported;
|
|
134
126
|
});
|
|
135
127
|
|
|
136
128
|
// Keep stale match notices out of later turns. Only this extension's notices
|
|
137
129
|
// are pruned; another source's are left for its own handler.
|
|
138
|
-
pi.on("context", (event) =>
|
|
139
|
-
const pruned = pruneStaleNotices(event.messages, "watch");
|
|
140
|
-
return pruned ? { messages: pruned } : undefined;
|
|
141
|
-
});
|
|
130
|
+
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
142
131
|
|
|
143
|
-
|
|
132
|
+
// `shutdown` detaches the UI controller, which clears both chips.
|
|
133
|
+
pi.on("session_shutdown", async () => {
|
|
144
134
|
runtime.onMatch = undefined;
|
|
145
135
|
await runtime.shutdown();
|
|
146
|
-
try {
|
|
147
|
-
ctx.ui.setStatus(STATUS_KEYS.watch, undefined);
|
|
148
|
-
ctx.ui.setStatus(STATUS_KEYS.watchMatches, undefined);
|
|
149
|
-
} catch {
|
|
150
|
-
// UI may already be gone.
|
|
151
|
-
}
|
|
152
136
|
});
|
|
153
137
|
}
|