@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,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
|
}
|
|
@@ -12,17 +12,18 @@ import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
|
12
12
|
import { isAbsolute, resolve } from "node:path";
|
|
13
13
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
15
|
+
import { LineSplitter } from "../../lib/lines.ts";
|
|
16
|
+
import type { KillTreeFn, SpawnFn, SpawnedProcess } from "../../lib/spawn.ts";
|
|
17
|
+
import { createChildSupervisor } from "./child.ts";
|
|
15
18
|
import { DEFAULT_CONFIG, loadConfig, type WatchConfig } from "./config.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
19
|
+
import { sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
20
|
+
import { compileRegex, sourceLabel } from "./format.ts";
|
|
18
21
|
import { createUiController, type UiController } from "./ui.ts";
|
|
19
22
|
import type { PendingMatch, WatchMatch, WatchRecord, WatchSource } from "./types.ts";
|
|
20
23
|
|
|
21
24
|
/** Largest slice read from a file per tick. */
|
|
22
25
|
const MAX_CHUNK = 1024 * 1024;
|
|
23
|
-
/**
|
|
24
|
-
const MAX_LINE_BYTES = 1024 * 1024;
|
|
25
|
-
/** Grace period before a stopped command is escalated to SIGKILL. */
|
|
26
|
+
/** Grace before a stopped command escalates to SIGKILL; `child.ts` defaults the same. */
|
|
26
27
|
const KILL_GRACE_MS = 2000;
|
|
27
28
|
|
|
28
29
|
/** Internal watcher state: the public record plus non-serializable fields. */
|
|
@@ -48,7 +49,7 @@ interface Internal {
|
|
|
48
49
|
regex: RegExp;
|
|
49
50
|
excludes: RegExp[];
|
|
50
51
|
offset: number;
|
|
51
|
-
|
|
52
|
+
lines: LineSplitter;
|
|
52
53
|
rawLines: string[];
|
|
53
54
|
matches: WatchMatch[];
|
|
54
55
|
child?: SpawnedProcess;
|
|
@@ -103,8 +104,11 @@ export interface WatchRuntimeOptions {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRuntime {
|
|
106
|
-
const
|
|
107
|
-
|
|
107
|
+
const children = createChildSupervisor({
|
|
108
|
+
spawn: options.spawn,
|
|
109
|
+
killTree: options.killTree,
|
|
110
|
+
graceMs: options.killGraceMs,
|
|
111
|
+
});
|
|
108
112
|
const now = options.now ?? (() => Date.now());
|
|
109
113
|
const fixedConfig = options.config;
|
|
110
114
|
const autoClock = options.autoClock ?? true;
|
|
@@ -208,7 +212,7 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
208
212
|
for (const exclude of w.excludes) if (exclude.test(line)) return false;
|
|
209
213
|
if (!w.regex.test(line)) return false;
|
|
210
214
|
|
|
211
|
-
const clean =
|
|
215
|
+
const clean = sanitizeUntrustedLine(line);
|
|
212
216
|
w.matches.push({ seq: ++w.seq, line: clean, at: time, seen: false });
|
|
213
217
|
if (w.matches.length > config.maxMatches) {
|
|
214
218
|
w.matches.splice(0, w.matches.length - config.maxMatches);
|
|
@@ -224,85 +228,16 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
224
228
|
};
|
|
225
229
|
|
|
226
230
|
const consume = (w: Internal, chunk: Buffer, time: number): void => {
|
|
227
|
-
const combined = w.pending.length > 0 ? Buffer.concat([w.pending, chunk]) : chunk;
|
|
228
|
-
let start = 0;
|
|
229
231
|
let matched = false;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (newline < 0) break;
|
|
233
|
-
if (emit(w, combined.subarray(start, newline).toString("utf-8"), time)) matched = true;
|
|
234
|
-
start = newline + 1;
|
|
235
|
-
}
|
|
236
|
-
let rest = combined.subarray(start);
|
|
237
|
-
if (rest.length > MAX_LINE_BYTES) {
|
|
238
|
-
if (emit(w, rest.toString("utf-8"), time)) matched = true;
|
|
239
|
-
rest = Buffer.alloc(0);
|
|
232
|
+
for (const line of w.lines.push(chunk)) {
|
|
233
|
+
if (emit(w, line, time)) matched = true;
|
|
240
234
|
}
|
|
241
|
-
w.pending = Buffer.from(rest);
|
|
242
235
|
if (matched && w.wake) fireOnMatch();
|
|
243
236
|
};
|
|
244
237
|
|
|
238
|
+
/** A source that ended mid-line still reports that line. */
|
|
245
239
|
const flushPending = (w: Internal, time: number): void => {
|
|
246
|
-
|
|
247
|
-
const rest = w.pending.toString("utf-8");
|
|
248
|
-
w.pending = Buffer.alloc(0);
|
|
249
|
-
emit(w, rest, time);
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const clearKillTimer = (w: Internal): void => {
|
|
253
|
-
if (w.killTimer) {
|
|
254
|
-
clearTimeout(w.killTimer);
|
|
255
|
-
w.killTimer = undefined;
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const killChild = (w: Internal, signal: "SIGTERM" | "SIGKILL"): void => {
|
|
260
|
-
const pid = w.pid;
|
|
261
|
-
try {
|
|
262
|
-
if (pid !== undefined) killTree(pid, signal, signal === "SIGKILL");
|
|
263
|
-
else w.child?.kill(signal);
|
|
264
|
-
} catch {
|
|
265
|
-
// Best-effort: the child may already be gone.
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
/** SIGTERM a command, escalating to SIGKILL when it has not closed in time. */
|
|
270
|
-
const terminateChild = (w: Internal): void => {
|
|
271
|
-
clearKillTimer(w);
|
|
272
|
-
killChild(w, "SIGTERM");
|
|
273
|
-
if (w.pid === undefined && !w.child) return;
|
|
274
|
-
w.killTimer = setTimeout(() => {
|
|
275
|
-
w.killTimer = undefined;
|
|
276
|
-
if (w.child) killChild(w, "SIGKILL");
|
|
277
|
-
}, killGraceMs);
|
|
278
|
-
w.killTimer.unref?.();
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
/** Resolve when every command has closed, or after `ms`, whichever comes first. */
|
|
282
|
-
const waitForChildren = (watches: Internal[], ms: number): Promise<void> => {
|
|
283
|
-
const alive = watches.filter((w) => w.child !== undefined);
|
|
284
|
-
if (alive.length === 0) return Promise.resolve();
|
|
285
|
-
return new Promise((resolve) => {
|
|
286
|
-
let remaining = alive.length;
|
|
287
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
288
|
-
const settle = (): void => {
|
|
289
|
-
if (--remaining > 0) return;
|
|
290
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
291
|
-
resolve();
|
|
292
|
-
};
|
|
293
|
-
for (const w of alive) {
|
|
294
|
-
let done = false;
|
|
295
|
-
const once = (): void => {
|
|
296
|
-
if (done) return;
|
|
297
|
-
done = true;
|
|
298
|
-
settle();
|
|
299
|
-
};
|
|
300
|
-
w.child?.on("close", once);
|
|
301
|
-
w.child?.on("error", once);
|
|
302
|
-
}
|
|
303
|
-
timer = setTimeout(resolve, ms);
|
|
304
|
-
timer.unref?.();
|
|
305
|
-
});
|
|
240
|
+
for (const line of w.lines.flush()) emit(w, line, time);
|
|
306
241
|
};
|
|
307
242
|
|
|
308
243
|
const pollFile = (w: Internal, time: number): void => {
|
|
@@ -317,7 +252,7 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
317
252
|
}
|
|
318
253
|
if (size < w.offset) {
|
|
319
254
|
w.offset = 0;
|
|
320
|
-
w.
|
|
255
|
+
w.lines.reset();
|
|
321
256
|
}
|
|
322
257
|
if (w.offset >= size) return;
|
|
323
258
|
|
|
@@ -344,42 +279,28 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
344
279
|
};
|
|
345
280
|
|
|
346
281
|
const startCommand = (w: Internal): void => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (w.status !== "running") return;
|
|
365
|
-
flushPending(w, now());
|
|
366
|
-
w.status = "exited";
|
|
367
|
-
w.exitCode = code;
|
|
368
|
-
w.signal = signal;
|
|
369
|
-
w.endedAt = now();
|
|
370
|
-
ui.paint();
|
|
371
|
-
if (runningCount() === 0) stopClock();
|
|
372
|
-
});
|
|
373
|
-
child.on("error", (error) => {
|
|
374
|
-
clearKillTimer(w);
|
|
375
|
-
w.child = undefined;
|
|
376
|
-
if (w.status !== "running") return;
|
|
377
|
-
w.status = "error";
|
|
378
|
-
w.error = error.message;
|
|
379
|
-
w.endedAt = now();
|
|
380
|
-
ui.paint();
|
|
381
|
-
if (runningCount() === 0) stopClock();
|
|
282
|
+
const error = children.start(w, w.source.command ?? "", w.source.cwd ?? process.cwd(), {
|
|
283
|
+
onData: (chunk) => consume(w, chunk, now()),
|
|
284
|
+
onSettled: (outcome) => {
|
|
285
|
+
if (w.status !== "running") return;
|
|
286
|
+
if (outcome.kind === "closed") {
|
|
287
|
+
flushPending(w, now());
|
|
288
|
+
w.status = "exited";
|
|
289
|
+
w.exitCode = outcome.code;
|
|
290
|
+
w.signal = outcome.signal;
|
|
291
|
+
} else {
|
|
292
|
+
w.status = "error";
|
|
293
|
+
w.error = outcome.message;
|
|
294
|
+
}
|
|
295
|
+
w.endedAt = now();
|
|
296
|
+
ui.paint();
|
|
297
|
+
if (runningCount() === 0) stopClock();
|
|
298
|
+
},
|
|
382
299
|
});
|
|
300
|
+
if (error === undefined) return;
|
|
301
|
+
w.status = "error";
|
|
302
|
+
w.error = error;
|
|
303
|
+
w.endedAt = now();
|
|
383
304
|
};
|
|
384
305
|
|
|
385
306
|
const tick = (): void => {
|
|
@@ -443,7 +364,7 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
443
364
|
regex,
|
|
444
365
|
excludes,
|
|
445
366
|
offset,
|
|
446
|
-
|
|
367
|
+
lines: new LineSplitter(),
|
|
447
368
|
rawLines: [],
|
|
448
369
|
matches: [],
|
|
449
370
|
notified: false,
|
|
@@ -476,13 +397,13 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
476
397
|
|
|
477
398
|
const matches = (id: string, lines?: number) => tail(id, lines, (w) => w.matches.map((m) => m.line));
|
|
478
399
|
const logs = (id: string, lines?: number) =>
|
|
479
|
-
tail(id, lines, (w) => w.rawLines.map((line) =>
|
|
400
|
+
tail(id, lines, (w) => w.rawLines.map((line) => sanitizeUntrustedLine(line)));
|
|
480
401
|
|
|
481
402
|
const stop = (id: string): boolean => {
|
|
482
403
|
const w = watches.get(id);
|
|
483
404
|
if (!w || w.status !== "running") return false;
|
|
484
405
|
flushPending(w, now());
|
|
485
|
-
|
|
406
|
+
children.terminate(w);
|
|
486
407
|
w.status = "stopped";
|
|
487
408
|
w.endedAt = now();
|
|
488
409
|
ui.paint();
|
|
@@ -535,8 +456,8 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
535
456
|
for (const w of watches.values()) {
|
|
536
457
|
if (w.status !== "running") continue;
|
|
537
458
|
w.status = "stopped";
|
|
538
|
-
|
|
539
|
-
|
|
459
|
+
children.clearTimer(w);
|
|
460
|
+
children.kill(w, "SIGKILL");
|
|
540
461
|
w.child = undefined;
|
|
541
462
|
}
|
|
542
463
|
watches.clear();
|
|
@@ -554,13 +475,13 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
554
475
|
flushPending(w, now());
|
|
555
476
|
w.status = "stopped";
|
|
556
477
|
w.endedAt = now();
|
|
557
|
-
|
|
558
|
-
|
|
478
|
+
children.clearTimer(w);
|
|
479
|
+
children.kill(w, "SIGTERM");
|
|
559
480
|
}
|
|
560
|
-
await
|
|
481
|
+
await children.wait(running, killGraceMs);
|
|
561
482
|
for (const w of running) {
|
|
562
|
-
|
|
563
|
-
if (w.child)
|
|
483
|
+
children.clearTimer(w);
|
|
484
|
+
if (w.child) children.kill(w, "SIGKILL");
|
|
564
485
|
}
|
|
565
486
|
watches.clear();
|
|
566
487
|
ui.detach();
|