@gavin-hu/my-pi-agent 0.21.0 → 0.22.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 +46 -31
- package/README.zh-CN.md +40 -26
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +49 -39
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -7
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/view/todo-screen.ts +122 -0
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +27 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +1 -1
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -4,18 +4,30 @@
|
|
|
4
4
|
* Each session owns one registry file per project, but a job is still owned by
|
|
5
5
|
* the session that started it. To tell a live owner from a dead one — so a new
|
|
6
6
|
* session can adopt a dead peer's records instead of reaping them blindly —
|
|
7
|
-
* each session writes a small heartbeat marker (its session id, pid,
|
|
8
|
-
* last-seen time) beside the registry
|
|
7
|
+
* each session writes a small heartbeat marker (its session id, pid, the process's
|
|
8
|
+
* start time, and last-seen time) beside the registry. Adoption reads those
|
|
9
9
|
* markers, so a live peer's jobs are left alone and a dead peer's are taken
|
|
10
10
|
* over.
|
|
11
11
|
*
|
|
12
|
+
* The heartbeat only ticks while a job runs, because that is when the repaint
|
|
13
|
+
* clock runs: a session that has finished its work stops writing, and its marker
|
|
14
|
+
* ages. That is why age alone is not death — the rule is the roster's own
|
|
15
|
+
* (`lib/session-roster.ts`'s `live` / `quiet` / `gone`), read here over a
|
|
16
|
+
* marker, and *only* `gone` lets another session take anything over. A marker
|
|
17
|
+
* whose process is alive is evidence of an owner, not litter, so pruning keeps
|
|
18
|
+
* it; the start token is what tells an idle session apart from a reused pid.
|
|
19
|
+
*
|
|
12
20
|
* Markers are best-effort: a missing directory or an unwritable file degrades
|
|
13
|
-
* to "
|
|
14
|
-
* (`
|
|
21
|
+
* to "no marker", which reads as an owner that is gone — the conservative
|
|
22
|
+
* default for adoption. The rule is pure (`sessionStateOf`) so it is unit-tested
|
|
23
|
+
* without real processes, and `createOwnership` is that rule wired to one
|
|
24
|
+
* runtime's directory, clock, and seams: the state of a peer, this session's own
|
|
25
|
+
* marker, and the pruning of the markers that are left.
|
|
15
26
|
*/
|
|
16
27
|
|
|
17
28
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
29
|
import { join } from "node:path";
|
|
30
|
+
import { type RosterSeams, type RosterState, rosterStateOf } from "../../lib/session-roster.ts";
|
|
19
31
|
import { sessionMarkerPath } from "./paths.ts";
|
|
20
32
|
|
|
21
33
|
/** Prefix shared by every marker file, so log/registry files are never parsed. */
|
|
@@ -27,6 +39,14 @@ export interface SessionMarker {
|
|
|
27
39
|
sessionId: string;
|
|
28
40
|
/** Host process pid, checked for liveness before trusting the timestamp. */
|
|
29
41
|
pid: number;
|
|
42
|
+
/**
|
|
43
|
+
* That process's start time, when the platform can say.
|
|
44
|
+
*
|
|
45
|
+
* A pid that is alive may be a different program than the one the marker named, and
|
|
46
|
+
* nothing cheaper than the start time tells them apart — so this is what keeps a
|
|
47
|
+
* reused pid from reading as an idle session forever.
|
|
48
|
+
*/
|
|
49
|
+
startToken?: string;
|
|
30
50
|
/** Millisecond timestamp of the last heartbeat. */
|
|
31
51
|
updatedAt: number;
|
|
32
52
|
}
|
|
@@ -39,16 +59,33 @@ function parseMarker(raw: unknown): SessionMarker | undefined {
|
|
|
39
59
|
if (typeof marker.sessionId !== "string" || typeof marker.pid !== "number" || typeof marker.updatedAt !== "number") {
|
|
40
60
|
return undefined;
|
|
41
61
|
}
|
|
42
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
sessionId: marker.sessionId,
|
|
64
|
+
pid: marker.pid,
|
|
65
|
+
...(typeof marker.startToken === "string" && marker.startToken !== "" ? { startToken: marker.startToken } : {}),
|
|
66
|
+
updatedAt: marker.updatedAt,
|
|
67
|
+
};
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
/** Write (or refresh) a session's marker. Failures are non-fatal for the caller. */
|
|
46
|
-
export function touchSessionMarker(
|
|
71
|
+
export function touchSessionMarker(
|
|
72
|
+
dir: string,
|
|
73
|
+
sessionId: string,
|
|
74
|
+
pid: number,
|
|
75
|
+
now: number,
|
|
76
|
+
startToken?: string,
|
|
77
|
+
): void {
|
|
47
78
|
try {
|
|
48
79
|
mkdirSync(dir, { recursive: true });
|
|
49
80
|
const path = sessionMarkerPath(dir, sessionId);
|
|
50
81
|
const tmp = `${path}.tmp`;
|
|
51
|
-
|
|
82
|
+
const marker: SessionMarker = {
|
|
83
|
+
sessionId,
|
|
84
|
+
pid,
|
|
85
|
+
...(startToken === undefined ? {} : { startToken }),
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
};
|
|
88
|
+
writeFileSync(tmp, JSON.stringify(marker), "utf-8");
|
|
52
89
|
renameSync(tmp, path);
|
|
53
90
|
} catch {
|
|
54
91
|
// A marker is best-effort; a missing one reads as a dead owner.
|
|
@@ -70,7 +107,7 @@ export function removeSessionMarker(dir: string, sessionId: string): void {
|
|
|
70
107
|
const path = sessionMarkerPath(dir, sessionId);
|
|
71
108
|
if (existsSync(path)) unlinkSync(path);
|
|
72
109
|
} catch {
|
|
73
|
-
// Best-effort; a leftover marker
|
|
110
|
+
// Best-effort; a leftover marker is reaped when its session is gone.
|
|
74
111
|
}
|
|
75
112
|
}
|
|
76
113
|
|
|
@@ -94,23 +131,99 @@ export function listSessionMarkers(dir: string): SessionMarker[] {
|
|
|
94
131
|
}
|
|
95
132
|
|
|
96
133
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
134
|
+
* What a marker says about its session: `live`, `quiet`, or `gone`.
|
|
135
|
+
*
|
|
136
|
+
* The rule is the roster's, asked over a marker, so `session-bus`, the board's page, and
|
|
137
|
+
* this extension cannot answer "is that session around" three ways. A dead pid is `gone`
|
|
138
|
+
* however fresh the heartbeat; a live pid with an aged heartbeat is `quiet`, which is the
|
|
139
|
+
* ordinary state of a session that has finished its jobs; and a pid that has since been
|
|
140
|
+
* reused is `gone` once the start token disagrees. A missing marker is `gone`: no file is
|
|
141
|
+
* no evidence of an owner.
|
|
99
142
|
*/
|
|
100
|
-
export function
|
|
143
|
+
export function sessionStateOf(
|
|
101
144
|
marker: SessionMarker | undefined,
|
|
102
145
|
now: number,
|
|
103
146
|
ttlMs: number,
|
|
104
|
-
|
|
105
|
-
):
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
seams: RosterSeams = {},
|
|
148
|
+
): RosterState {
|
|
149
|
+
if (marker === undefined) return "gone";
|
|
150
|
+
return rosterStateOf(
|
|
151
|
+
{
|
|
152
|
+
lastSeen: marker.updatedAt,
|
|
153
|
+
// A marker naming no usable process is judged by its heartbeat alone, which is the
|
|
154
|
+
// roster's answer for a record that cannot say which process owns it.
|
|
155
|
+
...(marker.pid > 0 ? { pid: marker.pid } : {}),
|
|
156
|
+
...(marker.startToken === undefined ? {} : { startToken: marker.startToken }),
|
|
157
|
+
},
|
|
158
|
+
now,
|
|
159
|
+
ttlMs,
|
|
160
|
+
seams,
|
|
161
|
+
);
|
|
109
162
|
}
|
|
110
163
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Delete the markers of sessions that are gone.
|
|
166
|
+
*
|
|
167
|
+
* A marker whose process is alive is kept, including one whose heartbeat has aged: that is
|
|
168
|
+
* what every idle session's marker looks like, and removing it would let a peer adopt a
|
|
169
|
+
* session that is still there. Only a session that has really gone — a dead pid, or a
|
|
170
|
+
* reused one — loses its marker.
|
|
171
|
+
*/
|
|
172
|
+
export function pruneSessionMarkers(dir: string, now: number, ttlMs: number, seams: RosterSeams = {}): void {
|
|
113
173
|
for (const marker of listSessionMarkers(dir)) {
|
|
114
|
-
if (
|
|
174
|
+
if (sessionStateOf(marker, now, ttlMs, seams) === "gone") removeSessionMarker(dir, marker.sessionId);
|
|
115
175
|
}
|
|
116
176
|
}
|
|
177
|
+
|
|
178
|
+
/** What the ownership rule needs from the runtime it serves. */
|
|
179
|
+
export interface OwnershipOptions {
|
|
180
|
+
/** The directory the markers sit in. */
|
|
181
|
+
directory(): string;
|
|
182
|
+
/** This session's id; it is never `gone`. */
|
|
183
|
+
sessionId(): string;
|
|
184
|
+
/** The clock the rule is read against. */
|
|
185
|
+
now(): number;
|
|
186
|
+
/** How old a beat may be before the marker's pid is re-checked for reuse. */
|
|
187
|
+
ttlMs(): number;
|
|
188
|
+
/** Whether a pid is still running. */
|
|
189
|
+
alive(pid: number): boolean;
|
|
190
|
+
/** That pid's start time, when the platform can say. */
|
|
191
|
+
startToken(pid: number): string | undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One runtime's view of the sessions that own jobs. */
|
|
195
|
+
export interface Ownership {
|
|
196
|
+
/** `live`, `quiet`, or `gone` for the session that owns a job. */
|
|
197
|
+
state(owner: string): RosterState;
|
|
198
|
+
/** Write this session's marker, naming its process and that process's start time. */
|
|
199
|
+
touch(): void;
|
|
200
|
+
/** Drop the markers of sessions that are gone, keeping a live or a quiet one. */
|
|
201
|
+
prune(): void;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Wire the rule to one runtime: what a peer's session is, this session's own marker, and
|
|
206
|
+
* the pruning of the markers nobody owns any more.
|
|
207
|
+
*/
|
|
208
|
+
export function createOwnership(options: OwnershipOptions): Ownership {
|
|
209
|
+
const seams = (): RosterSeams => ({ alive: options.alive, token: options.startToken });
|
|
210
|
+
// This process's token cannot change while the session runs, and on a platform without
|
|
211
|
+
// `ps` the ask costs a failed spawn, so it is asked once and kept, missing or not.
|
|
212
|
+
let token: string | undefined;
|
|
213
|
+
let asked = false;
|
|
214
|
+
const ownToken = (): string | undefined => {
|
|
215
|
+
if (!asked) {
|
|
216
|
+
asked = true;
|
|
217
|
+
token = options.startToken(process.pid);
|
|
218
|
+
}
|
|
219
|
+
return token;
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
state: (owner) =>
|
|
223
|
+
owner === options.sessionId()
|
|
224
|
+
? "live"
|
|
225
|
+
: sessionStateOf(readSessionMarker(options.directory(), owner), options.now(), options.ttlMs(), seams()),
|
|
226
|
+
touch: () => touchSessionMarker(options.directory(), options.sessionId(), process.pid, options.now(), ownToken()),
|
|
227
|
+
prune: () => pruneSessionMarkers(options.directory(), options.now(), options.ttlMs(), seams()),
|
|
228
|
+
};
|
|
229
|
+
}
|
package/extensions/job/store.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable job storage: the on-disk registry that outlives a session.
|
|
3
3
|
*
|
|
4
|
-
* The live
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The live table (the `Job` map) stays in the runtime and the handles stay in
|
|
5
|
+
* `child.ts`, because process events mutate both; this owns the parts that
|
|
6
|
+
* survive a session — the registry directory, the id counter, the atomic write of
|
|
7
|
+
* this session's own registry file, and the trusted artifact-path rules. Each
|
|
8
|
+
* session writes only its own file, so peers never race.
|
|
9
9
|
*
|
|
10
10
|
* Persistence is best-effort: an unwritable registry must never break a job that
|
|
11
11
|
* is otherwise running fine.
|
|
@@ -101,8 +101,8 @@ export function createJobStore(initialDir = registryDirFor(process.cwd())): JobS
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** Strip runtime-only fields for persistence. */
|
|
105
|
-
export function toRecord(job:
|
|
104
|
+
/** Strip runtime-only fields for persistence: the record, with or without a live handle. */
|
|
105
|
+
export function toRecord(job: JobRecord & { owned?: boolean }): JobRecord {
|
|
106
106
|
const { owned: _owned, ...record } = job;
|
|
107
107
|
return record;
|
|
108
108
|
}
|
package/extensions/job/tools.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import type { JobsRuntime } from "./runtime.ts";
|
|
35
35
|
import { JobParams, normalizeCall, type JobArgs, type JobCall } from "./schema.ts";
|
|
36
36
|
import { toRecord } from "./store.ts";
|
|
37
|
-
import { jobTail, type JobResultInput } from "./
|
|
37
|
+
import { jobTail, type JobResultInput } from "./render.ts";
|
|
38
38
|
import type { JobDetails, JobRecord } from "./types.ts";
|
|
39
39
|
|
|
40
40
|
export const TOOL_NAME = "job";
|
|
@@ -73,7 +73,7 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
73
73
|
try {
|
|
74
74
|
switch (call.action) {
|
|
75
75
|
case "start": {
|
|
76
|
-
const job = runtime.start(
|
|
76
|
+
const job = await runtime.start(
|
|
77
77
|
{
|
|
78
78
|
command: call.command,
|
|
79
79
|
cwd: call.cwd,
|
|
@@ -145,7 +145,7 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
145
145
|
case "kill": {
|
|
146
146
|
const before = runtime.get(call.id);
|
|
147
147
|
if (!before) return errorResult("kill", `no job "${call.id}".`);
|
|
148
|
-
const job = runtime.kill(call.id, call.signal);
|
|
148
|
+
const job = await runtime.kill(call.id, call.signal);
|
|
149
149
|
if (!job) return errorResult("kill", `no job "${call.id}".`);
|
|
150
150
|
const wasRunning = before.status === "running";
|
|
151
151
|
return textResult(
|
package/extensions/job/types.ts
CHANGED
|
@@ -59,6 +59,53 @@ export interface Job extends JobRecord {
|
|
|
59
59
|
owned: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface StartOptions {
|
|
63
|
+
command: string;
|
|
64
|
+
cwd?: string;
|
|
65
|
+
label?: string;
|
|
66
|
+
wake?: boolean;
|
|
67
|
+
detached?: boolean;
|
|
68
|
+
/** Give the child a stdin pipe, so `send` can write to it while it runs here. */
|
|
69
|
+
stdin?: boolean;
|
|
70
|
+
/** Auto-kill the job after this many milliseconds (SIGTERM, escalating to SIGKILL). */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface WaitResult {
|
|
75
|
+
job: Job;
|
|
76
|
+
timedOut: boolean;
|
|
77
|
+
cancelled: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A job's log as `logs` returns it: the model-facing render plus the raw tail. */
|
|
81
|
+
export interface LogResult {
|
|
82
|
+
job: Job;
|
|
83
|
+
/** Model-facing render of the tail, including status header/truncation note. */
|
|
84
|
+
text: string;
|
|
85
|
+
/** Sanitized tail lines, for the `/jobs` log pane. */
|
|
86
|
+
lines: string[];
|
|
87
|
+
truncated: boolean;
|
|
88
|
+
/** True when earlier log lines exist beyond the returned window. */
|
|
89
|
+
more: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SendOptions {
|
|
93
|
+
/** Text written to the job's stdin. */
|
|
94
|
+
input?: string;
|
|
95
|
+
/** Append a newline to `input`; defaults to true. */
|
|
96
|
+
newline?: boolean;
|
|
97
|
+
/** End the job's stdin after writing, so the child sees EOF. */
|
|
98
|
+
eof?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SendResult {
|
|
102
|
+
job: Job;
|
|
103
|
+
/** Characters of `input` written, excluding any newline this call added. */
|
|
104
|
+
sent: number;
|
|
105
|
+
/** True when this call ended the job's stdin. */
|
|
106
|
+
closed: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
62
109
|
/** Registry file contents. One file exists per session. */
|
|
63
110
|
export interface RegistryFile {
|
|
64
111
|
version: 1;
|
|
@@ -1,80 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `/jobs` screen: a dismissible list with a focused detail pane and a log
|
|
3
|
+
* view, drawn as a `DockListScreen`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* It is stateless with respect to time: elapsed values are computed from
|
|
6
|
+
* `Date.now()` at render, so the command can refresh the screen without
|
|
7
|
+
* rebuilding the component. The frame, the navigation keys, the y/N
|
|
8
|
+
* confirmation, and the log pane come from the shared `DockListScreen`, so this
|
|
9
|
+
* module owns only the job vocabulary: the rows, the focus block, the counts,
|
|
10
|
+
* the hints, and what the verbs do.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Log text arrives already sanitized by the runtime, so no untrusted escape
|
|
14
|
-
* sequence reaches the terminal through here.
|
|
12
|
+
* Full-screen surfaces live under `view/` and load on first use: the `/jobs`
|
|
13
|
+
* command imports this module behind its TUI branch, so registration never
|
|
14
|
+
* pays for it. See `docs/extensions/job.md`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
import type { ScreenHint, ViewportRowsSource } from "
|
|
20
|
-
import { DockListScreen, type DetailContents } from "
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import type {
|
|
24
|
-
import { paint } from "../../lib/tui/tokens.ts";
|
|
25
|
-
import {
|
|
26
|
-
compareJobs,
|
|
27
|
-
elapsedMs,
|
|
28
|
-
formatJobDetail,
|
|
29
|
-
jobCounts,
|
|
30
|
-
shortLabel,
|
|
31
|
-
statusGlyphChar,
|
|
32
|
-
statusRole,
|
|
33
|
-
type JobCounts,
|
|
34
|
-
} from "./format.ts";
|
|
35
|
-
import type { JobRecord } from "./types.ts";
|
|
36
|
-
|
|
37
|
-
/** One job row: selection marker, status glyph, id, label, and a right-aligned
|
|
38
|
-
* elapsed value, tagged `peer` when another session owns it. `statusRow` owns
|
|
39
|
-
* the clipping and the alignment. */
|
|
40
|
-
function jobRow(
|
|
41
|
-
job: JobRecord,
|
|
42
|
-
theme: Theme,
|
|
43
|
-
width: number,
|
|
44
|
-
options: Pick<StatusRow, "selected" | "marker"> & { foreign?: boolean } = {},
|
|
45
|
-
): string {
|
|
46
|
-
const elapsed = formatDuration(elapsedMs(job, Date.now()));
|
|
47
|
-
return statusRow(theme, width, {
|
|
48
|
-
...options,
|
|
49
|
-
glyph: statusGlyphChar(job.status),
|
|
50
|
-
glyphRole: statusRole(job.status),
|
|
51
|
-
label: `${job.id} ${shortLabel(job)}`,
|
|
52
|
-
labelRole: options.selected ? "accent" : "label",
|
|
53
|
-
// The tag rides the right-aligned cell, which survives a narrow terminal.
|
|
54
|
-
trailing: options.foreign ? `${elapsed}${SEPARATORS.item}peer` : elapsed,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Input for the transcript result: a themed job list or a log tail. */
|
|
59
|
-
export type JobResultInput =
|
|
60
|
-
| { kind: "list"; jobs: JobRecord[]; more: number; sessionId?: string }
|
|
61
|
-
| { kind: "logs"; lines: string[]; earlier: number };
|
|
62
|
-
|
|
63
|
-
/** Map the job vocabulary onto the shared transcript tail block. */
|
|
64
|
-
export function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
65
|
-
if (input.kind === "logs") {
|
|
66
|
-
return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: "lines" };
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
kind: "list",
|
|
70
|
-
rows: input.jobs,
|
|
71
|
-
more: input.more,
|
|
72
|
-
rowLine: (job, width, theme) => {
|
|
73
|
-
const foreign = input.sessionId !== undefined && job.sessionId !== input.sessionId;
|
|
74
|
-
return jobRow(job, theme, width, { marker: false, foreign });
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
18
|
+
import { sanitizeUntrustedLine } from "../../../lib/format.ts";
|
|
19
|
+
import type { ScreenHint, ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
20
|
+
import { DockListScreen, type DetailContents } from "../../../lib/tui/dock-screen.ts";
|
|
21
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
22
|
+
import { compareJobs, formatJobDetail, jobCounts, type JobCounts, jobRow, shortLabel } from "../format.ts";
|
|
23
|
+
import type { JobRecord } from "../types.ts";
|
|
78
24
|
|
|
79
25
|
export interface JobListCallbacks {
|
|
80
26
|
/** This session's id: a record with another id belongs to a peer session. */
|
|
@@ -161,8 +107,8 @@ export class JobListComponent extends DockListScreen<JobRecord> {
|
|
|
161
107
|
"Esc close",
|
|
162
108
|
"↑/↓ or j/k select",
|
|
163
109
|
{ text: "Enter or l logs", color: "accent" },
|
|
164
|
-
{ text: "d or K kill", color: "
|
|
165
|
-
{ text: "x clear finished", color: "
|
|
110
|
+
{ text: "d or K kill", color: "warn" },
|
|
111
|
+
{ text: "x clear finished", color: "warn" },
|
|
166
112
|
"PgUp/PgDn",
|
|
167
113
|
];
|
|
168
114
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `wait` action: waiter bookkeeping, and the promise a caller waits on.
|
|
3
3
|
*
|
|
4
4
|
* `wait` registers a resolver per job; a job's completion (or a kill/clear)
|
|
5
5
|
* wakes every waiter for that id. Kept separate from the runtime so the map and
|
|
6
6
|
* its cleanup rules are testable on their own and the runtime holds one field.
|
|
7
|
+
* The race the caller sees — the record, the timeout, or an abort — is here for
|
|
8
|
+
* the same reason: it is bookkeeping, not a decision about a record.
|
|
7
9
|
*/
|
|
8
10
|
|
|
11
|
+
/** Default `wait` timeout, for a caller that names none. */
|
|
12
|
+
export const DEFAULT_WAIT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
/** Why a `wait` stopped waiting; the caller reads the record itself. */
|
|
15
|
+
export interface WaitOutcome {
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
cancelled: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
/** Resolvers waiting on a job id. */
|
|
10
21
|
export interface Waiters {
|
|
11
22
|
/** Register `resolve` for `id`; returns a function that removes it again. */
|
|
@@ -44,3 +55,35 @@ export function createWaiters(): Waiters {
|
|
|
44
55
|
},
|
|
45
56
|
};
|
|
46
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve when `id` settles, or when the timeout or the abort signal wins.
|
|
61
|
+
*
|
|
62
|
+
* One promise, one cleanup: the timer is cleared, the abort listener removed, and
|
|
63
|
+
* the waiter dropped, whichever of the three settles it.
|
|
64
|
+
*/
|
|
65
|
+
export function waitFor(
|
|
66
|
+
waiters: Waiters,
|
|
67
|
+
id: string,
|
|
68
|
+
options: { timeoutMs?: number; signal?: AbortSignal } = {},
|
|
69
|
+
): Promise<WaitOutcome> {
|
|
70
|
+
return new Promise<WaitOutcome>((resolve) => {
|
|
71
|
+
let settled = false;
|
|
72
|
+
let remove = (): void => {};
|
|
73
|
+
const finish = (timedOut: boolean, cancelled: boolean): void => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
if (timer) clearTimeout(timer);
|
|
77
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
78
|
+
remove();
|
|
79
|
+
resolve({ timedOut, cancelled });
|
|
80
|
+
};
|
|
81
|
+
const resolveWaiter = (): void => finish(false, false);
|
|
82
|
+
const onAbort = (): void => finish(false, true);
|
|
83
|
+
const timer = setTimeout(() => finish(true, false), options.timeoutMs ?? DEFAULT_WAIT_MS);
|
|
84
|
+
|
|
85
|
+
remove = waiters.add(id, resolveWaiter);
|
|
86
|
+
if (options.signal?.aborted) onAbort();
|
|
87
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
14
13
|
import { messageOf } from "./errors.ts";
|
|
15
14
|
import { formatHealth, memoryHealth } from "./doctor.ts";
|
|
16
15
|
import { formatNotice, scopeLabel } from "./format.ts";
|
|
17
16
|
import type { MemoryRuntime } from "./runtime.ts";
|
|
18
|
-
import { MemoryPickerComponent } from "./tui.ts";
|
|
19
17
|
import { MEMORY_SCOPES, type MemoryScope } from "./types.ts";
|
|
20
18
|
|
|
21
19
|
/** How often the open picker re-checks the stores for an external edit. */
|
|
@@ -55,6 +53,10 @@ async function openPicker(pi: ExtensionAPI, runtime: MemoryRuntime, ctx: Extensi
|
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
57
55
|
|
|
56
|
+
// The dock screen and its poll loop are the TUI's: the plain listing above and
|
|
57
|
+
// registration load neither.
|
|
58
|
+
const { withPolledScreen } = await import("../../lib/screen-session.ts");
|
|
59
|
+
const { MemoryPickerComponent } = await import("./view/memory-picker.ts");
|
|
58
60
|
await withPolledScreen(
|
|
59
61
|
pi,
|
|
60
62
|
ctx,
|
|
@@ -25,17 +25,17 @@ import {
|
|
|
25
25
|
type TuiMouseEvent,
|
|
26
26
|
type TuiMouseEventResult,
|
|
27
27
|
} from "@earendil-works/pi-tui";
|
|
28
|
-
import { sanitizeLine } from "
|
|
29
|
-
import { ConfirmState } from "
|
|
30
|
-
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "
|
|
31
|
-
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "
|
|
32
|
-
import { clip } from "
|
|
33
|
-
import { emptyState, listRow } from "
|
|
34
|
-
import { titleRule } from "
|
|
35
|
-
import { paint } from "
|
|
36
|
-
import { messageOf } from "
|
|
37
|
-
import { scopeLabel, type MemoryEntries } from "
|
|
38
|
-
import type { MemoryScope } from "
|
|
28
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
29
|
+
import { ConfirmState } from "../../../lib/tui/confirm.ts";
|
|
30
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../../lib/tui/cursor.ts";
|
|
31
|
+
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
32
|
+
import { clip } from "../../../lib/tui/columns.ts";
|
|
33
|
+
import { emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
34
|
+
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
35
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
36
|
+
import { messageOf } from "../errors.ts";
|
|
37
|
+
import { scopeLabel, type MemoryEntries } from "../format.ts";
|
|
38
|
+
import type { MemoryScope } from "../types.ts";
|
|
39
39
|
|
|
40
40
|
/** Note limit enforced by the `memory` tool; the picker mirrors it so a save is never rejected late. */
|
|
41
41
|
const MAX_NOTE_CHARS = 500;
|
|
@@ -521,7 +521,7 @@ export class MemoryPickerComponent implements Component {
|
|
|
521
521
|
"Esc close",
|
|
522
522
|
{ text: "Enter/e edit", color: "accent" },
|
|
523
523
|
{ text: "a add", color: "accent" },
|
|
524
|
-
{ text: "d forget", color: "
|
|
524
|
+
{ text: "d forget", color: "warn" },
|
|
525
525
|
"m merge",
|
|
526
526
|
"p move",
|
|
527
527
|
"s/Tab scope",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Slash command for the plan-mode extension.
|
|
3
3
|
*
|
|
4
4
|
* `/plan` toggles plan mode, or enters it and sends an optional task;
|
|
5
|
-
*
|
|
5
|
+
* Shift+Tab toggles too, through bytefly's editor. Four subcommands are reserved: `/plan review [path]`
|
|
6
6
|
* opens a plan in the browser (and sends saved comments to the model as a
|
|
7
7
|
* refinement), `/plan share [stop]` prints or revokes the link a teammate opens,
|
|
8
8
|
* `/plan list` shows the plans directory, newest first with Enter opening a
|
|
@@ -15,11 +15,11 @@ import { READ_ONLY_SUMMARY } from "../../lib/plan-entry.ts";
|
|
|
15
15
|
import { qrLines } from "../../lib/qr.ts";
|
|
16
16
|
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
17
17
|
import { feedbackSummary } from "./feedback.ts";
|
|
18
|
-
import {
|
|
19
|
-
import { runBrowserReview } from "./review.ts";
|
|
18
|
+
import { runBrowserReview } from "./review-lazy.ts";
|
|
20
19
|
import type { PlanRuntime } from "./runtime.ts";
|
|
21
20
|
import type { PlanSummary } from "./plans.ts";
|
|
22
|
-
import {
|
|
21
|
+
import { createLazyPlanWebReview, NO_SERVER_DEPS } from "./web-lazy.ts";
|
|
22
|
+
import type { PlanWebReview } from "./web.ts";
|
|
23
23
|
|
|
24
24
|
const ENABLED_NOTICE = `Plan mode enabled — ${READ_ONLY_SUMMARY}.`;
|
|
25
25
|
const DISABLED_NOTICE = "Plan mode disabled — full access restored.";
|
|
@@ -28,7 +28,7 @@ const SHARE_PREFIX = "share";
|
|
|
28
28
|
const LIST_PREFIX = "list";
|
|
29
29
|
const CANCEL_PREFIX = "cancel";
|
|
30
30
|
|
|
31
|
-
/** Notice text for the current mode; shared by `/plan` and the
|
|
31
|
+
/** Notice text for the current mode; shared by `/plan` and the toggle request. */
|
|
32
32
|
export function planModeNotice(enabled: boolean): string {
|
|
33
33
|
return enabled ? ENABLED_NOTICE : DISABLED_NOTICE;
|
|
34
34
|
}
|
|
@@ -126,7 +126,8 @@ async function reviewInBrowser(
|
|
|
126
126
|
const file = path ? await runtime.plans.read(ctx.cwd, path) : await runtime.plans.latest(ctx.cwd);
|
|
127
127
|
if (!file) {
|
|
128
128
|
if (path) ctx.ui.notify(`Plan review: no plan file at ${path}`, "error");
|
|
129
|
-
else
|
|
129
|
+
else
|
|
130
|
+
ctx.ui.notify("Plan review: no plan to review; write one with `plan` action `write` or pass a path.", "warning");
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
132
133
|
// The server is loopback-only, so a remote turn cannot drive it.
|
|
@@ -161,7 +162,7 @@ async function reviewInBrowser(
|
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
164
|
const message = runtime.isEnabled()
|
|
164
|
-
? `The user reviewed the plan ${result.plan.relativePath} in the browser and left feedback:\n${summary}\nStay in plan mode, revise the plan (
|
|
165
|
+
? `The user reviewed the plan ${result.plan.relativePath} in the browser and left feedback:\n${summary}\nStay in plan mode, revise the plan (\`plan\` action \`write\`, reusing plan_path), then call \`plan\` action \`exit\` again.`
|
|
165
166
|
: `The user reviewed the plan ${result.plan.relativePath} in the browser and left feedback:\n${summary}\nUpdate the plan file to address the feedback.`;
|
|
166
167
|
if (ctx.isIdle()) await pi.sendUserMessage(message);
|
|
167
168
|
else await pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
@@ -187,6 +188,7 @@ async function openPlanIndex(
|
|
|
187
188
|
const summaries = await summarise();
|
|
188
189
|
|
|
189
190
|
if (ctx.mode !== "tui") {
|
|
191
|
+
const { formatPlanIndex } = await import("./plan-index.ts");
|
|
190
192
|
ctx.ui.notify(formatPlanIndex(summaries, dir), "info");
|
|
191
193
|
return;
|
|
192
194
|
}
|
|
@@ -195,6 +197,7 @@ async function openPlanIndex(
|
|
|
195
197
|
let selected: (PlanSummary | undefined) | undefined;
|
|
196
198
|
// A dock list shares the editor slot with the goal and todo rails, so it takes
|
|
197
199
|
// the dock to itself for as long as it is open, like the other list screens.
|
|
200
|
+
const { PlanIndexComponent } = await import("./plan-index.ts");
|
|
198
201
|
await withRailsSuppressed(pi, () =>
|
|
199
202
|
ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
200
203
|
const component = new PlanIndexComponent({
|
|
@@ -219,7 +222,7 @@ async function openPlanIndex(
|
|
|
219
222
|
export function registerCommands(
|
|
220
223
|
pi: ExtensionAPI,
|
|
221
224
|
runtime: PlanRuntime,
|
|
222
|
-
web: PlanWebReview =
|
|
225
|
+
web: PlanWebReview = createLazyPlanWebReview(runtime.plans, NO_SERVER_DEPS),
|
|
223
226
|
): void {
|
|
224
227
|
pi.registerCommand("plan", {
|
|
225
228
|
description:
|
|
@@ -259,7 +262,7 @@ export function registerCommands(
|
|
|
259
262
|
return;
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
// No task: toggle, exactly like
|
|
265
|
+
// No task: toggle, exactly like Shift+Tab.
|
|
263
266
|
if (!prompt) {
|
|
264
267
|
runtime.toggle(ctx);
|
|
265
268
|
ctx.ui.notify(planModeNotice(runtime.isEnabled()), "info");
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
* the cap diagnosis, the preview, and the merge summary in one place.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { SET_PLAN_STEPS_TOOL } from "./runtime.ts";
|
|
9
8
|
import { MAX_STEP_LENGTH, MAX_STEPS, type PlanStep, type StepUpdate } from "./steps.ts";
|
|
10
9
|
import type { TodoMerge } from "./todos.ts";
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* How every message names the tick-off call. One spelling, because the tool is now
|
|
13
|
+
* one verb among five and a message that still spelled the old name would teach the
|
|
14
|
+
* model a tool that no longer exists.
|
|
15
|
+
*/
|
|
16
|
+
export const TICK_CALL = "`plan` action `steps`";
|
|
17
|
+
|
|
12
18
|
/** Lines of plan shown in an unexpanded result. */
|
|
13
19
|
const PREVIEW_LINES = 8;
|
|
14
20
|
|
|
@@ -80,7 +86,7 @@ export function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
|
|
|
80
86
|
merged.deferred > 0 ? `${merged.deferred} deferred, list full` : "",
|
|
81
87
|
].filter(Boolean);
|
|
82
88
|
const suffix = notes.length > 0 ? ` (${notes.join(", ")})` : "";
|
|
83
|
-
const tick = `Tick each one off in the file with ${
|
|
89
|
+
const tick = `Tick each one off in the file with ${TICK_CALL} as you finish it, passing the step's text as \`expected\`.`;
|
|
84
90
|
const touched =
|
|
85
91
|
merged.stale > 0
|
|
86
92
|
? ` ${merged.stale} older item${merged.stale === 1 ? "" : "s"} no longer match${merged.stale === 1 ? "es" : ""} this plan but ${merged.stale === 1 ? "was" : "were"} left alone: ${merged.stale === 1 ? "it was" : "they were"} started or finished, so check ${merged.stale === 1 ? "it" : "them"} yourself.`
|