@gavin-hu/my-pi-agent 0.15.1 → 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 +10 -7
- package/README.zh-CN.md +8 -5
- 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 +38 -18
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +2 -2
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/server.ts +13 -2
- 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.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- 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/lib/README.md +9 -6
- package/lib/escape.ts +15 -2
- package/lib/pdf.ts +2 -2
- 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/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
|
@@ -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
|
});
|
package/extensions/plan/web.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type ServeResponse,
|
|
24
24
|
} from "../../lib/http/index.ts";
|
|
25
25
|
import { fenceLanguage, highlightLines } from "../../lib/highlight.ts";
|
|
26
|
+
import { type ArmTimer, defaultTimer } from "../../lib/timer.ts";
|
|
26
27
|
import { codeBody, fenceInfo, planBlocks } from "./blocks.ts";
|
|
27
28
|
import {
|
|
28
29
|
MAX_ANNOTATIONS,
|
|
@@ -82,6 +83,14 @@ export interface PlanWebReviewDeps {
|
|
|
82
83
|
port?: number;
|
|
83
84
|
/** Override the closed-tab grace; the default leaves a reload time to reconnect. */
|
|
84
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;
|
|
85
94
|
/** Override the accepted body size; a test keeps it small. */
|
|
86
95
|
maxBodyBytes?: number;
|
|
87
96
|
}
|
|
@@ -145,12 +154,15 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
145
154
|
const done = new Promise<PlanReviewResult>((resolve) => {
|
|
146
155
|
resolveDone = resolve;
|
|
147
156
|
});
|
|
148
|
-
let
|
|
157
|
+
let cancelDrop: (() => void) | undefined;
|
|
158
|
+
const clearDrop = (): void => {
|
|
159
|
+
cancelDrop?.();
|
|
160
|
+
cancelDrop = undefined;
|
|
161
|
+
};
|
|
149
162
|
const finish = (result: PlanReviewResult): void => {
|
|
150
163
|
if (settled) return;
|
|
151
164
|
settled = true;
|
|
152
|
-
|
|
153
|
-
dropTimer = undefined;
|
|
165
|
+
clearDrop();
|
|
154
166
|
resolveDone(result);
|
|
155
167
|
};
|
|
156
168
|
|
|
@@ -258,13 +270,13 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
258
270
|
const clients = new Set<ServerResponse>();
|
|
259
271
|
let hadClient = false;
|
|
260
272
|
const dropGrace = deps.dropGraceMs ?? DROP_GRACE_MS;
|
|
273
|
+
const schedule = deps.schedule ?? defaultTimer;
|
|
261
274
|
const scheduleDrop = (): void => {
|
|
262
|
-
if (clients.size > 0 || !hadClient || settled ||
|
|
263
|
-
|
|
264
|
-
|
|
275
|
+
if (clients.size > 0 || !hadClient || settled || cancelDrop) return;
|
|
276
|
+
cancelDrop = schedule(dropGrace, () => {
|
|
277
|
+
cancelDrop = undefined;
|
|
265
278
|
if (clients.size === 0) finish({ status: "cancelled" });
|
|
266
|
-
}
|
|
267
|
-
(dropTimer as { unref?: () => void }).unref?.();
|
|
279
|
+
});
|
|
268
280
|
};
|
|
269
281
|
|
|
270
282
|
const server = createServer((request, response) => {
|
|
@@ -292,10 +304,7 @@ export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps =
|
|
|
292
304
|
openEventStream(response, result);
|
|
293
305
|
hadClient = true;
|
|
294
306
|
clients.add(response);
|
|
295
|
-
|
|
296
|
-
clearTimeout(dropTimer);
|
|
297
|
-
dropTimer = undefined;
|
|
298
|
-
}
|
|
307
|
+
clearDrop();
|
|
299
308
|
request.on("close", () => {
|
|
300
309
|
clients.delete(response);
|
|
301
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;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { sanitizeLine, stripControlChars } from "../../lib/format.ts";
|
|
13
13
|
import type { AgentConfig } from "./agents.ts";
|
|
14
14
|
import { formatUsageStats } from "./format.ts";
|
|
15
15
|
import type { RunOptions } from "./run.ts";
|
|
@@ -458,12 +458,6 @@ export function createBackgroundRuntime(deps: BackgroundDeps): BackgroundRuntime
|
|
|
458
458
|
return { start, list, collect, cancel, shutdown, reset, subscribe };
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
/** A human duration for a run; kept here so the board and collect agree. */
|
|
462
|
-
export function backgroundDuration(run: BackgroundRun, now = Date.now()): string {
|
|
463
|
-
const end = run.finishedAt ?? now;
|
|
464
|
-
return formatDuration(Math.max(0, end - run.startedAt));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
461
|
/** Whether a run can be collected. */
|
|
468
462
|
export function canCollect(run: BackgroundRun): boolean {
|
|
469
463
|
return run.status !== "running" && run.status !== "interrupted";
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
15
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
17
16
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
18
17
|
import { normalizeTodos } from "../../lib/todo-state.ts";
|
|
18
|
+
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
19
19
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
20
|
-
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
21
20
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
22
21
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
23
22
|
import type { TodoRuntime } from "./runtime.ts";
|
|
@@ -98,24 +97,21 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
98
97
|
},
|
|
99
98
|
|
|
100
99
|
renderCall(args, theme, context) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
104
|
-
paint(theme, "meta", formatCallText(args.todos as Todo[] | undefined, context.argsComplete)),
|
|
105
|
-
);
|
|
106
|
-
return text;
|
|
100
|
+
const body = formatCallText(args.todos as Todo[] | undefined, context.argsComplete);
|
|
101
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, body);
|
|
107
102
|
},
|
|
108
103
|
|
|
109
104
|
renderResult(result, { expanded }, theme, context) {
|
|
110
105
|
const text = reuseText(context.lastComponent);
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
text.setText(
|
|
106
|
+
const outcome = detailsOrLine(theme, result, expanded, toolDetails<TodoDetails>(result.details));
|
|
107
|
+
if ("line" in outcome) {
|
|
108
|
+
text.setText(outcome.line);
|
|
114
109
|
return text;
|
|
115
110
|
}
|
|
116
|
-
|
|
117
|
-
// block
|
|
118
|
-
|
|
111
|
+
const details = outcome.details;
|
|
112
|
+
// A details block can still arrive without the list: the host rejected the
|
|
113
|
+
// arguments, so its message is the whole result.
|
|
114
|
+
if (!Array.isArray(details.todos)) {
|
|
119
115
|
text.setText(rejectionLine(theme, result, expanded));
|
|
120
116
|
return text;
|
|
121
117
|
}
|
|
@@ -236,8 +236,8 @@ outlive the session.
|
|
|
236
236
|
|
|
237
237
|
## Testing
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
`runtime.test.ts` covers config, schema, the child supervisor, and the tailer
|
|
240
|
+
with a fake clock and fake children; `tools.test.ts` covers the tool and command;
|
|
241
|
+
`ui.test.ts` covers registration, formatting, the status chips, and the screen
|
|
242
|
+
through the shared `createFakePi`/`fakeCtx` doubles. The test harness is
|
|
243
|
+
`test/helpers/fixtures/watch.ts`.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
11
|
+
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
12
12
|
import { formatWatchList, shortLabel } from "./format.ts";
|
|
13
13
|
import type { WatchRuntime } from "./runtime.ts";
|
|
14
14
|
import { WatchListComponent } from "./tui.ts";
|
|
@@ -92,54 +92,40 @@ export function registerCommands(pi: ExtensionAPI, runtime: WatchRuntime): void
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
() => runtime.list(),
|
|
107
|
-
theme,
|
|
108
|
-
{
|
|
109
|
-
lines: (watchId) => runtime.matches(watchId, 500),
|
|
110
|
-
stop: (watchId) => {
|
|
111
|
-
runtime.stop(watchId);
|
|
112
|
-
},
|
|
113
|
-
clear: () => {
|
|
114
|
-
const result = runtime.clear(undefined, false);
|
|
115
|
-
if (result.cleared > 0) {
|
|
116
|
-
ctx.ui.notify(`Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
117
|
-
}
|
|
118
|
-
},
|
|
95
|
+
await withPolledScreen(
|
|
96
|
+
pi,
|
|
97
|
+
ctx,
|
|
98
|
+
(screen) =>
|
|
99
|
+
new WatchListComponent(
|
|
100
|
+
() => runtime.list(),
|
|
101
|
+
screen.theme,
|
|
102
|
+
{
|
|
103
|
+
lines: (watchId) => runtime.matches(watchId, 500),
|
|
104
|
+
stop: (watchId) => {
|
|
105
|
+
runtime.stop(watchId);
|
|
119
106
|
},
|
|
120
|
-
() => {
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
clear: () => {
|
|
108
|
+
const result = runtime.clear(undefined, false);
|
|
109
|
+
if (result.cleared > 0) {
|
|
110
|
+
ctx.ui.notify(`Cleared ${result.cleared} watcher${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
111
|
+
}
|
|
123
112
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} finally {
|
|
141
|
-
stopTimer();
|
|
142
|
-
}
|
|
113
|
+
},
|
|
114
|
+
screen.close,
|
|
115
|
+
screen.requestRender,
|
|
116
|
+
screen.viewportRows,
|
|
117
|
+
),
|
|
118
|
+
{
|
|
119
|
+
pollMs: REFRESH_MS,
|
|
120
|
+
// The runtime's own clock owns file polling; this only refreshes what
|
|
121
|
+
// the screen shows, so opening it cannot change the poll cadence.
|
|
122
|
+
poll: (component, handles) => {
|
|
123
|
+
const open = component.currentLineId();
|
|
124
|
+
if (open) component.refreshLines(open);
|
|
125
|
+
handles.requestRender();
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
143
129
|
},
|
|
144
130
|
});
|
|
145
131
|
}
|
|
@@ -17,6 +17,7 @@ import type { KillTreeFn, SpawnFn, SpawnedProcess } from "../../lib/spawn.ts";
|
|
|
17
17
|
import { createChildSupervisor } from "./child.ts";
|
|
18
18
|
import { DEFAULT_CONFIG, loadConfig, type WatchConfig } from "./config.ts";
|
|
19
19
|
import { sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
20
|
+
import { defaultInterval } from "../../lib/timer.ts";
|
|
20
21
|
import { compileRegex, sourceLabel } from "./format.ts";
|
|
21
22
|
import { createUiController, type UiController } from "./ui.ts";
|
|
22
23
|
import type { PendingMatch, WatchMatch, WatchRecord, WatchSource } from "./types.ts";
|
|
@@ -118,7 +119,7 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
118
119
|
const watches = new Map<string, Internal>();
|
|
119
120
|
let counter = 0;
|
|
120
121
|
let disposed = false;
|
|
121
|
-
let clock:
|
|
122
|
+
let clock: (() => void) | undefined;
|
|
122
123
|
let onMatchHandler: ((matches: PendingMatch[]) => void) | undefined;
|
|
123
124
|
|
|
124
125
|
const record = (w: Internal): WatchRecord => ({
|
|
@@ -163,16 +164,13 @@ export function createWatchRuntime(options: WatchRuntimeOptions = {}): WatchRunt
|
|
|
163
164
|
});
|
|
164
165
|
|
|
165
166
|
const stopClock = (): void => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
clock = undefined;
|
|
169
|
-
}
|
|
167
|
+
clock?.();
|
|
168
|
+
clock = undefined;
|
|
170
169
|
};
|
|
171
170
|
|
|
172
171
|
const ensureClock = (): void => {
|
|
173
172
|
if (disposed || !autoClock || clock) return;
|
|
174
|
-
clock =
|
|
175
|
-
clock.unref?.();
|
|
173
|
+
clock = defaultInterval(config.pollMs, () => tick());
|
|
176
174
|
};
|
|
177
175
|
|
|
178
176
|
const pendingMatches = (): PendingMatch[] => {
|
|
@@ -11,7 +11,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
12
|
import { errorResult } from "../../lib/tool-result.ts";
|
|
13
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
15
15
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
16
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
17
|
import {
|
|
@@ -182,29 +182,18 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
182
182
|
|
|
183
183
|
renderCall(args, theme, context) {
|
|
184
184
|
const action = typeof args.action === "string" ? args.action : "watch";
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
188
|
-
paint(
|
|
189
|
-
theme,
|
|
190
|
-
"meta",
|
|
191
|
-
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
192
|
-
),
|
|
193
|
-
);
|
|
194
|
-
return text;
|
|
185
|
+
const subject = formatCallText(action, args as Record<string, unknown>, context.argsComplete);
|
|
186
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, subject, "middle");
|
|
195
187
|
},
|
|
196
188
|
|
|
197
189
|
renderResult(result, options, theme, context) {
|
|
198
190
|
const text = reuseText(context.lastComponent);
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
text.setText(
|
|
202
|
-
return text;
|
|
203
|
-
}
|
|
204
|
-
if (details.error) {
|
|
205
|
-
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
191
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<WatchDetails>(result.details));
|
|
192
|
+
if ("line" in outcome) {
|
|
193
|
+
text.setText(outcome.line);
|
|
206
194
|
return text;
|
|
207
195
|
}
|
|
196
|
+
const details = outcome.details;
|
|
208
197
|
|
|
209
198
|
if (details.action === "list" && details.watches) {
|
|
210
199
|
if (details.watches.length === 0) {
|
|
@@ -378,9 +378,9 @@ the content type and size.
|
|
|
378
378
|
|
|
379
379
|
## Testing
|
|
380
380
|
|
|
381
|
-
`extensions/web-access/` mirrors the halves (`search/`, `fetch/`) plus
|
|
382
|
-
`
|
|
383
|
-
`fetch`
|
|
381
|
+
`extensions/web-access/` mirrors the halves (`search/`, `fetch/`) plus the
|
|
382
|
+
`http.test.ts` suite for the shared runner, charset, config, and registration.
|
|
383
|
+
It covers the injected `fetch` runner, the provider registry and resolution, each provider's URL builder and
|
|
384
384
|
parser (DuckDuckGo, SearXNG, Brave), the orchestration pipeline and throttle,
|
|
385
385
|
charset decoding (declared, `<meta>`, and BOM precedence), a streamed body over
|
|
386
386
|
the byte cap, redirect re-validation (per-hop SSRF with literal IPs),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { messageOf } from "../../../lib/errors.ts";
|
|
11
|
+
import { abortableSleep } from "../../../lib/timer.ts";
|
|
11
12
|
import { createFetchRunner, type HttpRunner } from "../http.ts";
|
|
12
13
|
import { resolveSearchApiKey, type WebSearchConfig } from "./config.ts";
|
|
13
14
|
import { resolveProvider } from "./registry.ts";
|
|
@@ -40,24 +41,14 @@ export interface SearchDeps {
|
|
|
40
41
|
/** A sleeper, injectable so throttle scheduling is testable without wall time. */
|
|
41
42
|
type ThrottleSleep = (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
reject(new WebSearchError("Search aborted."));
|
|
52
|
-
};
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
finish();
|
|
55
|
-
resolve();
|
|
56
|
-
}, ms);
|
|
57
|
-
if (signal?.aborted) return onAbort();
|
|
58
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
44
|
+
/** The shared abortable sleep, mapped to the one error this extension throws. */
|
|
45
|
+
const delay: ThrottleSleep = async (ms, signal) => {
|
|
46
|
+
try {
|
|
47
|
+
await abortableSleep(ms, signal);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new WebSearchError("Search aborted.");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
61
52
|
|
|
62
53
|
/**
|
|
63
54
|
* A stateful throttle that spaces requests by `minIntervalMs`. `now` and `sleep`
|