@gavin-hu/my-pi-agent 0.15.0 → 0.16.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 +12 -11
- package/README.zh-CN.md +9 -7
- package/extensions/bytefly/README.md +98 -116
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/format.ts +3 -8
- package/extensions/bytefly/panel/join.ts +1 -6
- package/extensions/bytefly/panel/settings.ts +74 -377
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
- package/extensions/bytefly/panel/tui/frame.ts +2 -2
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui.ts +4 -6
- package/extensions/bytefly/picker/resources.ts +7 -3
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/context/README.md +162 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +82 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +319 -0
- package/extensions/context/tui.ts +268 -0
- package/extensions/context/types.ts +122 -0
- package/extensions/file-browse/README.md +83 -8
- package/extensions/file-browse/client.ts +3 -1
- package/extensions/file-browse/config.ts +23 -0
- package/extensions/file-browse/files.ts +183 -48
- package/extensions/file-browse/fuzzy.ts +65 -11
- package/extensions/file-browse/html.ts +78 -133
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +158 -0
- package/extensions/file-browse/live.ts +256 -0
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +33 -8
- package/extensions/file-browse/server.ts +78 -13
- package/extensions/file-browse/styles.ts +124 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +72 -56
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +3 -3
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +10 -14
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +7 -26
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +6 -14
- package/extensions/plan/tools/enter.ts +6 -7
- package/extensions/plan/tools/exit.ts +6 -7
- package/extensions/plan/tools/steps.ts +6 -7
- package/extensions/plan/tools/write.ts +8 -9
- package/extensions/plan/web-router.ts +2 -12
- package/extensions/plan/web.ts +30 -15
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +10 -3
- package/extensions/rewind/git.ts +27 -14
- package/extensions/rewind/hunk-picker.ts +1 -1
- package/extensions/rewind/path-picker.ts +1 -1
- package/extensions/rewind/review-tui.ts +3 -3
- package/extensions/rewind/snapshot.ts +13 -4
- package/extensions/subagent/background-tui.ts +1 -1
- package/extensions/subagent/background.ts +1 -7
- package/extensions/todo/tools.ts +10 -14
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +7 -18
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +4 -7
- package/extensions/worktree/status.ts +14 -9
- package/lib/README.md +12 -8
- package/lib/escape.ts +15 -2
- package/lib/git/read.ts +20 -0
- package/lib/http/headers.ts +20 -0
- package/lib/http/server.ts +22 -4
- package/lib/pdf.ts +2 -2
- package/lib/pool.ts +62 -0
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tui/README.md +3 -3
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +12 -4
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/package.json +8 -4
- package/skills/changelog/SKILL.md +136 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
package/extensions/job/tools.ts
CHANGED
|
@@ -9,17 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
12
|
import { errorResult } from "../../lib/tool-result.ts";
|
|
14
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
-
import {
|
|
16
|
-
elapsedSuffix,
|
|
17
|
-
errorLine,
|
|
18
|
-
rejectionLine,
|
|
19
|
-
reuseCallText,
|
|
20
|
-
syncElapsedTimer,
|
|
21
|
-
toolCallTitle,
|
|
22
|
-
} from "../../lib/tui/tool.ts";
|
|
14
|
+
import { callLine, detailsOrLine, elapsedSuffix, rejectionLine, syncElapsedTimer } from "../../lib/tui/tool.ts";
|
|
23
15
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
24
16
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
25
17
|
import {
|
|
@@ -229,30 +221,19 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
229
221
|
|
|
230
222
|
renderCall(args, theme, context) {
|
|
231
223
|
const action = typeof args.action === "string" ? args.action : "job";
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
235
|
-
paint(
|
|
236
|
-
theme,
|
|
237
|
-
"meta",
|
|
238
|
-
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
239
|
-
),
|
|
240
|
-
);
|
|
241
|
-
return text;
|
|
224
|
+
const subject = formatCallText(action, args as Record<string, unknown>, context.argsComplete);
|
|
225
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, subject, "middle");
|
|
242
226
|
},
|
|
243
227
|
|
|
244
228
|
renderResult(result, options, theme, context) {
|
|
245
229
|
const text = reuseText(context.lastComponent);
|
|
246
230
|
const startedAt = syncElapsedTimer(options.isPartial, context);
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
text.setText(
|
|
250
|
-
return text;
|
|
251
|
-
}
|
|
252
|
-
if (details.error) {
|
|
253
|
-
text.setText(errorLine(theme, details.error));
|
|
231
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<JobDetails>(result.details));
|
|
232
|
+
if ("line" in outcome) {
|
|
233
|
+
text.setText(outcome.line);
|
|
254
234
|
return text;
|
|
255
235
|
}
|
|
236
|
+
const details = outcome.details;
|
|
256
237
|
|
|
257
238
|
if (options.isPartial) {
|
|
258
239
|
const id = details.job?.id;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
14
14
|
import { messageOf } from "./errors.ts";
|
|
15
15
|
import { formatHealth, memoryHealth } from "./doctor.ts";
|
|
16
16
|
import { formatNotice, scopeLabel } from "./format.ts";
|
|
@@ -55,49 +55,35 @@ async function openPicker(pi: ExtensionAPI, runtime: MemoryRuntime, ctx: Extensi
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Pick up an external edit while the screen is open, repainting only on a change.
|
|
90
|
-
timer = setInterval(() => {
|
|
91
|
-
if (runtime.refresh().changed.length > 0) tui.requestRender();
|
|
92
|
-
}, PICKER_POLL_MS);
|
|
93
|
-
timer.unref?.();
|
|
94
|
-
|
|
95
|
-
return component;
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
} finally {
|
|
99
|
-
stopTimer();
|
|
100
|
-
}
|
|
58
|
+
await withPolledScreen(
|
|
59
|
+
pi,
|
|
60
|
+
ctx,
|
|
61
|
+
(screen) =>
|
|
62
|
+
new MemoryPickerComponent(
|
|
63
|
+
screen.tui,
|
|
64
|
+
screen.theme,
|
|
65
|
+
{
|
|
66
|
+
entries: () => runtime.entries(),
|
|
67
|
+
trusted: () => runtime.trusted(),
|
|
68
|
+
refresh: () => runtime.refresh().changed.length > 0,
|
|
69
|
+
add: (scope, text) => runtime.add(scope, text),
|
|
70
|
+
update: (scope, from, to) => runtime.update(scope, from, to),
|
|
71
|
+
forget: (scope, entry) => runtime.forget(scope, entry),
|
|
72
|
+
merge: (scope, first, second, merged) => runtime.merge(scope, first, second, merged),
|
|
73
|
+
move: (from, to, text) => runtime.move(from, to, text),
|
|
74
|
+
},
|
|
75
|
+
screen.close,
|
|
76
|
+
screen.requestRender,
|
|
77
|
+
screen.viewportRows,
|
|
78
|
+
),
|
|
79
|
+
{
|
|
80
|
+
pollMs: PICKER_POLL_MS,
|
|
81
|
+
// Pick up an external edit while the screen is open, repainting only on a change.
|
|
82
|
+
poll: (_component, handles) => {
|
|
83
|
+
if (runtime.refresh().changed.length > 0) handles.requestRender();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
101
87
|
}
|
|
102
88
|
|
|
103
89
|
export function registerCommands(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
14
14
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
-
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
16
|
-
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
15
|
import { messageOf } from "./errors.ts";
|
|
18
16
|
import { formatCallText, formatResultText, scopeLabel } from "./format.ts";
|
|
19
17
|
import type { MemoryRuntime } from "./runtime.ts";
|
|
@@ -139,27 +137,21 @@ export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|
|
|
139
137
|
},
|
|
140
138
|
|
|
141
139
|
renderCall(args, theme, context) {
|
|
142
|
-
const text = reuseCallText(context.lastComponent);
|
|
143
140
|
const body = formatCallText(
|
|
144
141
|
args as { action?: unknown; text?: unknown; to?: unknown; scope?: unknown },
|
|
145
142
|
context.argsComplete,
|
|
146
143
|
);
|
|
147
|
-
|
|
148
|
-
return text;
|
|
144
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, body);
|
|
149
145
|
},
|
|
150
146
|
|
|
151
147
|
renderResult(result, { expanded }, theme, context) {
|
|
152
148
|
const text = reuseText(context.lastComponent);
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
text.setText(
|
|
156
|
-
return text;
|
|
157
|
-
}
|
|
158
|
-
if (details.error) {
|
|
159
|
-
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
149
|
+
const outcome = detailsOrLine(theme, result, expanded, toolDetails<MemoryDetails>(result.details));
|
|
150
|
+
if ("line" in outcome) {
|
|
151
|
+
text.setText(outcome.line);
|
|
160
152
|
return text;
|
|
161
153
|
}
|
|
162
|
-
text.setText(formatResultText(details, expanded));
|
|
154
|
+
text.setText(formatResultText(outcome.details, expanded));
|
|
163
155
|
return text;
|
|
164
156
|
},
|
|
165
157
|
});
|
|
@@ -10,7 +10,7 @@ import { Type } from "typebox";
|
|
|
10
10
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
11
11
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
12
12
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
14
14
|
import { PLAN_CONSENT, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
|
|
15
15
|
import { ENTER_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
16
16
|
import type { EnterPlanModeDetails } from "../types.ts";
|
|
@@ -72,18 +72,17 @@ export function registerEnterTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
72
72
|
},
|
|
73
73
|
|
|
74
74
|
renderCall(_args, theme, context) {
|
|
75
|
-
|
|
76
|
-
text.setText(toolCallTitle(theme, `${ENTER_TOOL} `) + paint(theme, "meta", "requested plan mode"));
|
|
77
|
-
return text;
|
|
75
|
+
return callLine(theme, context?.lastComponent, ENTER_TOOL, "requested plan mode");
|
|
78
76
|
},
|
|
79
77
|
|
|
80
78
|
renderResult(result, options, theme, context) {
|
|
81
79
|
const text = reuseText(context?.lastComponent);
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
text.setText(
|
|
80
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<EnterPlanModeDetails>(result.details));
|
|
81
|
+
if ("line" in outcome) {
|
|
82
|
+
text.setText(outcome.line);
|
|
85
83
|
return text;
|
|
86
84
|
}
|
|
85
|
+
const details = outcome.details;
|
|
87
86
|
if (details.unavailable) text.setText(paint(theme, "warn", "No UI to confirm plan mode"));
|
|
88
87
|
else if (details.already) text.setText(paint(theme, "meta", "Already in plan mode"));
|
|
89
88
|
else if (details.entered) text.setText(paint(theme, "ok", `${GLYPHS.check} Plan mode enabled`));
|
|
@@ -17,7 +17,7 @@ import { GOAL_TOOL } from "../../../lib/tool-names.ts";
|
|
|
17
17
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
18
18
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
19
19
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
|
|
21
21
|
import { feedbackSummary } from "../feedback.ts";
|
|
22
22
|
import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
|
|
23
23
|
import { sanitizePlanText } from "../plan-text.ts";
|
|
@@ -256,18 +256,17 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
|
|
|
256
256
|
},
|
|
257
257
|
|
|
258
258
|
renderCall(_args, theme, context) {
|
|
259
|
-
|
|
260
|
-
text.setText(toolCallTitle(theme, `${EXIT_TOOL} `) + paint(theme, "meta", "submitted a plan"));
|
|
261
|
-
return text;
|
|
259
|
+
return callLine(theme, context?.lastComponent, EXIT_TOOL, "submitted a plan");
|
|
262
260
|
},
|
|
263
261
|
|
|
264
262
|
renderResult(result, { expanded }, theme, context) {
|
|
265
263
|
const text = reuseText(context?.lastComponent);
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
text.setText(
|
|
264
|
+
const outcome = detailsOrLine(theme, result, expanded, toolDetails<ExitPlanModeDetails>(result.details));
|
|
265
|
+
if ("line" in outcome) {
|
|
266
|
+
text.setText(outcome.line);
|
|
269
267
|
return text;
|
|
270
268
|
}
|
|
269
|
+
const details = outcome.details;
|
|
271
270
|
const heading = details.unavailable
|
|
272
271
|
? paint(theme, "warn", "No UI to approve the plan")
|
|
273
272
|
: details.approved
|
|
@@ -12,7 +12,7 @@ import { Type, type Static } from "typebox";
|
|
|
12
12
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
13
13
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
14
14
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
16
16
|
import { describeUpdates, stepLabel } from "../format.ts";
|
|
17
17
|
import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
18
18
|
import { extractPlanSteps } from "../steps.ts";
|
|
@@ -103,7 +103,6 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
|
|
|
103
103
|
},
|
|
104
104
|
|
|
105
105
|
renderCall(args, theme, context) {
|
|
106
|
-
const text = reuseCallText(context?.lastComponent);
|
|
107
106
|
const updates = (args as Partial<SetPlanStepsArgs>).updates;
|
|
108
107
|
const label =
|
|
109
108
|
updates && updates.length === 1 && updates[0]?.step !== undefined
|
|
@@ -111,17 +110,17 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
|
|
|
111
110
|
: updates && updates.length > 1
|
|
112
111
|
? `${updates.length} steps`
|
|
113
112
|
: "steps";
|
|
114
|
-
|
|
115
|
-
return text;
|
|
113
|
+
return callLine(theme, context?.lastComponent, SET_PLAN_STEPS_TOOL, label);
|
|
116
114
|
},
|
|
117
115
|
|
|
118
116
|
renderResult(result, options, theme, context) {
|
|
119
117
|
const text = reuseText(context?.lastComponent);
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
text.setText(
|
|
118
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<SetPlanStepsDetails>(result.details));
|
|
119
|
+
if ("line" in outcome) {
|
|
120
|
+
text.setText(outcome.line);
|
|
123
121
|
return text;
|
|
124
122
|
}
|
|
123
|
+
const details = outcome.details;
|
|
125
124
|
const completed = details.updates.filter((update) => update.status === "completed").map((u) => u.step);
|
|
126
125
|
const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
|
|
127
126
|
const heading =
|
|
@@ -11,7 +11,7 @@ import { sanitizeLine } from "../../../lib/format.ts";
|
|
|
11
11
|
import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
|
|
12
12
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
13
13
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
|
|
15
15
|
import { overCap, stepElisionNote } from "../format.ts";
|
|
16
16
|
import { WRITE_PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
|
|
17
17
|
import { MAX_STEPS, PLAN_OUTLINE, planStepSummary } from "../steps.ts";
|
|
@@ -106,21 +106,20 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
|
|
|
106
106
|
},
|
|
107
107
|
|
|
108
108
|
renderCall(args, theme, context) {
|
|
109
|
-
const text = reuseCallText(context?.lastComponent);
|
|
110
109
|
const title = (args as Partial<WritePlanArgs>).title;
|
|
111
|
-
|
|
112
|
-
text.setText(toolCallTitle(theme, `${WRITE_PLAN_TOOL} `) + suffix);
|
|
113
|
-
return text;
|
|
110
|
+
return callLine(theme, context?.lastComponent, WRITE_PLAN_TOOL, title ? sanitizeLine(title) : "plan");
|
|
114
111
|
},
|
|
115
112
|
|
|
116
113
|
renderResult(result, options, theme, context) {
|
|
117
114
|
const text = reuseText(context?.lastComponent);
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
text.setText(
|
|
115
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<WritePlanDetails>(result.details));
|
|
116
|
+
if ("line" in outcome) {
|
|
117
|
+
text.setText(outcome.line);
|
|
121
118
|
return text;
|
|
122
119
|
}
|
|
123
|
-
text.setText(
|
|
120
|
+
text.setText(
|
|
121
|
+
paint(theme, "ok", `${GLYPHS.check} Saved plan `) + paint(theme, "meta", outcome.details.relativePath),
|
|
122
|
+
);
|
|
124
123
|
return text;
|
|
125
124
|
},
|
|
126
125
|
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
+
eventHeaders,
|
|
16
17
|
isLocalHostHeader,
|
|
17
18
|
jsonError,
|
|
18
19
|
jsonHeaders,
|
|
@@ -48,17 +49,6 @@ export interface ReviewContext {
|
|
|
48
49
|
onCancel: () => void;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
/**
|
|
52
|
-
* The event stream's headers: the one response that is not the page policy, because
|
|
53
|
-
* it is a long-lived `text/event-stream` rather than a document.
|
|
54
|
-
*/
|
|
55
|
-
const EVENT_HEADERS: Record<string, string> = {
|
|
56
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
57
|
-
"Cache-Control": "no-store",
|
|
58
|
-
Connection: "keep-alive",
|
|
59
|
-
"X-Accel-Buffering": "no",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
52
|
/**
|
|
63
53
|
* Parse and validate an `{ annotations }` request body. Returns the annotations,
|
|
64
54
|
* or an error response to send instead.
|
|
@@ -127,7 +117,7 @@ export async function handleReviewRequest(
|
|
|
127
117
|
if (pathname === "/" || pathname === "/index.html") {
|
|
128
118
|
return { status: 200, headers: pageHeaders(), body: renderReviewPage(await ctx.page()) };
|
|
129
119
|
}
|
|
130
|
-
if (pathname === "/events") return { status: 200, headers:
|
|
120
|
+
if (pathname === "/events") return { status: 200, headers: eventHeaders(), stream: true };
|
|
131
121
|
if (pathname === "/api/cancel") {
|
|
132
122
|
if (method.toUpperCase() !== "POST") return jsonError(405, "Use POST to cancel.");
|
|
133
123
|
ctx.onCancel();
|
package/extensions/plan/web.ts
CHANGED
|
@@ -14,8 +14,16 @@ import { randomBytes } from "node:crypto";
|
|
|
14
14
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
15
15
|
import { basename } from "node:path";
|
|
16
16
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
bindLoopback,
|
|
19
|
+
createCloser,
|
|
20
|
+
jsonHeaders,
|
|
21
|
+
openEventStream,
|
|
22
|
+
respond,
|
|
23
|
+
type ServeResponse,
|
|
24
|
+
} from "../../lib/http/index.ts";
|
|
18
25
|
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
26
|
+
import { type ArmTimer, defaultTimer } from "../../lib/timer.ts";
|
|
19
27
|
import { codeBody, fenceInfo, planBlocks } from "./blocks.ts";
|
|
20
28
|
import {
|
|
21
29
|
MAX_ANNOTATIONS,
|
|
@@ -75,6 +83,14 @@ export interface PlanWebReviewDeps {
|
|
|
75
83
|
port?: number;
|
|
76
84
|
/** Override the closed-tab grace; the default leaves a reload time to reconnect. */
|
|
77
85
|
dropGraceMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Override the timer that arms the closed-tab grace.
|
|
88
|
+
*
|
|
89
|
+
* The grace is a race against the browser's reload, so a test drives it by
|
|
90
|
+
* hand: a few tens of milliseconds of deadline makes the test fail when the
|
|
91
|
+
* machine is busy, which is a fact about the runner and not about the code.
|
|
92
|
+
*/
|
|
93
|
+
schedule?: ArmTimer;
|
|
78
94
|
/** Override the accepted body size; a test keeps it small. */
|
|
79
95
|
maxBodyBytes?: number;
|
|
80
96
|
}
|
|
@@ -138,12 +154,15 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
138
154
|
const done = new Promise<PlanReviewResult>((resolve) => {
|
|
139
155
|
resolveDone = resolve;
|
|
140
156
|
});
|
|
141
|
-
let
|
|
157
|
+
let cancelDrop: (() => void) | undefined;
|
|
158
|
+
const clearDrop = (): void => {
|
|
159
|
+
cancelDrop?.();
|
|
160
|
+
cancelDrop = undefined;
|
|
161
|
+
};
|
|
142
162
|
const finish = (result: PlanReviewResult): void => {
|
|
143
163
|
if (settled) return;
|
|
144
164
|
settled = true;
|
|
145
|
-
|
|
146
|
-
dropTimer = undefined;
|
|
165
|
+
clearDrop();
|
|
147
166
|
resolveDone(result);
|
|
148
167
|
};
|
|
149
168
|
|
|
@@ -251,13 +270,13 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
251
270
|
const clients = new Set<ServerResponse>();
|
|
252
271
|
let hadClient = false;
|
|
253
272
|
const dropGrace = deps.dropGraceMs ?? DROP_GRACE_MS;
|
|
273
|
+
const schedule = deps.schedule ?? defaultTimer;
|
|
254
274
|
const scheduleDrop = (): void => {
|
|
255
|
-
if (clients.size > 0 || !hadClient || settled ||
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
if (clients.size > 0 || !hadClient || settled || cancelDrop) return;
|
|
276
|
+
cancelDrop = schedule(dropGrace, () => {
|
|
277
|
+
cancelDrop = undefined;
|
|
258
278
|
if (clients.size === 0) finish({ status: "cancelled" });
|
|
259
|
-
}
|
|
260
|
-
(dropTimer as { unref?: () => void }).unref?.();
|
|
279
|
+
});
|
|
261
280
|
};
|
|
262
281
|
|
|
263
282
|
const server = createServer((request, response) => {
|
|
@@ -282,14 +301,10 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
282
301
|
};
|
|
283
302
|
}
|
|
284
303
|
if (result.stream) {
|
|
285
|
-
response
|
|
286
|
-
response.write("retry: 2000\n\n");
|
|
304
|
+
openEventStream(response, result);
|
|
287
305
|
hadClient = true;
|
|
288
306
|
clients.add(response);
|
|
289
|
-
|
|
290
|
-
clearTimeout(dropTimer);
|
|
291
|
-
dropTimer = undefined;
|
|
292
|
-
}
|
|
307
|
+
clearDrop();
|
|
293
308
|
request.on("close", () => {
|
|
294
309
|
clients.delete(response);
|
|
295
310
|
scheduleDrop();
|
|
@@ -7,7 +7,7 @@ import { askHuman } from "../../lib/interaction.ts";
|
|
|
7
7
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
8
8
|
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
9
9
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { detailsOrLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
11
11
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
12
12
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
13
13
|
import { ASK_USER_QUESTION_TOOL } from "../../lib/tool-names.ts";
|
|
@@ -94,11 +94,12 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
94
94
|
|
|
95
95
|
renderResult(result, options, theme, context) {
|
|
96
96
|
const component = reuseText(context?.lastComponent);
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
component.setText(
|
|
97
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<AskResult>(result.details));
|
|
98
|
+
if ("line" in outcome) {
|
|
99
|
+
component.setText(outcome.line);
|
|
100
100
|
return component;
|
|
101
101
|
}
|
|
102
|
+
const details = outcome.details;
|
|
102
103
|
if (details.unavailable) {
|
|
103
104
|
component.setText(paint(theme, "bad", UNAVAILABLE_TEXT));
|
|
104
105
|
return component;
|
|
@@ -82,8 +82,15 @@ export abstract class ChecklistScreen<T> implements Component {
|
|
|
82
82
|
/** A verb other than navigation, toggle, all, none, and enter. */
|
|
83
83
|
protected verb?(data: string): void;
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The row under the cursor.
|
|
87
|
+
*
|
|
88
|
+
* Not named `focused`: Pi's `Focusable` check is `"focused" in component`, and a
|
|
89
|
+
* component that answers it is handed the focus flag by assignment, which a
|
|
90
|
+
* read-only accessor turns into a process-ending `TypeError`. The checklist
|
|
91
|
+
* tracks its own cursor and needs no hardware cursor.
|
|
92
|
+
*/
|
|
93
|
+
protected get focusedRow(): T | undefined {
|
|
87
94
|
return this.rows()[this.cursor.selected];
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -107,7 +114,7 @@ export abstract class ChecklistScreen<T> implements Component {
|
|
|
107
114
|
}
|
|
108
115
|
if (navigateList(data, this.cursor, this.rows().length)) return;
|
|
109
116
|
if (data === " ") {
|
|
110
|
-
const row = this.
|
|
117
|
+
const row = this.focusedRow;
|
|
111
118
|
if (row !== undefined) this.toggle(row);
|
|
112
119
|
this.options.requestRender();
|
|
113
120
|
return;
|
package/extensions/rewind/git.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
gitDir,
|
|
21
21
|
repoRoot,
|
|
22
22
|
revParse,
|
|
23
|
+
revParseMany,
|
|
23
24
|
runGitOrThrow as run,
|
|
24
25
|
type RunGit,
|
|
25
26
|
} from "../../lib/git/index.ts";
|
|
@@ -27,7 +28,7 @@ import { parseNameStatus, parseNumStat, type ChangedPath, type PathStat } from "
|
|
|
27
28
|
|
|
28
29
|
// Re-export exactly the shared helpers this extension consumes, so rewind code
|
|
29
30
|
// has one git import surface instead of reaching into `lib/git` directly.
|
|
30
|
-
export { GitError, currentBranch, gitDir, repoRoot, revParse, type RunGit };
|
|
31
|
+
export { GitError, currentBranch, gitDir, repoRoot, revParse, revParseMany, type RunGit };
|
|
31
32
|
|
|
32
33
|
/** Timeout budget for one git command issued by this extension. */
|
|
33
34
|
const SNAPSHOT_TIMEOUT_MS = 30_000;
|
|
@@ -262,20 +263,32 @@ export async function applyPatchReverse(runGit: RunGit, cwd: string, patch: stri
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* The markers an interrupted git operation leaves, in precedence order: the
|
|
268
|
+
* first one present names the operation a rewind would clobber.
|
|
269
|
+
*/
|
|
270
|
+
const BUSY_MARKERS: ReadonlyArray<readonly [string, string]> = [
|
|
271
|
+
["MERGE_HEAD", "merge"],
|
|
272
|
+
["rebase-merge", "rebase"],
|
|
273
|
+
["rebase-apply", "rebase"],
|
|
274
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
275
|
+
["REVERT_HEAD", "revert"],
|
|
276
|
+
["BISECT_LOG", "bisect"],
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Label of an in-progress git operation that a rewind must not clobber, if any.
|
|
281
|
+
*
|
|
282
|
+
* `rev-parse --git-path` prints one path per argument, so all six markers are one
|
|
283
|
+
* git call: a clean repository — the common case, where every one of them is
|
|
284
|
+
* missing — would otherwise pay six spawns to learn nothing is in progress.
|
|
285
|
+
*/
|
|
266
286
|
export async function busyGitState(runGit: RunGit, cwd: string): Promise<string | undefined> {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
[
|
|
272
|
-
["REVERT_HEAD", "revert"],
|
|
273
|
-
["BISECT_LOG", "bisect"],
|
|
274
|
-
];
|
|
275
|
-
for (const [name, label] of markers) {
|
|
276
|
-
const result = await runGit(["rev-parse", "--git-path", name], { cwd });
|
|
277
|
-
if (result.code !== 0) continue;
|
|
278
|
-
const path = result.stdout.trim();
|
|
287
|
+
const result = await runGit(["rev-parse", ...BUSY_MARKERS.flatMap(([name]) => ["--git-path", name])], { cwd });
|
|
288
|
+
if (result.code !== 0) return undefined;
|
|
289
|
+
const paths = result.stdout.split("\n");
|
|
290
|
+
for (const [index, [, label]] of BUSY_MARKERS.entries()) {
|
|
291
|
+
const path = paths[index]?.trim();
|
|
279
292
|
if (path && existsSync(resolve(cwd, path))) return label;
|
|
280
293
|
}
|
|
281
294
|
return undefined;
|
|
@@ -94,7 +94,7 @@ export class HunkPickerComponent extends ChecklistScreen<HunkRow> {
|
|
|
94
94
|
|
|
95
95
|
/** The focused hunk's first removed and added line. */
|
|
96
96
|
protected detailLines(width: number): string[] {
|
|
97
|
-
const focused = this.
|
|
97
|
+
const focused = this.focusedRow;
|
|
98
98
|
if (!focused) return [];
|
|
99
99
|
return previewOf(focused.hunk).map((line) => {
|
|
100
100
|
const role = line.startsWith("+") ? "ok" : line.startsWith("-") ? "bad" : "meta";
|
|
@@ -137,7 +137,7 @@ export class PathPickerComponent extends ChecklistScreen<Row> {
|
|
|
137
137
|
|
|
138
138
|
/** What the focused row will do, in words. */
|
|
139
139
|
protected detailLines(width: number): string[] {
|
|
140
|
-
const row = this.
|
|
140
|
+
const row = this.focusedRow;
|
|
141
141
|
if (!row) return [];
|
|
142
142
|
const text =
|
|
143
143
|
row.kind === "group"
|
|
@@ -133,7 +133,7 @@ export class ReviewListComponent extends ChecklistScreen<ReviewEntry> {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
protected onEnter(): void {
|
|
136
|
-
const entry = this.
|
|
136
|
+
const entry = this.focusedRow;
|
|
137
137
|
if (entry) this.close({ kind: "diff", path: entry.path });
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -142,7 +142,7 @@ export class ReviewListComponent extends ChecklistScreen<ReviewEntry> {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
protected verb(data: string): void {
|
|
145
|
-
const entry = this.
|
|
145
|
+
const entry = this.focusedRow;
|
|
146
146
|
if (!entry) return;
|
|
147
147
|
if (data === "r") {
|
|
148
148
|
const paths = this.chosen.size > 0 ? [...this.chosen] : [entry.path];
|
|
@@ -174,7 +174,7 @@ export class ReviewListComponent extends ChecklistScreen<ReviewEntry> {
|
|
|
174
174
|
|
|
175
175
|
protected detailLines(width: number): string[] {
|
|
176
176
|
const detail: string[] = [];
|
|
177
|
-
const focused = this.
|
|
177
|
+
const focused = this.focusedRow;
|
|
178
178
|
if (focused) detail.push(clip(` ${paint(this.theme, "meta", formatReviewDetail(focused))}`, width));
|
|
179
179
|
const selectionLine =
|
|
180
180
|
this.chosen.size > 0
|