@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
package/extensions/plan/web.ts
CHANGED
|
@@ -15,11 +15,25 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
|
|
|
15
15
|
import { basename } from "node:path";
|
|
16
16
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
17
17
|
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
18
|
-
import { codeBody, fenceInfo, planBlocks
|
|
19
|
-
import {
|
|
18
|
+
import { codeBody, fenceInfo, planBlocks } from "./blocks.ts";
|
|
19
|
+
import {
|
|
20
|
+
MAX_ANNOTATIONS,
|
|
21
|
+
MAX_COMMENT_LENGTH,
|
|
22
|
+
MAX_QUOTE_LENGTH,
|
|
23
|
+
parseFeedback,
|
|
24
|
+
stripFeedback,
|
|
25
|
+
type PlanAnnotation,
|
|
26
|
+
} from "./feedback.ts";
|
|
20
27
|
import { planTitle, type PlanStore, type StoredPlan } from "./plans.ts";
|
|
21
|
-
import { changedSections,
|
|
22
|
-
import {
|
|
28
|
+
import { changedSections, type SectionDigest } from "./plan-text.ts";
|
|
29
|
+
import { extractPlanSections, extractPlanSteps } from "./steps.ts";
|
|
30
|
+
import {
|
|
31
|
+
handleReviewRequest,
|
|
32
|
+
JSON_HEADERS,
|
|
33
|
+
type ReviewContext,
|
|
34
|
+
type ServeResponse,
|
|
35
|
+
type SaveResult,
|
|
36
|
+
} from "./web-router.ts";
|
|
23
37
|
import type { ReviewPageData } from "./web-page.ts";
|
|
24
38
|
|
|
25
39
|
/** How a browser review ended. */
|
|
@@ -46,8 +60,8 @@ export interface PlanReviewOptions {
|
|
|
46
60
|
signal?: AbortSignal;
|
|
47
61
|
/** Whether the browser may approve the plan (only the `exit_plan_mode` flow). */
|
|
48
62
|
canApprove?: boolean;
|
|
49
|
-
/** The
|
|
50
|
-
|
|
63
|
+
/** The section digests the previous review showed, so this page can mark changed sections. */
|
|
64
|
+
baseline?: SectionDigest[];
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
/** Starts and tracks review sessions. */
|
|
@@ -64,6 +78,10 @@ export interface PlanWebReviewDeps {
|
|
|
64
78
|
token?: () => string;
|
|
65
79
|
/** Override the bind port; the default `0` lets the OS pick. */
|
|
66
80
|
port?: number;
|
|
81
|
+
/** Override the closed-tab grace; the default leaves a reload time to reconnect. */
|
|
82
|
+
dropGraceMs?: number;
|
|
83
|
+
/** Override the accepted body size; a test keeps it small. */
|
|
84
|
+
maxBodyBytes?: number;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
/** Largest feedback body accepted, in bytes. */
|
|
@@ -71,21 +89,37 @@ const MAX_BODY_BYTES = 1024 * 1024;
|
|
|
71
89
|
/** Grace period before an empty client set counts as a closed tab. */
|
|
72
90
|
const DROP_GRACE_MS = 1500;
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
/** A request body, or the fact that it was too large to read. */
|
|
93
|
+
type BodyResult = { ok: true; body: string } | { ok: false };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read a request body, stopping at {@link MAX_BODY_BYTES}. An oversize body is
|
|
97
|
+
* reported rather than destroyed here, so the handler can still answer 413 over
|
|
98
|
+
* the open socket instead of the client seeing a dropped connection.
|
|
99
|
+
*/
|
|
100
|
+
function readBody(request: IncomingMessage, limit: number): Promise<BodyResult> {
|
|
75
101
|
return new Promise((resolve) => {
|
|
76
102
|
const chunks: Buffer[] = [];
|
|
77
103
|
let size = 0;
|
|
104
|
+
let done = false;
|
|
105
|
+
const settle = (result: BodyResult): void => {
|
|
106
|
+
if (done) return;
|
|
107
|
+
done = true;
|
|
108
|
+
resolve(result);
|
|
109
|
+
};
|
|
78
110
|
request.on("data", (chunk: Buffer) => {
|
|
111
|
+
if (done) return;
|
|
79
112
|
size += chunk.length;
|
|
80
|
-
if (size >
|
|
81
|
-
|
|
82
|
-
request.
|
|
113
|
+
if (size > limit) {
|
|
114
|
+
chunks.length = 0;
|
|
115
|
+
request.pause();
|
|
116
|
+
settle({ ok: false });
|
|
83
117
|
return;
|
|
84
118
|
}
|
|
85
119
|
chunks.push(chunk);
|
|
86
120
|
});
|
|
87
|
-
request.on("end", () =>
|
|
88
|
-
request.on("error", () =>
|
|
121
|
+
request.on("end", () => settle({ ok: true, body: Buffer.concat(chunks).toString("utf-8") }));
|
|
122
|
+
request.on("error", () => settle({ ok: false }));
|
|
89
123
|
});
|
|
90
124
|
}
|
|
91
125
|
|
|
@@ -115,6 +149,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
115
149
|
): Promise<PlanReviewSession | undefined> => {
|
|
116
150
|
const { signal } = options;
|
|
117
151
|
const canApprove = options.canApprove === true;
|
|
152
|
+
const maxBody = deps.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
118
153
|
const token = (deps.token ?? (() => randomBytes(16).toString("hex")))();
|
|
119
154
|
let settled = false;
|
|
120
155
|
let resolveDone!: (result: PlanReviewResult) => void;
|
|
@@ -132,24 +167,47 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
132
167
|
|
|
133
168
|
// The stripped plan the page last rendered; a save must anchor against a file
|
|
134
169
|
// that still matches it, or the comments land beside the wrong text.
|
|
135
|
-
let
|
|
136
|
-
// Section titles that differ from what the previous review showed. Feedback
|
|
137
|
-
// is stripped on both sides, so a comment never reads as an edit.
|
|
138
|
-
const changed =
|
|
139
|
-
options.previous === undefined ? new Set<string>() : changedSections(stripFeedback(options.previous), baseline);
|
|
170
|
+
let pageText = stripFeedback(plan.content);
|
|
140
171
|
let savedPlan: StoredPlan | undefined;
|
|
141
172
|
// Set by `onSaved`/`onCancel`; resolved once the HTTP response has flushed,
|
|
142
173
|
// so the caller cannot close the socket before the browser sees the reply.
|
|
143
174
|
let pendingResult: PlanReviewResult | undefined;
|
|
144
175
|
|
|
145
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Re-read the plan into `pageText` and return it. Named apart from the page
|
|
178
|
+
* build because it writes the state a later save validates against, so a `GET`
|
|
179
|
+
* does not hide that write inside what looks like a read.
|
|
180
|
+
*/
|
|
181
|
+
const refreshPageText = async (): Promise<StoredPlan> => {
|
|
146
182
|
const current = (await store.read(cwd, plan.path)) ?? plan;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
pageText = stripFeedback(current.content);
|
|
184
|
+
return current;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const page = async (): Promise<ReviewPageData> => {
|
|
188
|
+
const stored = await refreshPageText();
|
|
189
|
+
const current = pageText;
|
|
190
|
+
const blocks = planBlocks(current);
|
|
191
|
+
const sections = extractPlanSections(current);
|
|
192
|
+
// Per-occurrence change flags, aligned with the sections list: the same shape
|
|
193
|
+
// the terminal review renders, so duplicate headings cannot both light up.
|
|
194
|
+
const changed = changedSections(options.baseline, current);
|
|
195
|
+
const outline = sections.flatMap((section, index) =>
|
|
196
|
+
section.title === ""
|
|
197
|
+
? []
|
|
198
|
+
: [
|
|
199
|
+
{
|
|
200
|
+
id: `s-${section.line}`,
|
|
201
|
+
line: section.line,
|
|
202
|
+
level: section.level,
|
|
203
|
+
text: section.title,
|
|
204
|
+
...(changed[index] ? { changed: true } : {}),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
);
|
|
150
208
|
// The same step list approval seeds and set_plan_steps indexes, so the page's
|
|
151
209
|
// progress cannot disagree with the todo list.
|
|
152
|
-
const stepList = extractPlanSteps(
|
|
210
|
+
const stepList = extractPlanSteps(current);
|
|
153
211
|
// Highlight code blocks once, at the boundary: the client copies only the
|
|
154
212
|
// tok-* spans out of the server-escaped HTML.
|
|
155
213
|
const code: Record<string, string[]> = {};
|
|
@@ -161,21 +219,16 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
161
219
|
return {
|
|
162
220
|
// The document's own H1 reads better than the file-name slug, and the
|
|
163
221
|
// slug is still the fallback for a plan that starts with something else.
|
|
164
|
-
title:
|
|
222
|
+
title: sections.find((section) => section.level === 1)?.title || planTitle(basename(plan.path)),
|
|
165
223
|
blocks,
|
|
166
|
-
annotations: parseFeedback(
|
|
167
|
-
outline
|
|
168
|
-
id: `s-${heading.line}`,
|
|
169
|
-
line: heading.line,
|
|
170
|
-
level: heading.level,
|
|
171
|
-
text: heading.text,
|
|
172
|
-
...(changed.has(heading.text) ? { changed: true } : {}),
|
|
173
|
-
})),
|
|
224
|
+
annotations: parseFeedback(stored.content),
|
|
225
|
+
outline,
|
|
174
226
|
steps: {
|
|
175
227
|
done: stepList.filter((step) => step.status === "completed").length,
|
|
176
228
|
total: stepList.length,
|
|
177
229
|
},
|
|
178
230
|
code,
|
|
231
|
+
limits: { comments: MAX_ANNOTATIONS, comment: MAX_COMMENT_LENGTH, quote: MAX_QUOTE_LENGTH },
|
|
179
232
|
token,
|
|
180
233
|
canApprove,
|
|
181
234
|
};
|
|
@@ -184,7 +237,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
184
237
|
const save = async (annotations: PlanAnnotation[]): Promise<SaveResult> => {
|
|
185
238
|
const current = await store.read(cwd, plan.path);
|
|
186
239
|
if (!current) return { ok: false, status: 404, message: "The plan file is gone." };
|
|
187
|
-
if (stripFeedback(current.content) !==
|
|
240
|
+
if (stripFeedback(current.content) !== pageText) {
|
|
188
241
|
return { ok: false, status: 409, message: "The plan changed on disk; reload the page and review again." };
|
|
189
242
|
}
|
|
190
243
|
try {
|
|
@@ -215,31 +268,34 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
215
268
|
// button and the abort signal are the deterministic paths.
|
|
216
269
|
const clients = new Set<ServerResponse>();
|
|
217
270
|
let hadClient = false;
|
|
271
|
+
const dropGrace = deps.dropGraceMs ?? DROP_GRACE_MS;
|
|
218
272
|
const scheduleDrop = (): void => {
|
|
219
273
|
if (clients.size > 0 || !hadClient || settled || dropTimer) return;
|
|
220
274
|
dropTimer = setTimeout(() => {
|
|
221
275
|
dropTimer = undefined;
|
|
222
276
|
if (clients.size === 0) finish({ status: "cancelled" });
|
|
223
|
-
},
|
|
277
|
+
}, dropGrace);
|
|
224
278
|
(dropTimer as { unref?: () => void }).unref?.();
|
|
225
279
|
};
|
|
226
280
|
|
|
227
281
|
const server = createServer((request, response) => {
|
|
228
282
|
void (async () => {
|
|
229
283
|
const method = request.method ?? "GET";
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
284
|
+
const read =
|
|
285
|
+
method.toUpperCase() === "POST" ? await readBody(request, maxBody) : { ok: true as const, body: "" };
|
|
286
|
+
if (!read.ok) {
|
|
287
|
+
response.writeHead(413, { ...JSON_HEADERS, Connection: "close" });
|
|
233
288
|
response.end(JSON.stringify({ error: "The request body is too large." }));
|
|
234
289
|
return;
|
|
235
290
|
}
|
|
291
|
+
const body = read.body;
|
|
236
292
|
let result: ServeResponse;
|
|
237
293
|
try {
|
|
238
294
|
result = await handleReviewRequest(context, method, request.url ?? "/", request.headers, body);
|
|
239
295
|
} catch {
|
|
240
296
|
result = {
|
|
241
297
|
status: 500,
|
|
242
|
-
headers: {
|
|
298
|
+
headers: { ...JSON_HEADERS },
|
|
243
299
|
body: JSON.stringify({ error: "Internal error" }),
|
|
244
300
|
};
|
|
245
301
|
}
|
|
@@ -264,16 +320,25 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
264
320
|
return;
|
|
265
321
|
}
|
|
266
322
|
// A save or cancel only resolves the session after its response is on the
|
|
267
|
-
// wire; otherwise `close()` would force-close the socket mid-reply.
|
|
268
|
-
|
|
269
|
-
|
|
323
|
+
// wire; otherwise `close()` would force-close the socket mid-reply. The
|
|
324
|
+
// `close` path covers a client that walks away before the flush callback
|
|
325
|
+
// runs, so a settle cannot be lost with the socket.
|
|
326
|
+
const settlePending = (): void => {
|
|
270
327
|
const terminal = pendingResult;
|
|
328
|
+
if (!terminal) return;
|
|
271
329
|
pendingResult = undefined;
|
|
272
330
|
finish(terminal);
|
|
273
|
-
}
|
|
331
|
+
};
|
|
332
|
+
response.end(result.body ?? "", settlePending);
|
|
333
|
+
response.on("close", settlePending);
|
|
274
334
|
})();
|
|
275
335
|
});
|
|
276
336
|
|
|
337
|
+
// An accept-level error (EMFILE and friends) is emitted on the server, and an
|
|
338
|
+
// `error` event with no listener would take the host down mid-review. The
|
|
339
|
+
// review still ends through its own paths: cancel, abort, drop, or shutdown.
|
|
340
|
+
server.on("error", () => {});
|
|
341
|
+
|
|
277
342
|
const preferred = deps.port ?? ports.get(plan.path) ?? 0;
|
|
278
343
|
let port: number | undefined;
|
|
279
344
|
try {
|
|
@@ -131,7 +131,7 @@ None in v1 — the tool is either available (a UI exists) or not.
|
|
|
131
131
|
|---|---|
|
|
132
132
|
| Tool | `ask_user_question`; `exposure: "model-only"`, `defaultActive: false`, activated in `session_start` when `ctx.hasUI` is true. |
|
|
133
133
|
| Execution | `executionMode: "sequential"`. |
|
|
134
|
-
| Annotations | `readOnlyHint: true`, `openWorldHint: false
|
|
134
|
+
| Annotations | `readOnlyHint: true`, `openWorldHint: false` (a read-only tool declares no `destructiveHint`). |
|
|
135
135
|
| Output schema | None. Structured data is returned as tool-result `details` (not `structuredContent`). |
|
|
136
136
|
| State storage | Tool-result `details` (`questions`, `answers`, `cancelled`, `unavailable`). No `pi.appendEntry`. |
|
|
137
137
|
| Lifecycle | `session_start` only: add the tool to the active set when a UI exists. No `session_shutdown`. |
|
|
@@ -45,7 +45,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
45
45
|
parameters: AskUserQuestionParams,
|
|
46
46
|
exposure: "model-only",
|
|
47
47
|
defaultActive: false,
|
|
48
|
-
annotations: { readOnlyHint: true, openWorldHint: false
|
|
48
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
49
49
|
executionMode: "sequential",
|
|
50
50
|
|
|
51
51
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -28,6 +28,12 @@ pi install ./ # install the package
|
|
|
28
28
|
**Conversation only**, or **Code only**.
|
|
29
29
|
- *Code* rewrites the working tree to the snapshot and is itself undoable
|
|
30
30
|
(a `pre-restore` safety snapshot is taken first).
|
|
31
|
+
- When code moves, a second step asks **Restore what?** — **Everything (N
|
|
32
|
+
files)** or **Choose paths…**. The checklist lists every changed path
|
|
33
|
+
grouped by directory (`M` modified, `D` deleted since the snapshot, `A`
|
|
34
|
+
created after it), with all of them selected, so `Enter` twice reproduces a
|
|
35
|
+
whole-tree restore. `Space` toggles a file or a directory, `a`/`n` select
|
|
36
|
+
all/none, and the confirm that follows previews only the chosen subset.
|
|
31
37
|
- *Conversation* moves the session tree back to that prompt and puts the
|
|
32
38
|
prompt text back in the editor; the abandoned branch stays in the session.
|
|
33
39
|
- Shows a `↺ N` status chip counting the prompts on the active branch — the
|
|
@@ -45,18 +51,23 @@ the main checkout; `list`/matching filter to the current root by default.
|
|
|
45
51
|
|
|
46
52
|
| Command | What it does |
|
|
47
53
|
|---|---|
|
|
48
|
-
| `/rewind` | Open the timeline; pick a prompt, then a restore scope. |
|
|
54
|
+
| `/rewind` | Open the timeline; pick a prompt, then a restore scope, then (for code) the paths. |
|
|
55
|
+
|
|
56
|
+
The path checklist is its own `ctx.ui.custom` screen: `Space` toggles the focused
|
|
57
|
+
row, `a`/`n` select all or none, `Enter` returns the selection, and `Esc` cancels
|
|
58
|
+
the rewind.
|
|
49
59
|
|
|
50
60
|
The timeline is keyboard-driven: `↑`/`↓` or `j`/`k` move, `PgUp`/`PgDn`/`Home`/`End`
|
|
51
61
|
jump, `Enter` chooses the focused prompt, the mouse wheel scrolls, `Esc` closes.
|
|
52
62
|
The scope choice and the confirmation (with a diff preview) run after the screen
|
|
53
|
-
closes.
|
|
63
|
+
closes. The path checklist is the same grammar: `Space` toggles, `a`/`n` select
|
|
64
|
+
all or none, `Enter` restores the selection, `Esc` cancels the rewind.
|
|
54
65
|
|
|
55
66
|
## Behaviour by mode
|
|
56
67
|
|
|
57
68
|
| Mode | Behaviour |
|
|
58
69
|
|---|---|
|
|
59
|
-
| Interactive TUI | Full selectable timeline component
|
|
70
|
+
| Interactive TUI | Full selectable timeline component, then the scope choice, the path checklist, and the confirmation. |
|
|
60
71
|
| RPC (`hasUI`) | Timeline falls back to `ui.select`; scope choice and confirmation use forwarded dialogs. |
|
|
61
72
|
| `print` / `json` | No picker: the prompt list is printed. |
|
|
62
73
|
|
|
@@ -110,9 +121,14 @@ A clean working tree still records a commit (so the metadata always travels with
|
|
|
110
121
|
the snapshot); git reuses the identical tree object, so the only cost is a small
|
|
111
122
|
commit. These refs are not branches and are not touched by `git push --all`.
|
|
112
123
|
|
|
124
|
+
## Limitations
|
|
125
|
+
|
|
126
|
+
- **The path checklist is TUI-only.** RPC, `print`, and `json` modes restore the
|
|
127
|
+
whole tree; there is no typed path syntax, because a list of exact paths is what
|
|
128
|
+
the checklist is for and a glob would need its own validation and preview.
|
|
129
|
+
|
|
113
130
|
## Non-goals
|
|
114
131
|
|
|
115
|
-
- No per-file or partial code rewind; a restore is all-or-nothing.
|
|
116
132
|
- No capture of `.gitignore`d files.
|
|
117
133
|
- No moving HEAD, committing, stashing, or pushing.
|
|
118
134
|
- No non-git projects for code snapshots (conversation-only rewind still works
|
|
@@ -126,7 +142,7 @@ commit. These refs are not branches and are not touched by `git push --all`.
|
|
|
126
142
|
|---|---|
|
|
127
143
|
| Model-facing tools | None. `/rewind` is user-driven; automatic snapshots cover the start of every task. |
|
|
128
144
|
| Commands | `/rewind` (`pi.registerCommand`). |
|
|
129
|
-
| Events | `session_start` (reset run state, status, stale-index sweep), `session_tree` (status refresh after tree navigation), `before_agent_start` (
|
|
145
|
+
| Events | `session_start` (reset run state, status, stale-index sweep), `session_tree` (status refresh after tree navigation), `before_agent_start` (record the prompt — absent on a run a custom message starts — and resolve the run's snapshot decision), `tool_call` (take the run's snapshot before the first mutating call), `session_shutdown` (release indexes, clear status). |
|
|
130
146
|
| State storage | Git refs `refs/pi/rewind/<id>` with metadata in the commit body. No tool-result `details` and no `pi.appendEntry`. |
|
|
131
147
|
| Lifecycle | Index paths are created lazily and released in `session_shutdown`; status chip is set on start/tree/snapshot and cleared on shutdown. |
|
|
132
148
|
|
|
@@ -136,7 +152,9 @@ commit. These refs are not branches and are not touched by `git push --all`.
|
|
|
136
152
|
`before_agent_start` records the prompt summary and resolves the run's root,
|
|
137
153
|
policy, and config, so the first mutating `tool_call` takes the snapshot with a
|
|
138
154
|
fully synchronous guard. No `await` sits between that guard's check and its
|
|
139
|
-
claim, so parallel sibling calls cannot double-snapshot.
|
|
155
|
+
claim, so parallel sibling calls cannot double-snapshot. A run a custom message
|
|
156
|
+
starts (a wake) has no prompt to record; the snapshot it takes is otherwise the
|
|
157
|
+
same.
|
|
140
158
|
- **The timeline comes from the session tree, not from git.** `/rewind` lists
|
|
141
159
|
every user message on the active branch and pairs each with the snapshot whose
|
|
142
160
|
recorded `entryId` matches, so read-only prompts appear too and the list
|
|
@@ -190,7 +208,9 @@ commit. These refs are not branches and are not touched by `git push --all`.
|
|
|
190
208
|
| `git.ts` | Single git entry point: rewind plumbing, the shared read helpers rewind consumes, and the production `RunGit`. |
|
|
191
209
|
| `snapshot.ts` | Create a snapshot (tree from a temporary index → commit → ref). |
|
|
192
210
|
| `snapshot-run.ts` | Per-run automatic-snapshot state: one snapshot per prompt, with a synchronous guard. |
|
|
193
|
-
| `restore.ts` | Plan and apply a working-tree rewind. |
|
|
211
|
+
| `restore.ts` | Plan and apply a working-tree rewind, whole or for a chosen subset of paths. |
|
|
212
|
+
| `paths.ts` | Pure: the changed-path list from `git diff --name-status`, its directory grouping, the change marks, and the restore counts. |
|
|
213
|
+
| `path-picker.ts` | The checklist screen a partial restore opens: grouped paths, toggles, and the selection it returns. |
|
|
194
214
|
| `store.ts` | Versioned metadata encoding and ref-backed listing/pruning. |
|
|
195
215
|
| `policy.ts` | Which tool calls are worth a snapshot. |
|
|
196
216
|
| `temp-index.ts` | Per-process `GIT_INDEX_FILE` paths: create, release, and sweep stale files. |
|
|
@@ -198,7 +218,7 @@ commit. These refs are not branches and are not touched by `git push --all`.
|
|
|
198
218
|
| `queue.ts` | The serialized git operation queue for staging calls (`GIT_INDEX_FILE`). |
|
|
199
219
|
| `runtime.ts` | Root/config caches and the snapshot/list/plan/restore operations. |
|
|
200
220
|
| `timeline.ts` | Pure: prompts on the branch, paired with their snapshots. |
|
|
201
|
-
| `rewind.ts` | Scope choice, confirmation, and applying code + conversation rewind. |
|
|
221
|
+
| `rewind.ts` | Scope choice, the path step, confirmation, and applying code + conversation rewind. |
|
|
202
222
|
| `rewind-tui.ts` | The selectable, width-safe timeline component, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
203
223
|
| `commands.ts` | The `/rewind` command. |
|
|
204
224
|
| `format.ts` | Model-facing and transcript text (pure). |
|
|
@@ -61,7 +61,7 @@ async function openRewind(pi: ExtensionAPI, runtime: RewindRuntime, ctx: Extensi
|
|
|
61
61
|
|
|
62
62
|
const point = await withRailsSuppressed(pi, () => choosePoint(ctx, points));
|
|
63
63
|
if (!point) return;
|
|
64
|
-
await rewindTo(runtime, ctx, root, point);
|
|
64
|
+
await rewindTo(pi, runtime, ctx, root, point);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function registerCommands(pi: ExtensionAPI, runtime: RewindRuntime): void {
|
|
@@ -131,11 +131,11 @@ export function previewDiffText(diff: string, maxLines = DIFF_PREVIEW_LINES): st
|
|
|
131
131
|
return [...lines.slice(0, maxLines), `… ${lines.length - maxLines} more lines`].join("\n");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** Restore confirmation text. */
|
|
134
|
+
/** Restore confirmation text; a partial restore also names how much it covered. */
|
|
135
135
|
export function formatRestoreText(summary: RestoreSummary): string {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
];
|
|
136
|
+
const scope = summary.of === undefined ? "" : ` of ${summary.of}`;
|
|
137
|
+
const files = `${summary.changed}${scope} file${summary.changed === 1 && summary.of === undefined ? "" : "s"}`;
|
|
138
|
+
const parts = [`Restored snapshot #${summary.id}: ${files} changed`];
|
|
139
139
|
if (summary.removed > 0) parts.push(`${summary.removed} removed`);
|
|
140
140
|
const line = `${parts.join(", ")}.`;
|
|
141
141
|
const safety = summary.safety ? ` A safety snapshot #${summary.safety} captures the pre-restore state.` : "";
|
package/extensions/rewind/git.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
runGitOrThrow as run,
|
|
23
23
|
type RunGit,
|
|
24
24
|
} from "../../lib/git/index.ts";
|
|
25
|
+
import { parseNameStatus, type ChangedPath } from "./paths.ts";
|
|
25
26
|
|
|
26
27
|
// Re-export exactly the shared helpers this extension consumes, so rewind code
|
|
27
28
|
// has one git import surface instead of reaching into `lib/git` directly.
|
|
@@ -128,33 +129,70 @@ export async function readTreeReset(runGit: RunGit, cwd: string, commit: string,
|
|
|
128
129
|
await run(runGit, ["read-tree", "--reset", "-u", commit], { cwd, env: { GIT_INDEX_FILE: indexFile } });
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
/**
|
|
133
|
+
* A pathspec that matches `path` literally.
|
|
134
|
+
*
|
|
135
|
+
* The paths come from `git diff` output, so a file named `weird[1].txt` is a real
|
|
136
|
+
* possibility; without `:(literal)` git would read it as a glob and restore the
|
|
137
|
+
* wrong thing (or nothing).
|
|
138
|
+
*/
|
|
139
|
+
function literalPathspec(path: string): string {
|
|
140
|
+
return `:(literal)${path}`;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The changed paths between two trees, with how each changed.
|
|
145
|
+
*
|
|
146
|
+
* One call replaces the separate added-path and changed-count queries, so the
|
|
147
|
+
* preview, the checklist, and the apply step all read the same list.
|
|
148
|
+
*/
|
|
149
|
+
export async function nameStatus(
|
|
150
|
+
runGit: RunGit,
|
|
151
|
+
cwd: string,
|
|
152
|
+
from: string,
|
|
153
|
+
to: string,
|
|
154
|
+
paths?: readonly string[],
|
|
155
|
+
): Promise<ChangedPath[]> {
|
|
156
|
+
const args = ["diff", "--no-color", "--name-status", from, to, "--"];
|
|
157
|
+
if (paths) args.push(...paths.map(literalPathspec));
|
|
158
|
+
const result = await runGit(args, { cwd });
|
|
159
|
+
return result.code === 0 ? parseNameStatus(result.stdout) : [];
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
/**
|
|
145
|
-
export async function
|
|
162
|
+
/** `git diff --stat` between two trees, optionally limited to `paths`. */
|
|
163
|
+
export async function diffStat(
|
|
146
164
|
runGit: RunGit,
|
|
147
165
|
cwd: string,
|
|
148
166
|
from: string,
|
|
149
167
|
to: string,
|
|
150
|
-
|
|
151
|
-
): Promise<
|
|
152
|
-
const args = ["diff", "--no-color", "--
|
|
153
|
-
if (
|
|
154
|
-
args.push(from, to, "--");
|
|
168
|
+
paths?: readonly string[],
|
|
169
|
+
): Promise<string> {
|
|
170
|
+
const args = ["diff", "--no-color", "--stat", from, to, "--"];
|
|
171
|
+
if (paths) args.push(...paths.map(literalPathspec));
|
|
155
172
|
const result = await runGit(args, { cwd });
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
return result.code === 0 ? result.stdout.trim() : "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Rewrite the given paths in the working tree from `commit`.
|
|
178
|
+
*
|
|
179
|
+
* `read-tree --reset -u` takes no pathspec, so a partial restore checks the paths
|
|
180
|
+
* out of the snapshot instead. The index it writes is the temporary one, so the
|
|
181
|
+
* real index stays untouched exactly as the whole-tree reset leaves it. Callers
|
|
182
|
+
* pass only paths present in `commit`; an added path is deleted, not checked out.
|
|
183
|
+
*/
|
|
184
|
+
export async function restorePaths(
|
|
185
|
+
runGit: RunGit,
|
|
186
|
+
cwd: string,
|
|
187
|
+
commit: string,
|
|
188
|
+
indexFile: string,
|
|
189
|
+
paths: readonly string[],
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
if (paths.length === 0) return;
|
|
192
|
+
await run(runGit, ["checkout", commit, "--", ...paths.map(literalPathspec)], {
|
|
193
|
+
cwd,
|
|
194
|
+
env: { GIT_INDEX_FILE: indexFile },
|
|
195
|
+
});
|
|
158
196
|
}
|
|
159
197
|
|
|
160
198
|
/** Label of an in-progress git operation that a rewind must not clobber, if any. */
|
|
@@ -36,12 +36,10 @@ export function registerRewind(pi: ExtensionAPI): void {
|
|
|
36
36
|
await runtime.setStatus(ctx);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
// A
|
|
40
|
-
//
|
|
41
|
-
// custom message (a wake) has no user prompt — `event.prompt` is undefined
|
|
42
|
-
// there despite its type — so `beginRun` accepts a missing prompt.
|
|
39
|
+
// A run started by a custom message (a wake) has no user prompt: only a user
|
|
40
|
+
// run carries one, and a newer host spells that as a union on `messageType`.
|
|
43
41
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
44
|
-
await run.beginRun(event.prompt, ctx);
|
|
42
|
+
await run.beginRun("prompt" in event ? event.prompt : undefined, ctx);
|
|
45
43
|
});
|
|
46
44
|
|
|
47
45
|
pi.on("session_shutdown", (_event, ctx) => {
|