@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.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/CHANGELOG.md +100 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +1 -7
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +2 -3
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +74 -61
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +72 -23
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +39 -24
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
package/src/tools/browser.ts
CHANGED
|
@@ -10,6 +10,7 @@ import puppeteer from "puppeteer";
|
|
|
10
10
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
11
11
|
import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
|
|
12
12
|
import type { ToolSession } from "../sdk";
|
|
13
|
+
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
13
14
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
14
15
|
import type { OutputMeta } from "./output-meta";
|
|
15
16
|
import { resolveToCwd } from "./path-utils";
|
|
@@ -60,6 +61,172 @@ const INTERACTIVE_AX_ROLES = new Set([
|
|
|
60
61
|
"treeitem",
|
|
61
62
|
]);
|
|
62
63
|
|
|
64
|
+
const LEGACY_SELECTOR_PREFIXES = ["p-aria/", "p-text/", "p-xpath/", "p-pierce/"] as const;
|
|
65
|
+
|
|
66
|
+
function normalizeSelector(selector: string): string {
|
|
67
|
+
if (!selector) return selector;
|
|
68
|
+
if (selector.startsWith("p-") && !LEGACY_SELECTOR_PREFIXES.some(prefix => selector.startsWith(prefix))) {
|
|
69
|
+
throw new ToolError(
|
|
70
|
+
`Unsupported selector prefix. Use CSS or puppeteer query handlers (aria/, text/, xpath/, pierce/). Got: ${selector}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (selector.startsWith("p-text/")) {
|
|
74
|
+
return `text/${selector.slice("p-text/".length)}`;
|
|
75
|
+
}
|
|
76
|
+
if (selector.startsWith("p-xpath/")) {
|
|
77
|
+
return `xpath/${selector.slice("p-xpath/".length)}`;
|
|
78
|
+
}
|
|
79
|
+
if (selector.startsWith("p-pierce/")) {
|
|
80
|
+
return `pierce/${selector.slice("p-pierce/".length)}`;
|
|
81
|
+
}
|
|
82
|
+
if (selector.startsWith("p-aria/")) {
|
|
83
|
+
const rest = selector.slice("p-aria/".length);
|
|
84
|
+
// Playwright-style: p-aria/[name="Sign in"] → aria/Sign in
|
|
85
|
+
const nameMatch = rest.match(/\[\s*name\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\]]+))\s*\]/);
|
|
86
|
+
const name = nameMatch?.[1] ?? nameMatch?.[2] ?? nameMatch?.[3];
|
|
87
|
+
if (name) return `aria/${name.trim()}`;
|
|
88
|
+
return `aria/${rest}`;
|
|
89
|
+
}
|
|
90
|
+
return selector;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; reason: string };
|
|
94
|
+
|
|
95
|
+
async function resolveActionableQueryHandlerClickTarget(handles: ElementHandle[]): Promise<ElementHandle | null> {
|
|
96
|
+
const candidates: Array<{ handle: ElementHandle; rect: { x: number; y: number; w: number; h: number } }> = [];
|
|
97
|
+
|
|
98
|
+
for (const handle of handles) {
|
|
99
|
+
let clickable: ElementHandle = handle;
|
|
100
|
+
let clickableProxy: ElementHandle | null = null;
|
|
101
|
+
try {
|
|
102
|
+
const proxy = await handle.evaluateHandle(el => {
|
|
103
|
+
const target =
|
|
104
|
+
(el as Element).closest(
|
|
105
|
+
'a,button,[role="button"],[role="link"],input[type="button"],input[type="submit"]',
|
|
106
|
+
) ?? el;
|
|
107
|
+
return target;
|
|
108
|
+
});
|
|
109
|
+
const nodeHandle = proxy.asElement();
|
|
110
|
+
clickableProxy = nodeHandle ? (nodeHandle as unknown as ElementHandle) : null;
|
|
111
|
+
if (clickableProxy) {
|
|
112
|
+
clickable = clickableProxy;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const intersecting = await clickable.isIntersectingViewport();
|
|
120
|
+
if (!intersecting) continue;
|
|
121
|
+
const rect = (await clickable.evaluate(el => {
|
|
122
|
+
const r = (el as Element).getBoundingClientRect();
|
|
123
|
+
return { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
124
|
+
})) as { x: number; y: number; w: number; h: number };
|
|
125
|
+
if (rect.w < 1 || rect.h < 1) continue;
|
|
126
|
+
candidates.push({ handle: clickable, rect });
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore
|
|
129
|
+
} finally {
|
|
130
|
+
if (clickableProxy && clickableProxy !== handle) {
|
|
131
|
+
try {
|
|
132
|
+
await clickableProxy.dispose();
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!candidates.length) return null;
|
|
139
|
+
|
|
140
|
+
// Prefer top-most visible element (nav/header usually wins), tie-break by left-most.
|
|
141
|
+
candidates.sort((a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x);
|
|
142
|
+
return candidates[0]?.handle ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function isClickActionable(handle: ElementHandle): Promise<ActionabilityResult> {
|
|
146
|
+
return (await handle.evaluate(el => {
|
|
147
|
+
const element = el as HTMLElement;
|
|
148
|
+
const style = globalThis.getComputedStyle(element);
|
|
149
|
+
if (style.display === "none") return { ok: false as const, reason: "display:none" };
|
|
150
|
+
if (style.visibility === "hidden") return { ok: false as const, reason: "visibility:hidden" };
|
|
151
|
+
if (style.pointerEvents === "none") return { ok: false as const, reason: "pointer-events:none" };
|
|
152
|
+
if (Number(style.opacity) === 0) return { ok: false as const, reason: "opacity:0" };
|
|
153
|
+
|
|
154
|
+
const r = element.getBoundingClientRect();
|
|
155
|
+
if (r.width < 1 || r.height < 1) return { ok: false as const, reason: "zero-size" };
|
|
156
|
+
|
|
157
|
+
const vw = globalThis.innerWidth;
|
|
158
|
+
const vh = globalThis.innerHeight;
|
|
159
|
+
const left = Math.max(0, Math.min(vw, r.left));
|
|
160
|
+
const right = Math.max(0, Math.min(vw, r.right));
|
|
161
|
+
const top = Math.max(0, Math.min(vh, r.top));
|
|
162
|
+
const bottom = Math.max(0, Math.min(vh, r.bottom));
|
|
163
|
+
if (right - left < 1 || bottom - top < 1) return { ok: false as const, reason: "off-viewport" };
|
|
164
|
+
|
|
165
|
+
const x = Math.floor((left + right) / 2);
|
|
166
|
+
const y = Math.floor((top + bottom) / 2);
|
|
167
|
+
const topEl = globalThis.document.elementFromPoint(x, y);
|
|
168
|
+
if (!topEl) return { ok: false as const, reason: "elementFromPoint-null" };
|
|
169
|
+
if (topEl === element || element.contains(topEl) || (topEl as Element).contains(element)) {
|
|
170
|
+
return { ok: true as const, x, y };
|
|
171
|
+
}
|
|
172
|
+
return { ok: false as const, reason: "obscured" };
|
|
173
|
+
})) as ActionabilityResult;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function clickQueryHandlerText(
|
|
177
|
+
page: Page,
|
|
178
|
+
selector: string,
|
|
179
|
+
timeoutMs: number,
|
|
180
|
+
signal?: AbortSignal,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
183
|
+
const clickSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
184
|
+
const start = Date.now();
|
|
185
|
+
let lastSeen = 0;
|
|
186
|
+
let lastReason: string | null = null;
|
|
187
|
+
|
|
188
|
+
while (Date.now() - start < timeoutMs) {
|
|
189
|
+
throwIfAborted(clickSignal);
|
|
190
|
+
const handles = (await untilAborted(clickSignal, () => page.$$(selector))) as ElementHandle[];
|
|
191
|
+
try {
|
|
192
|
+
lastSeen = handles.length;
|
|
193
|
+
const target = await resolveActionableQueryHandlerClickTarget(handles);
|
|
194
|
+
if (!target) {
|
|
195
|
+
lastReason = handles.length ? "no-visible-candidate" : "no-matches";
|
|
196
|
+
await Bun.sleep(100);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const actionability = await isClickActionable(target);
|
|
200
|
+
if (!actionability.ok) {
|
|
201
|
+
lastReason = actionability.reason;
|
|
202
|
+
await Bun.sleep(100);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await untilAborted(clickSignal, () => target.click());
|
|
208
|
+
return;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
lastReason = err instanceof Error ? err.message : String(err);
|
|
211
|
+
await Bun.sleep(100);
|
|
212
|
+
}
|
|
213
|
+
} finally {
|
|
214
|
+
await Promise.all(
|
|
215
|
+
handles.map(async h => {
|
|
216
|
+
try {
|
|
217
|
+
await h.dispose();
|
|
218
|
+
} catch {}
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw new ToolError(
|
|
225
|
+
`Timed out clicking ${selector} (seen ${lastSeen} matches; last reason: ${lastReason ?? "unknown"}). ` +
|
|
226
|
+
"If there are multiple matching elements, use observe+click_id or a more specific selector.",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
63
230
|
/**
|
|
64
231
|
* Stealth init scripts for Puppeteer.
|
|
65
232
|
*/
|
|
@@ -93,7 +260,10 @@ function resolvePageClient(page: Page): PuppeteerCdpClient | null {
|
|
|
93
260
|
|
|
94
261
|
const puppeteerGetArgsSchema = Type.Array(
|
|
95
262
|
Type.Object({
|
|
96
|
-
selector: Type.String({
|
|
263
|
+
selector: Type.String({
|
|
264
|
+
description:
|
|
265
|
+
"Selector for the target element (CSS, or puppeteer query handler like aria/, text/, xpath/, pierce/; also accepts legacy p- prefixes)",
|
|
266
|
+
}),
|
|
97
267
|
attribute: Type.Optional(Type.String({ description: "Attribute name (get_attribute)" })),
|
|
98
268
|
}),
|
|
99
269
|
{ description: "Batch arguments for get_* actions", minItems: 1 },
|
|
@@ -126,7 +296,12 @@ const browserSchema = Type.Object({
|
|
|
126
296
|
{ description: "Action to perform" },
|
|
127
297
|
),
|
|
128
298
|
url: Type.Optional(Type.String({ description: "URL to navigate to (goto)" })),
|
|
129
|
-
selector: Type.Optional(
|
|
299
|
+
selector: Type.Optional(
|
|
300
|
+
Type.String({
|
|
301
|
+
description:
|
|
302
|
+
"Selector for the target element (CSS, or puppeteer query handler like aria/, text/, xpath/, pierce/; also accepts legacy p- prefixes)",
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
130
305
|
element_id: Type.Optional(Type.Number({ description: "Element ID from observe" })),
|
|
131
306
|
include_all: Type.Optional(Type.Boolean({ description: "Include non-interactive nodes in observe" })),
|
|
132
307
|
viewport_only: Type.Optional(Type.Boolean({ description: "Limit observe output to elements in the viewport" })),
|
|
@@ -159,8 +334,18 @@ const browserSchema = Type.Object({
|
|
|
159
334
|
),
|
|
160
335
|
delta_x: Type.Optional(Type.Number({ description: "Scroll delta X (scroll)" })),
|
|
161
336
|
delta_y: Type.Optional(Type.Number({ description: "Scroll delta Y (scroll)" })),
|
|
162
|
-
from_selector: Type.Optional(
|
|
163
|
-
|
|
337
|
+
from_selector: Type.Optional(
|
|
338
|
+
Type.String({
|
|
339
|
+
description:
|
|
340
|
+
"Drag start selector (CSS, or puppeteer query handler like aria/, text/, xpath/, pierce/; also accepts legacy p- prefixes)",
|
|
341
|
+
}),
|
|
342
|
+
),
|
|
343
|
+
to_selector: Type.Optional(
|
|
344
|
+
Type.String({
|
|
345
|
+
description:
|
|
346
|
+
"Drag end selector (CSS, or puppeteer query handler like aria/, text/, xpath/, pierce/; also accepts legacy p- prefixes)",
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
164
349
|
});
|
|
165
350
|
|
|
166
351
|
/** Input schema for the Puppeteer tool. */
|
|
@@ -287,20 +472,23 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
287
472
|
private async resetBrowser(params?: BrowserParams): Promise<Page> {
|
|
288
473
|
await this.closeBrowser();
|
|
289
474
|
this.currentHeadless = this.session.settings.get("browser.headless");
|
|
475
|
+
const initialViewport = params?.viewport ?? DEFAULT_VIEWPORT;
|
|
290
476
|
this.browser = await puppeteer.launch({
|
|
291
477
|
headless: this.currentHeadless,
|
|
292
|
-
defaultViewport:
|
|
478
|
+
defaultViewport: this.currentHeadless ? initialViewport : null,
|
|
293
479
|
args: [
|
|
294
480
|
"--no-sandbox",
|
|
295
481
|
"--disable-setuid-sandbox",
|
|
296
482
|
"--disable-blink-features=AutomationControlled",
|
|
297
|
-
`--window-size=${
|
|
483
|
+
`--window-size=${initialViewport.width},${initialViewport.height}`,
|
|
298
484
|
],
|
|
299
485
|
ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULT_ARGS],
|
|
300
486
|
});
|
|
301
487
|
this.page = await this.browser.newPage();
|
|
302
488
|
await this.applyStealthPatches(this.page);
|
|
303
|
-
|
|
489
|
+
if (this.currentHeadless || params?.viewport) {
|
|
490
|
+
await this.applyViewport(this.page, params?.viewport);
|
|
491
|
+
}
|
|
304
492
|
return this.page;
|
|
305
493
|
}
|
|
306
494
|
|
|
@@ -317,7 +505,9 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
317
505
|
}
|
|
318
506
|
this.page = await this.browser.newPage();
|
|
319
507
|
await this.applyStealthPatches(this.page);
|
|
320
|
-
|
|
508
|
+
if (this.currentHeadless || params?.viewport) {
|
|
509
|
+
await this.applyViewport(this.page, params?.viewport);
|
|
510
|
+
}
|
|
321
511
|
return this.page;
|
|
322
512
|
}
|
|
323
513
|
|
|
@@ -795,8 +985,13 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
795
985
|
const selector = ensureParam(params.selector, "selector", params.action);
|
|
796
986
|
details.selector = selector;
|
|
797
987
|
const page = await this.ensurePage(params);
|
|
798
|
-
const
|
|
799
|
-
|
|
988
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
989
|
+
if (resolvedSelector.startsWith("text/")) {
|
|
990
|
+
await clickQueryHandlerText(page, resolvedSelector, timeoutMs, signal);
|
|
991
|
+
} else {
|
|
992
|
+
const locator = page.locator(resolvedSelector).setTimeout(timeoutMs);
|
|
993
|
+
await untilAborted(signal, () => locator.click());
|
|
994
|
+
}
|
|
800
995
|
return toolResult(details).text(`Clicked ${selector}`).done();
|
|
801
996
|
}
|
|
802
997
|
case "click_id": {
|
|
@@ -816,7 +1011,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
816
1011
|
const text = ensureParam(params.text, "text", params.action);
|
|
817
1012
|
details.selector = selector;
|
|
818
1013
|
const page = await this.ensurePage(params);
|
|
819
|
-
const
|
|
1014
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
1015
|
+
const locator = page.locator(resolvedSelector).setTimeout(timeoutMs);
|
|
820
1016
|
const handle = (await untilAborted(signal, () => locator.waitHandle())) as ElementHandle;
|
|
821
1017
|
await untilAborted(signal, () => handle.type(text, { delay: 0 }));
|
|
822
1018
|
await handle.dispose();
|
|
@@ -842,7 +1038,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
842
1038
|
const value = ensureParam(params.value, "value", params.action);
|
|
843
1039
|
details.selector = selector;
|
|
844
1040
|
const page = await this.ensurePage(params);
|
|
845
|
-
const
|
|
1041
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
1042
|
+
const locator = page.locator(resolvedSelector).setTimeout(timeoutMs);
|
|
846
1043
|
await untilAborted(signal, () => locator.fill(value));
|
|
847
1044
|
return toolResult(details).text(`Filled ${selector}`).done();
|
|
848
1045
|
}
|
|
@@ -873,7 +1070,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
873
1070
|
const key = ensureParam(params.key, "key", params.action) as KeyInput;
|
|
874
1071
|
const page = await this.ensurePage(params);
|
|
875
1072
|
if (params.selector) {
|
|
876
|
-
|
|
1073
|
+
const resolvedSelector = normalizeSelector(params.selector as string);
|
|
1074
|
+
await untilAborted(signal, () => page.focus(resolvedSelector));
|
|
877
1075
|
}
|
|
878
1076
|
await untilAborted(signal, () => page.keyboard.press(key));
|
|
879
1077
|
return toolResult(details).text(`Pressed ${key}`).done();
|
|
@@ -889,8 +1087,12 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
889
1087
|
const fromSelector = ensureParam(params.from_selector, "from_selector", params.action);
|
|
890
1088
|
const toSelector = ensureParam(params.to_selector, "to_selector", params.action);
|
|
891
1089
|
const page = await this.ensurePage(params);
|
|
892
|
-
const
|
|
893
|
-
const
|
|
1090
|
+
const resolvedFromSelector = normalizeSelector(fromSelector);
|
|
1091
|
+
const resolvedToSelector = normalizeSelector(toSelector);
|
|
1092
|
+
const fromHandle = (await untilAborted(signal, () =>
|
|
1093
|
+
page.$(resolvedFromSelector),
|
|
1094
|
+
)) as ElementHandle | null;
|
|
1095
|
+
const toHandle = (await untilAborted(signal, () => page.$(resolvedToSelector))) as ElementHandle | null;
|
|
894
1096
|
if (!fromHandle || !toHandle) {
|
|
895
1097
|
throw new ToolError("Drag selectors did not resolve to elements");
|
|
896
1098
|
}
|
|
@@ -925,7 +1127,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
925
1127
|
const selector = ensureParam(params.selector, "selector", params.action);
|
|
926
1128
|
details.selector = selector;
|
|
927
1129
|
const page = await this.ensurePage(params);
|
|
928
|
-
const
|
|
1130
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
1131
|
+
const locator = page.locator(resolvedSelector).setTimeout(timeoutMs);
|
|
929
1132
|
await untilAborted(signal, () => locator.wait());
|
|
930
1133
|
return toolResult(details).text(`Selector ready: ${selector}`).done();
|
|
931
1134
|
}
|
|
@@ -948,8 +1151,9 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
948
1151
|
const values = (await Promise.all(
|
|
949
1152
|
params.args.map((arg, index) => {
|
|
950
1153
|
const selector = ensureParam(arg.selector, `args[${index}].selector`, params.action);
|
|
1154
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
951
1155
|
return untilAborted(signal, () =>
|
|
952
|
-
page.$eval(
|
|
1156
|
+
page.$eval(resolvedSelector, (el: Element) => (el as HTMLElement).innerText),
|
|
953
1157
|
);
|
|
954
1158
|
}),
|
|
955
1159
|
)) as string[];
|
|
@@ -960,8 +1164,9 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
960
1164
|
}
|
|
961
1165
|
const selector = ensureParam(params.selector, "selector", params.action);
|
|
962
1166
|
details.selector = selector;
|
|
1167
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
963
1168
|
const value = (await untilAborted(signal, () =>
|
|
964
|
-
page.$eval(
|
|
1169
|
+
page.$eval(resolvedSelector, (el: Element) => (el as HTMLElement).innerText),
|
|
965
1170
|
)) as string;
|
|
966
1171
|
details.result = value;
|
|
967
1172
|
return toolResult(details).text(value).done();
|
|
@@ -972,8 +1177,9 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
972
1177
|
const values = (await Promise.all(
|
|
973
1178
|
params.args.map((arg, index) => {
|
|
974
1179
|
const selector = ensureParam(arg.selector, `args[${index}].selector`, params.action);
|
|
1180
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
975
1181
|
return untilAborted(signal, () =>
|
|
976
|
-
page.$eval(
|
|
1182
|
+
page.$eval(resolvedSelector, (el: Element) => (el as HTMLElement).innerHTML),
|
|
977
1183
|
);
|
|
978
1184
|
}),
|
|
979
1185
|
)) as string[];
|
|
@@ -984,8 +1190,9 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
984
1190
|
}
|
|
985
1191
|
const selector = ensureParam(params.selector, "selector", params.action);
|
|
986
1192
|
details.selector = selector;
|
|
1193
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
987
1194
|
const value = (await untilAborted(signal, () =>
|
|
988
|
-
page.$eval(
|
|
1195
|
+
page.$eval(resolvedSelector, (el: Element) => (el as HTMLElement).innerHTML),
|
|
989
1196
|
)) as string;
|
|
990
1197
|
details.result = value;
|
|
991
1198
|
return toolResult(details).text(value).done();
|
|
@@ -997,8 +1204,13 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
997
1204
|
params.args.map((arg, index) => {
|
|
998
1205
|
const selector = ensureParam(arg.selector, `args[${index}].selector`, params.action);
|
|
999
1206
|
const attribute = ensureParam(arg.attribute, `args[${index}].attribute`, params.action);
|
|
1207
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
1000
1208
|
return untilAborted(signal, () =>
|
|
1001
|
-
page.$eval(
|
|
1209
|
+
page.$eval(
|
|
1210
|
+
resolvedSelector,
|
|
1211
|
+
(el: Element, attr: string) => (el as HTMLElement).getAttribute(String(attr)),
|
|
1212
|
+
attribute,
|
|
1213
|
+
),
|
|
1002
1214
|
);
|
|
1003
1215
|
}),
|
|
1004
1216
|
)) as string[];
|
|
@@ -1010,9 +1222,10 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1010
1222
|
const selector = ensureParam(params.selector, "selector", params.action);
|
|
1011
1223
|
const attribute = ensureParam(params.attribute, "attribute", params.action);
|
|
1012
1224
|
details.selector = selector;
|
|
1225
|
+
const resolvedSelector = normalizeSelector(selector);
|
|
1013
1226
|
const value = (await untilAborted(signal, () =>
|
|
1014
1227
|
page.$eval(
|
|
1015
|
-
|
|
1228
|
+
resolvedSelector,
|
|
1016
1229
|
(el: { getAttribute: (name: string) => string | null }, attr: string) =>
|
|
1017
1230
|
el.getAttribute(String(attr)),
|
|
1018
1231
|
attribute,
|
|
@@ -1074,9 +1287,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1074
1287
|
let buffer: Buffer;
|
|
1075
1288
|
|
|
1076
1289
|
if (params.selector) {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
)) as ElementHandle | null;
|
|
1290
|
+
const resolvedSelector = normalizeSelector(params.selector as string);
|
|
1291
|
+
const handle = (await untilAborted(signal, () => page.$(resolvedSelector))) as ElementHandle | null;
|
|
1080
1292
|
if (!handle) {
|
|
1081
1293
|
throw new ToolError("Screenshot selector did not resolve to an element");
|
|
1082
1294
|
}
|
|
@@ -1110,15 +1322,25 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1110
1322
|
details.mimeType = mimeType;
|
|
1111
1323
|
details.bytes = buffer.length;
|
|
1112
1324
|
|
|
1325
|
+
// Compress for API content (same as pasted images)
|
|
1326
|
+
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
1327
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
1328
|
+
|
|
1113
1329
|
const lines = ["Screenshot captured", `Format: ${format}`, `Bytes: ${buffer.length}`];
|
|
1330
|
+
if (resized.wasResized) {
|
|
1331
|
+
lines.push(`Compressed: ${resized.width}x${resized.height} (${resized.mimeType})`);
|
|
1332
|
+
}
|
|
1114
1333
|
if (savedPath) {
|
|
1115
1334
|
lines.push(`Saved: ${savedPath}`);
|
|
1116
1335
|
}
|
|
1336
|
+
if (dimensionNote) {
|
|
1337
|
+
lines.push(dimensionNote);
|
|
1338
|
+
}
|
|
1117
1339
|
|
|
1118
1340
|
return toolResult(details)
|
|
1119
1341
|
.content([
|
|
1120
1342
|
{ type: "text", text: lines.join("\n") },
|
|
1121
|
-
{ type: "image", data:
|
|
1343
|
+
{ type: "image", data: resized.data, mimeType: resized.mimeType },
|
|
1122
1344
|
])
|
|
1123
1345
|
.done();
|
|
1124
1346
|
}
|
package/src/tools/calculator.ts
CHANGED
|
@@ -7,16 +7,9 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import calculatorDescription from "../prompts/tools/calculator.md" with { type: "text" };
|
|
10
|
-
import { renderStatusLine, renderTreeList } from "../tui";
|
|
10
|
+
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
11
11
|
import type { ToolSession } from ".";
|
|
12
|
-
import {
|
|
13
|
-
formatCount,
|
|
14
|
-
formatEmptyMessage,
|
|
15
|
-
formatErrorMessage,
|
|
16
|
-
PREVIEW_LIMITS,
|
|
17
|
-
TRUNCATE_LENGTHS,
|
|
18
|
-
truncateToWidth,
|
|
19
|
-
} from "./render-utils";
|
|
12
|
+
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS, TRUNCATE_LENGTHS } from "./render-utils";
|
|
20
13
|
|
|
21
14
|
// =============================================================================
|
|
22
15
|
// Token Types
|
|
@@ -466,7 +459,7 @@ export const calculatorToolRenderer = {
|
|
|
466
459
|
*/
|
|
467
460
|
renderResult(
|
|
468
461
|
result: { content: Array<{ type: string; text?: string }>; details?: CalculatorToolDetails; isError?: boolean },
|
|
469
|
-
|
|
462
|
+
options: RenderResultOptions,
|
|
470
463
|
uiTheme: Theme,
|
|
471
464
|
args?: CalculatorRenderArgs,
|
|
472
465
|
): Component {
|
|
@@ -474,7 +467,13 @@ export const calculatorToolRenderer = {
|
|
|
474
467
|
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
475
468
|
if (result.isError) {
|
|
476
469
|
const header = renderStatusLine({ icon: "error", title: "Calc" }, uiTheme);
|
|
477
|
-
|
|
470
|
+
const renderedLines = [header, formatErrorMessage(textContent, uiTheme)];
|
|
471
|
+
return {
|
|
472
|
+
render() {
|
|
473
|
+
return renderedLines;
|
|
474
|
+
},
|
|
475
|
+
invalidate() {},
|
|
476
|
+
};
|
|
478
477
|
}
|
|
479
478
|
|
|
480
479
|
// Prefer structured details; fall back to parsing text content
|
|
@@ -491,7 +490,13 @@ export const calculatorToolRenderer = {
|
|
|
491
490
|
|
|
492
491
|
if (outputs.length === 0) {
|
|
493
492
|
const header = renderStatusLine({ icon: "warning", title: "Calc" }, uiTheme);
|
|
494
|
-
|
|
493
|
+
const renderedLines = [header, formatEmptyMessage("No results", uiTheme)];
|
|
494
|
+
return {
|
|
495
|
+
render() {
|
|
496
|
+
return renderedLines;
|
|
497
|
+
},
|
|
498
|
+
invalidate() {},
|
|
499
|
+
};
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
const description = args?.calculations?.[0]?.expression
|
|
@@ -501,18 +506,32 @@ export const calculatorToolRenderer = {
|
|
|
501
506
|
{ icon: "success", title: "Calc", description, meta: [formatCount("result", outputs.length)] },
|
|
502
507
|
uiTheme,
|
|
503
508
|
);
|
|
504
|
-
const lines = renderTreeList(
|
|
505
|
-
{
|
|
506
|
-
items: outputs,
|
|
507
|
-
expanded,
|
|
508
|
-
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
509
|
-
itemType: "result",
|
|
510
|
-
renderItem: output => uiTheme.fg("toolOutput", output),
|
|
511
|
-
},
|
|
512
|
-
uiTheme,
|
|
513
|
-
);
|
|
514
509
|
|
|
515
|
-
|
|
510
|
+
let cached: RenderCache | undefined;
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
render(width) {
|
|
514
|
+
const { expanded } = options;
|
|
515
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
516
|
+
if (cached?.key === key) return cached.lines;
|
|
517
|
+
const treeLines = renderTreeList(
|
|
518
|
+
{
|
|
519
|
+
items: outputs,
|
|
520
|
+
expanded,
|
|
521
|
+
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
522
|
+
itemType: "result",
|
|
523
|
+
renderItem: output => uiTheme.fg("toolOutput", output),
|
|
524
|
+
},
|
|
525
|
+
uiTheme,
|
|
526
|
+
);
|
|
527
|
+
const lines = [header, ...treeLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
528
|
+
cached = { key, lines };
|
|
529
|
+
return lines;
|
|
530
|
+
},
|
|
531
|
+
invalidate() {
|
|
532
|
+
cached = undefined;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
516
535
|
},
|
|
517
536
|
mergeCallAndResult: true,
|
|
518
537
|
};
|
package/src/tools/fetch.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
12
12
|
import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
|
|
13
|
-
import {
|
|
13
|
+
import { renderStatusLine } from "../tui";
|
|
14
|
+
import { CachedOutputBlock } from "../tui/output-block";
|
|
14
15
|
import { ensureTool } from "../utils/tools-manager";
|
|
15
16
|
import { specialHandlers } from "../web/scrapers";
|
|
16
17
|
import type { RenderResult } from "../web/scrapers/types";
|
|
@@ -980,7 +981,6 @@ export function renderFetchResult(
|
|
|
980
981
|
options: RenderResultOptions,
|
|
981
982
|
uiTheme: Theme = theme,
|
|
982
983
|
): Component {
|
|
983
|
-
const { expanded } = options;
|
|
984
984
|
const details = result.details;
|
|
985
985
|
|
|
986
986
|
if (!details) {
|
|
@@ -1031,20 +1031,32 @@ export function renderFetchResult(
|
|
|
1031
1031
|
metadataLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const remaining = Math.max(0, contentLines.length - previewLines.length);
|
|
1038
|
-
const contentPreviewLines =
|
|
1039
|
-
previewLines.length > 0 ? previewLines.map(line => uiTheme.fg("dim", line)) : [uiTheme.fg("dim", "(no content)")];
|
|
1040
|
-
if (remaining > 0) {
|
|
1041
|
-
const hint = formatExpandHint(uiTheme, expanded, true);
|
|
1042
|
-
contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
|
|
1043
|
-
}
|
|
1034
|
+
const outputBlock = new CachedOutputBlock();
|
|
1035
|
+
let lastExpanded: boolean | undefined;
|
|
1036
|
+
let contentPreviewLines: string[] | undefined;
|
|
1044
1037
|
|
|
1045
1038
|
return {
|
|
1046
|
-
render: (width: number) =>
|
|
1047
|
-
|
|
1039
|
+
render: (width: number) => {
|
|
1040
|
+
const { expanded } = options;
|
|
1041
|
+
|
|
1042
|
+
if (contentPreviewLines === undefined || lastExpanded !== expanded) {
|
|
1043
|
+
const previewLimit = expanded ? 12 : 3;
|
|
1044
|
+
const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
|
|
1045
|
+
const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, "…"));
|
|
1046
|
+
const remaining = Math.max(0, contentLines.length - previewLines.length);
|
|
1047
|
+
contentPreviewLines =
|
|
1048
|
+
previewLines.length > 0
|
|
1049
|
+
? previewLines.map(line => uiTheme.fg("dim", line))
|
|
1050
|
+
: [uiTheme.fg("dim", "(no content)")];
|
|
1051
|
+
if (remaining > 0) {
|
|
1052
|
+
const hint = formatExpandHint(uiTheme, expanded, true);
|
|
1053
|
+
contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
|
|
1054
|
+
}
|
|
1055
|
+
lastExpanded = expanded;
|
|
1056
|
+
outputBlock.invalidate();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return outputBlock.render(
|
|
1048
1060
|
{
|
|
1049
1061
|
header,
|
|
1050
1062
|
state: truncated ? "warning" : "success",
|
|
@@ -1056,8 +1068,13 @@ export function renderFetchResult(
|
|
|
1056
1068
|
applyBg: false,
|
|
1057
1069
|
},
|
|
1058
1070
|
uiTheme,
|
|
1059
|
-
)
|
|
1060
|
-
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
invalidate: () => {
|
|
1074
|
+
outputBlock.invalidate();
|
|
1075
|
+
contentPreviewLines = undefined;
|
|
1076
|
+
lastExpanded = undefined;
|
|
1077
|
+
},
|
|
1061
1078
|
};
|
|
1062
1079
|
}
|
|
1063
1080
|
|