@oh-my-pi/pi-coding-agent 11.4.0 → 11.5.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 +25 -1
- package/package.json +7 -7
- package/src/patch/shared.ts +2 -2
- package/src/prompts/tools/browser.md +1 -1
- package/src/session/blob-store.ts +18 -20
- package/src/session/session-manager.ts +103 -2
- package/src/tools/bash.ts +5 -3
- package/src/tools/browser.ts +27 -59
- package/src/utils/image-resize.ts +26 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [11.5.0] - 2026-02-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added terminal breadcrumb tracking to remember the last session per terminal, enabling `--continue` to work correctly with concurrent sessions in different terminals
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Changed screenshot format to always use PNG instead of supporting JPEG with quality parameter
|
|
14
|
+
- Changed default extract_readable format from text to markdown
|
|
15
|
+
- Changed screenshot storage to use temporary directory with Snowflake IDs instead of artifacts directory
|
|
16
|
+
- Changed ResizedImage interface to return buffer as Uint8Array with lazy-loaded base64 data getter for improved memory efficiency
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- Removed JPEG quality parameter from screenshot options
|
|
21
|
+
- Removed format selection for screenshots (now PNG only)
|
|
22
|
+
- Removed ability to save screenshots to custom paths or artifacts directory
|
|
23
|
+
|
|
24
|
+
## [11.4.1] - 2026-02-06
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed tab character display in error messages and bash tool output by properly replacing tabs with spaces
|
|
28
|
+
|
|
5
29
|
## [11.4.0] - 2026-02-06
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -3971,4 +3995,4 @@ Initial public release.
|
|
|
3971
3995
|
- Git branch display in footer
|
|
3972
3996
|
- Message queueing during streaming responses
|
|
3973
3997
|
- OAuth integration for Gmail and Google Calendar access
|
|
3974
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
3998
|
+
- HTML export with syntax highlighting and collapsible sections
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.5.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"@mozilla/readability": "0.6.0",
|
|
91
91
|
"@oclif/core": "^4.8.0",
|
|
92
92
|
"@oclif/plugin-autocomplete": "^3.2.40",
|
|
93
|
-
"@oh-my-pi/omp-stats": "11.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.
|
|
93
|
+
"@oh-my-pi/omp-stats": "11.5.0",
|
|
94
|
+
"@oh-my-pi/pi-agent-core": "11.5.0",
|
|
95
|
+
"@oh-my-pi/pi-ai": "11.5.0",
|
|
96
|
+
"@oh-my-pi/pi-natives": "11.5.0",
|
|
97
|
+
"@oh-my-pi/pi-tui": "11.5.0",
|
|
98
|
+
"@oh-my-pi/pi-utils": "11.5.0",
|
|
99
99
|
"@sinclair/typebox": "^0.34.48",
|
|
100
100
|
"ajv": "^8.17.1",
|
|
101
101
|
"chalk": "^5.6.2",
|
package/src/patch/shared.ts
CHANGED
|
@@ -265,13 +265,13 @@ export const editToolRenderer = {
|
|
|
265
265
|
|
|
266
266
|
if (result.isError) {
|
|
267
267
|
if (errorText) {
|
|
268
|
-
text += `\n\n${uiTheme.fg("error", errorText)}`;
|
|
268
|
+
text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
|
|
269
269
|
}
|
|
270
270
|
} else if (result.details?.diff) {
|
|
271
271
|
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
272
272
|
} else if (editDiffPreview) {
|
|
273
273
|
if ("error" in editDiffPreview) {
|
|
274
|
-
text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
|
|
274
|
+
text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
|
|
275
275
|
} else if (editDiffPreview.diff) {
|
|
276
276
|
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
277
277
|
}
|
|
@@ -16,7 +16,7 @@ Use this tool to navigate, click, type, scroll, drag, query DOM content, and cap
|
|
|
16
16
|
- Use `action: "get_text"`, `"get_html"`, or `"get_attribute"` for DOM queries
|
|
17
17
|
- For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`)
|
|
18
18
|
- Use `action: "extract_readable"` to return reader-mode content (title/byline/excerpt/text or markdown)
|
|
19
|
-
- Set `format` to `"
|
|
19
|
+
- Set `format` to `"markdown"` (default) or `"text"`
|
|
20
20
|
- Use `action: "screenshot"` to capture images (optionally with `selector` to capture a single element)
|
|
21
21
|
- Use `action: "close"` to release the browser when done
|
|
22
22
|
</instruction>
|
|
@@ -4,6 +4,12 @@ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
|
|
5
5
|
const BLOB_PREFIX = "blob:sha256:";
|
|
6
6
|
|
|
7
|
+
export interface BlobPutResult {
|
|
8
|
+
hash: string;
|
|
9
|
+
path: string;
|
|
10
|
+
get ref(): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
|
|
9
15
|
*
|
|
@@ -18,22 +24,19 @@ export class BlobStore {
|
|
|
18
24
|
* Write binary data to the blob store.
|
|
19
25
|
* @returns SHA-256 hex hash of the data
|
|
20
26
|
*/
|
|
21
|
-
async put(data: Buffer): Promise<
|
|
22
|
-
const
|
|
23
|
-
hasher.update(data);
|
|
24
|
-
const hash = hasher.digest("hex");
|
|
27
|
+
async put(data: Buffer): Promise<BlobPutResult> {
|
|
28
|
+
const hash = new Bun.CryptoHasher("sha256").update(data).digest("hex");
|
|
25
29
|
const blobPath = path.join(this.dir, hash);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
30
|
+
const result = {
|
|
31
|
+
hash,
|
|
32
|
+
path: blobPath,
|
|
33
|
+
get ref() {
|
|
34
|
+
return `${BLOB_PREFIX}${hash}`;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
34
37
|
|
|
35
38
|
await Bun.write(blobPath, data);
|
|
36
|
-
return
|
|
39
|
+
return result;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/** Read blob by hash, returns Buffer or null if not found. */
|
|
@@ -71,11 +74,6 @@ export function parseBlobRef(data: string): string | null {
|
|
|
71
74
|
return data.slice(BLOB_PREFIX.length);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
/** Create a blob reference string from a SHA-256 hash. */
|
|
75
|
-
export function makeBlobRef(hash: string): string {
|
|
76
|
-
return `${BLOB_PREFIX}${hash}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
77
|
/**
|
|
80
78
|
* Externalize an image's base64 data to the blob store, returning a blob reference.
|
|
81
79
|
* If the data is already a blob reference, returns it unchanged.
|
|
@@ -83,8 +81,8 @@ export function makeBlobRef(hash: string): string {
|
|
|
83
81
|
export async function externalizeImageData(blobStore: BlobStore, base64Data: string): Promise<string> {
|
|
84
82
|
if (isBlobRef(base64Data)) return base64Data;
|
|
85
83
|
const buffer = Buffer.from(base64Data, "base64");
|
|
86
|
-
const
|
|
87
|
-
return
|
|
84
|
+
const { ref } = await blobStore.put(buffer);
|
|
85
|
+
return ref;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
/**
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { getBlobsDir, getAgentDir as getDefaultAgentDir } from "../config";
|
|
6
|
-
import { BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
|
|
7
|
+
import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
|
|
7
8
|
import {
|
|
8
9
|
type BashExecutionMessage,
|
|
9
10
|
type CustomMessage,
|
|
@@ -222,6 +223,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
222
223
|
| "getEntries"
|
|
223
224
|
| "getTree"
|
|
224
225
|
| "getUsageStatistics"
|
|
226
|
+
| "putBlob"
|
|
225
227
|
>;
|
|
226
228
|
|
|
227
229
|
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
|
@@ -468,6 +470,96 @@ function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
|
468
470
|
return sessionDir;
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
// =============================================================================
|
|
474
|
+
// Terminal breadcrumbs: maps terminal (TTY) -> last session file for --continue
|
|
475
|
+
// =============================================================================
|
|
476
|
+
|
|
477
|
+
const TERMINAL_SESSIONS_DIR = "terminal-sessions";
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get a stable identifier for the current terminal.
|
|
481
|
+
* Uses the TTY device path (e.g., /dev/pts/3), falling back to environment
|
|
482
|
+
* variables for terminal multiplexers or terminal emulators.
|
|
483
|
+
* Returns null if no terminal can be identified (e.g., piped input).
|
|
484
|
+
*/
|
|
485
|
+
function getTerminalId(): string | null {
|
|
486
|
+
// TTY device path — most reliable, unique per terminal tab
|
|
487
|
+
if (process.stdin.isTTY) {
|
|
488
|
+
try {
|
|
489
|
+
// On Linux/macOS, /proc/self/fd/0 -> /dev/pts/N
|
|
490
|
+
const ttyPath = fs.readlinkSync("/proc/self/fd/0");
|
|
491
|
+
if (ttyPath.startsWith("/dev/")) {
|
|
492
|
+
return ttyPath.slice(5).replace(/\//g, "-"); // /dev/pts/3 -> pts-3
|
|
493
|
+
}
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Fallback to terminal-specific env vars
|
|
498
|
+
const kittyId = process.env.KITTY_WINDOW_ID;
|
|
499
|
+
if (kittyId) return `kitty-${kittyId}`;
|
|
500
|
+
|
|
501
|
+
const tmuxPane = process.env.TMUX_PANE;
|
|
502
|
+
if (tmuxPane) return `tmux-${tmuxPane}`;
|
|
503
|
+
|
|
504
|
+
const terminalSessionId = process.env.TERM_SESSION_ID; // macOS Terminal.app
|
|
505
|
+
if (terminalSessionId) return `apple-${terminalSessionId}`;
|
|
506
|
+
|
|
507
|
+
const wtSession = process.env.WT_SESSION; // Windows Terminal
|
|
508
|
+
if (wtSession) return `wt-${wtSession}`;
|
|
509
|
+
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Write a breadcrumb linking the current terminal to a session file.
|
|
515
|
+
* The breadcrumb contains the cwd and session path so --continue can
|
|
516
|
+
* find "this terminal's last session" even when running concurrent instances.
|
|
517
|
+
*/
|
|
518
|
+
function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
519
|
+
const terminalId = getTerminalId();
|
|
520
|
+
if (!terminalId) return;
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
|
|
524
|
+
const breadcrumbFile = path.join(breadcrumbDir, terminalId);
|
|
525
|
+
const content = `${cwd}\n${sessionFile}\n`;
|
|
526
|
+
// Bun.write auto-creates parent dirs
|
|
527
|
+
void Bun.write(breadcrumbFile, content);
|
|
528
|
+
} catch {
|
|
529
|
+
// Best-effort — don't break session creation if breadcrumb fails
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Read the terminal breadcrumb for the current terminal, scoped to a cwd.
|
|
535
|
+
* Returns the session file path if it exists and matches the cwd, null otherwise.
|
|
536
|
+
*/
|
|
537
|
+
async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
538
|
+
const terminalId = getTerminalId();
|
|
539
|
+
if (!terminalId) return null;
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const breadcrumbFile = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR, terminalId);
|
|
543
|
+
const content = await Bun.file(breadcrumbFile).text();
|
|
544
|
+
const lines = content.trim().split("\n");
|
|
545
|
+
if (lines.length < 2) return null;
|
|
546
|
+
|
|
547
|
+
const breadcrumbCwd = lines[0];
|
|
548
|
+
const sessionFile = lines[1];
|
|
549
|
+
|
|
550
|
+
// Only return if cwd matches (user might have cd'd)
|
|
551
|
+
if (path.resolve(breadcrumbCwd) !== path.resolve(cwd)) return null;
|
|
552
|
+
|
|
553
|
+
// Verify the session file still exists
|
|
554
|
+
const stat = fs.statSync(sessionFile, { throwIfNoEntry: false });
|
|
555
|
+
if (stat?.isFile()) return sessionFile;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
|
|
558
|
+
// Breadcrumb doesn't exist or is corrupt — fall through
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
471
563
|
/** Exported for testing */
|
|
472
564
|
export async function loadEntriesFromFile(
|
|
473
565
|
filePath: string,
|
|
@@ -1013,6 +1105,11 @@ export class SessionManager {
|
|
|
1013
1105
|
// Note: call _initSession() or _initSessionFile() after construction
|
|
1014
1106
|
}
|
|
1015
1107
|
|
|
1108
|
+
/** Puts a binary blob into the blob store and returns the blob reference */
|
|
1109
|
+
async putBlob(data: Buffer): Promise<BlobPutResult> {
|
|
1110
|
+
return this.blobStore.put(data);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1016
1113
|
/** Initialize with a specific session file (used by factory methods) */
|
|
1017
1114
|
private async _initSessionFile(sessionFile: string): Promise<void> {
|
|
1018
1115
|
await this.setSessionFile(sessionFile);
|
|
@@ -1029,6 +1126,7 @@ export class SessionManager {
|
|
|
1029
1126
|
this.persistError = undefined;
|
|
1030
1127
|
this.persistErrorReported = false;
|
|
1031
1128
|
this.sessionFile = path.resolve(sessionFile);
|
|
1129
|
+
writeTerminalBreadcrumb(this.cwd, this.sessionFile);
|
|
1032
1130
|
this.fileEntries = await loadEntriesFromFile(this.sessionFile, this.storage);
|
|
1033
1131
|
if (this.fileEntries.length > 0) {
|
|
1034
1132
|
const header = this.fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
@@ -1134,6 +1232,7 @@ export class SessionManager {
|
|
|
1134
1232
|
if (this.persist) {
|
|
1135
1233
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1136
1234
|
this.sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
1235
|
+
writeTerminalBreadcrumb(this.cwd, this.sessionFile);
|
|
1137
1236
|
}
|
|
1138
1237
|
return this.sessionFile;
|
|
1139
1238
|
}
|
|
@@ -1992,7 +2091,9 @@ export class SessionManager {
|
|
|
1992
2091
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1993
2092
|
): Promise<SessionManager> {
|
|
1994
2093
|
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
1995
|
-
|
|
2094
|
+
// Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
|
|
2095
|
+
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
2096
|
+
const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
|
|
1996
2097
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1997
2098
|
if (mostRecent) {
|
|
1998
2099
|
await manager._initSessionFile(mostRecent);
|
package/src/tools/bash.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { applyHeadTail, normalizeBashCommand } from "./bash-normalize";
|
|
|
17
17
|
import type { OutputMeta } from "./output-meta";
|
|
18
18
|
import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
|
|
19
19
|
import { resolveToCwd } from "./path-utils";
|
|
20
|
-
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
20
|
+
import { formatBytes, replaceTabs, wrapBrackets } from "./render-utils";
|
|
21
21
|
import { ToolError } from "./tool-errors";
|
|
22
22
|
import { toolResult } from "./tool-result";
|
|
23
23
|
import { DEFAULT_MAX_BYTES } from "./truncate";
|
|
@@ -271,11 +271,13 @@ export const bashToolRenderer = {
|
|
|
271
271
|
const hasOutput = displayOutput.trim().length > 0;
|
|
272
272
|
if (hasOutput) {
|
|
273
273
|
if (expanded) {
|
|
274
|
-
outputLines.push(
|
|
274
|
+
outputLines.push(
|
|
275
|
+
...displayOutput.split("\n").map(line => uiTheme.fg("toolOutput", replaceTabs(line))),
|
|
276
|
+
);
|
|
275
277
|
} else {
|
|
276
278
|
const styledOutput = displayOutput
|
|
277
279
|
.split("\n")
|
|
278
|
-
.map(line => uiTheme.fg("toolOutput", line))
|
|
280
|
+
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
|
279
281
|
.join("\n");
|
|
280
282
|
const textContent = styledOutput;
|
|
281
283
|
const result = truncateToVisualLines(textContent, previewLines, width);
|
package/src/tools/browser.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { Readability } from "@mozilla/readability";
|
|
3
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
7
8
|
import { JSDOM, VirtualConsole } from "jsdom";
|
|
8
9
|
import type { Browser, CDPSession, ElementHandle, KeyInput, Page, SerializedAXNode } from "puppeteer";
|
|
@@ -13,7 +14,6 @@ import type { ToolSession } from "../sdk";
|
|
|
13
14
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
14
15
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
15
16
|
import type { OutputMeta } from "./output-meta";
|
|
16
|
-
import { resolveToCwd } from "./path-utils";
|
|
17
17
|
import stealthTamperingScript from "./puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
18
18
|
import stealthActivityScript from "./puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
19
19
|
import stealthHairlineScript from "./puppeteer/02_stealth_hairline.txt" with { type: "text" };
|
|
@@ -319,11 +319,10 @@ const browserSchema = Type.Object({
|
|
|
319
319
|
),
|
|
320
320
|
full_page: Type.Optional(Type.Boolean({ description: "Capture full page screenshot (screenshot)" })),
|
|
321
321
|
format: Type.Optional(
|
|
322
|
-
StringEnum(["
|
|
323
|
-
description: "Output format
|
|
322
|
+
StringEnum(["text", "markdown"], {
|
|
323
|
+
description: "Output format for extract_readable (text/markdown)",
|
|
324
324
|
}),
|
|
325
325
|
),
|
|
326
|
-
quality: Type.Optional(Type.Number({ description: "JPEG quality 0-100 (screenshot)" })),
|
|
327
326
|
path: Type.Optional(Type.String({ description: "Optional path to save screenshot (relative to cwd)" })),
|
|
328
327
|
viewport: Type.Optional(
|
|
329
328
|
Type.Object({
|
|
@@ -407,11 +406,6 @@ function clampTimeout(timeoutSeconds?: number): number {
|
|
|
407
406
|
return Math.min(Math.max(timeoutSeconds, 1), MAX_TIMEOUT_SECONDS);
|
|
408
407
|
}
|
|
409
408
|
|
|
410
|
-
function clampQuality(quality?: number): number | undefined {
|
|
411
|
-
if (quality === undefined) return undefined;
|
|
412
|
-
return Math.min(Math.max(quality, 0), 100);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
409
|
function ensureParam<T>(value: T | undefined, name: string, action: string): T {
|
|
416
410
|
if (value === undefined || value === null || value === "") {
|
|
417
411
|
throw new ToolError(`Missing required parameter '${name}' for action '${action}'.`);
|
|
@@ -419,10 +413,6 @@ function ensureParam<T>(value: T | undefined, name: string, action: string): T {
|
|
|
419
413
|
return value;
|
|
420
414
|
}
|
|
421
415
|
|
|
422
|
-
function resolveArtifactsDir(session: ToolSession): string | null {
|
|
423
|
-
return session.getArtifactsDir?.() ?? session.getSessionFile()?.slice(0, -6) ?? null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
416
|
function formatEvaluateResult(value: unknown): string {
|
|
427
417
|
if (typeof value === "string") return value;
|
|
428
418
|
if (value === undefined) return "undefined";
|
|
@@ -895,7 +885,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
895
885
|
params: BrowserParams,
|
|
896
886
|
signal?: AbortSignal,
|
|
897
887
|
_onUpdate?: AgentToolUpdateCallback<BrowserToolDetails>,
|
|
898
|
-
|
|
888
|
+
_ctx?: AgentToolContext,
|
|
899
889
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
900
890
|
try {
|
|
901
891
|
throwIfAborted(signal);
|
|
@@ -1237,10 +1227,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1237
1227
|
}
|
|
1238
1228
|
case "extract_readable": {
|
|
1239
1229
|
const page = await this.ensurePage(params);
|
|
1240
|
-
const format = params.format ?? "
|
|
1241
|
-
if (format !== "text" && format !== "markdown") {
|
|
1242
|
-
throw new ToolError("extract_readable format must be text or markdown");
|
|
1243
|
-
}
|
|
1230
|
+
const format = params.format ?? "markdown";
|
|
1244
1231
|
const html = (await untilAborted(signal, () => page.content())) as string;
|
|
1245
1232
|
const url = page.url();
|
|
1246
1233
|
const virtualConsole = new VirtualConsole();
|
|
@@ -1277,13 +1264,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1277
1264
|
}
|
|
1278
1265
|
case "screenshot": {
|
|
1279
1266
|
const page = await this.ensurePage(params);
|
|
1280
|
-
const format = params.format ?? "png";
|
|
1281
|
-
if (format !== "png" && format !== "jpeg") {
|
|
1282
|
-
throw new ToolError("Screenshot format must be png or jpeg");
|
|
1283
|
-
}
|
|
1284
|
-
const imageFormat = format;
|
|
1285
1267
|
const fullPage = params.selector ? false : (params.full_page ?? false);
|
|
1286
|
-
const quality = imageFormat === "jpeg" ? clampQuality(params.quality ?? 80) : undefined;
|
|
1287
1268
|
let buffer: Buffer;
|
|
1288
1269
|
|
|
1289
1270
|
if (params.selector) {
|
|
@@ -1292,47 +1273,34 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1292
1273
|
if (!handle) {
|
|
1293
1274
|
throw new ToolError("Screenshot selector did not resolve to an element");
|
|
1294
1275
|
}
|
|
1295
|
-
buffer = (await untilAborted(signal, () =>
|
|
1296
|
-
handle.screenshot({ type: imageFormat, quality }),
|
|
1297
|
-
)) as Buffer;
|
|
1276
|
+
buffer = (await untilAborted(signal, () => handle.screenshot({ type: "png" }))) as Buffer;
|
|
1298
1277
|
await handle.dispose();
|
|
1299
1278
|
details.selector = params.selector;
|
|
1300
1279
|
} else {
|
|
1301
|
-
buffer = (await untilAborted(signal, () =>
|
|
1302
|
-
page.screenshot({ type: imageFormat, quality, fullPage }),
|
|
1303
|
-
)) as Buffer;
|
|
1280
|
+
buffer = (await untilAborted(signal, () => page.screenshot({ type: "png", fullPage }))) as Buffer;
|
|
1304
1281
|
}
|
|
1305
1282
|
|
|
1306
|
-
const mimeType = imageFormat === "png" ? "image/png" : "image/jpeg";
|
|
1307
|
-
const base64 = buffer.toBase64();
|
|
1308
|
-
let savedPath: string | undefined;
|
|
1309
|
-
if (params.path) {
|
|
1310
|
-
const resolved = resolveToCwd(params.path, this.session.cwd);
|
|
1311
|
-
const ext = path.extname(resolved);
|
|
1312
|
-
savedPath = ext ? resolved : `${resolved}.${imageFormat}`;
|
|
1313
|
-
await Bun.write(savedPath, buffer);
|
|
1314
|
-
} else {
|
|
1315
|
-
const artifactsDir = resolveArtifactsDir(this.session);
|
|
1316
|
-
if (artifactsDir) {
|
|
1317
|
-
savedPath = path.join(artifactsDir, `puppeteer-${Date.now()}.${imageFormat}`);
|
|
1318
|
-
await Bun.write(savedPath, buffer);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
details.screenshotPath = savedPath;
|
|
1322
|
-
details.mimeType = mimeType;
|
|
1323
|
-
details.bytes = buffer.length;
|
|
1324
|
-
|
|
1325
1283
|
// Compress for API content (same as pasted images)
|
|
1326
|
-
|
|
1284
|
+
// NOTE: screenshots can be deceptively large (especially PNG) even at modest resolutions,
|
|
1285
|
+
// and tool results are immediately embedded in the next LLM request.
|
|
1286
|
+
// Use a tighter budget than the global per-image limit to avoid 413 request_too_large.
|
|
1287
|
+
const resized = await resizeImage(
|
|
1288
|
+
{ type: "image", data: buffer.toBase64(), mimeType: "image/png" },
|
|
1289
|
+
{ maxBytes: 0.75 * 1024 * 1024 },
|
|
1290
|
+
);
|
|
1327
1291
|
const dimensionNote = formatDimensionNote(resized);
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1292
|
+
const tempFile = path.join(tmpdir(), `omp-sshots-${Snowflake.next()}.png`);
|
|
1293
|
+
await Bun.write(tempFile, resized.buffer);
|
|
1294
|
+
details.screenshotPath = tempFile;
|
|
1295
|
+
details.mimeType = resized.mimeType;
|
|
1296
|
+
details.bytes = resized.buffer.length;
|
|
1297
|
+
|
|
1298
|
+
// Show both raw bytes (saved to disk) and compressed bytes (sent to model).
|
|
1299
|
+
const lines = [
|
|
1300
|
+
"Screenshot captured",
|
|
1301
|
+
`Format: ${resized.mimeType} (${(resized.buffer.length / 1024).toFixed(2)} KB)`,
|
|
1302
|
+
`Dimensions: ${resized.width}x${resized.height}`,
|
|
1303
|
+
];
|
|
1336
1304
|
if (dimensionNote) {
|
|
1337
1305
|
lines.push(dimensionNote);
|
|
1338
1306
|
}
|
|
@@ -9,13 +9,14 @@ export interface ImageResizeOptions {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface ResizedImage {
|
|
12
|
-
|
|
12
|
+
buffer: Uint8Array;
|
|
13
13
|
mimeType: string;
|
|
14
14
|
originalWidth: number;
|
|
15
15
|
originalHeight: number;
|
|
16
16
|
width: number;
|
|
17
17
|
height: number;
|
|
18
18
|
wasResized: boolean;
|
|
19
|
+
get data(): string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
@@ -64,13 +65,16 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
64
65
|
const originalSize = inputBuffer.length;
|
|
65
66
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
66
67
|
return {
|
|
67
|
-
|
|
68
|
+
buffer: inputBuffer,
|
|
68
69
|
mimeType: img.mimeType ?? `image/${format}`,
|
|
69
70
|
originalWidth,
|
|
70
71
|
originalHeight,
|
|
71
72
|
width: originalWidth,
|
|
72
73
|
height: originalHeight,
|
|
73
74
|
wasResized: false,
|
|
75
|
+
get data() {
|
|
76
|
+
return img.data;
|
|
77
|
+
},
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -119,13 +123,16 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
119
123
|
|
|
120
124
|
if (best.buffer.length <= opts.maxBytes) {
|
|
121
125
|
return {
|
|
122
|
-
|
|
126
|
+
buffer: best.buffer,
|
|
123
127
|
mimeType: best.mimeType,
|
|
124
128
|
originalWidth,
|
|
125
129
|
originalHeight,
|
|
126
130
|
width: finalWidth,
|
|
127
131
|
height: finalHeight,
|
|
128
132
|
wasResized: true,
|
|
133
|
+
get data() {
|
|
134
|
+
return best.buffer.toBase64();
|
|
135
|
+
},
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
138
|
|
|
@@ -135,13 +142,16 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
135
142
|
|
|
136
143
|
if (best.buffer.length <= opts.maxBytes) {
|
|
137
144
|
return {
|
|
138
|
-
|
|
145
|
+
buffer: best.buffer,
|
|
139
146
|
mimeType: best.mimeType,
|
|
140
147
|
originalWidth,
|
|
141
148
|
originalHeight,
|
|
142
149
|
width: finalWidth,
|
|
143
150
|
height: finalHeight,
|
|
144
151
|
wasResized: true,
|
|
152
|
+
get data() {
|
|
153
|
+
return best.buffer.toBase64();
|
|
154
|
+
},
|
|
145
155
|
};
|
|
146
156
|
}
|
|
147
157
|
}
|
|
@@ -160,13 +170,16 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
160
170
|
|
|
161
171
|
if (best.buffer.length <= opts.maxBytes) {
|
|
162
172
|
return {
|
|
163
|
-
|
|
173
|
+
buffer: best.buffer,
|
|
164
174
|
mimeType: best.mimeType,
|
|
165
175
|
originalWidth,
|
|
166
176
|
originalHeight,
|
|
167
177
|
width: finalWidth,
|
|
168
178
|
height: finalHeight,
|
|
169
179
|
wasResized: true,
|
|
180
|
+
get data() {
|
|
181
|
+
return best.buffer.toBase64();
|
|
182
|
+
},
|
|
170
183
|
};
|
|
171
184
|
}
|
|
172
185
|
}
|
|
@@ -174,24 +187,30 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
174
187
|
|
|
175
188
|
// Last resort: return smallest version we produced
|
|
176
189
|
return {
|
|
177
|
-
|
|
190
|
+
buffer: best.buffer,
|
|
178
191
|
mimeType: best.mimeType,
|
|
179
192
|
originalWidth,
|
|
180
193
|
originalHeight,
|
|
181
194
|
width: finalWidth,
|
|
182
195
|
height: finalHeight,
|
|
183
196
|
wasResized: true,
|
|
197
|
+
get data() {
|
|
198
|
+
return best.buffer.toBase64();
|
|
199
|
+
},
|
|
184
200
|
};
|
|
185
201
|
} catch {
|
|
186
202
|
// Failed to load image
|
|
187
203
|
return {
|
|
188
|
-
|
|
204
|
+
buffer: inputBuffer,
|
|
189
205
|
mimeType: img.mimeType,
|
|
190
206
|
originalWidth: 0,
|
|
191
207
|
originalHeight: 0,
|
|
192
208
|
width: 0,
|
|
193
209
|
height: 0,
|
|
194
210
|
wasResized: false,
|
|
211
|
+
get data() {
|
|
212
|
+
return img.data;
|
|
213
|
+
},
|
|
195
214
|
};
|
|
196
215
|
}
|
|
197
216
|
}
|