@oh-my-pi/pi-coding-agent 14.0.4 → 14.0.5
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 +42 -0
- package/package.json +11 -8
- package/src/config/model-registry.ts +3 -2
- package/src/config/model-resolver.ts +33 -25
- package/src/config/settings.ts +9 -2
- package/src/dap/session.ts +31 -39
- package/src/debug/log-formatting.ts +2 -2
- package/src/edit/modes/chunk.ts +8 -3
- package/src/lsp/client.ts +4 -2
- package/src/lsp/index.ts +4 -9
- package/src/lsp/utils.ts +26 -0
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/controllers/event-controller.ts +438 -426
- package/src/modes/theme/mermaid-cache.ts +5 -7
- package/src/priority.json +8 -0
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/tools/chunk-edit.md +39 -40
- package/src/prompts/tools/read-chunk.md +4 -1
- package/src/session/agent-session.ts +10 -0
- package/src/session/compaction/compaction.ts +1 -1
- package/src/tools/browser.ts +84 -21
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find.ts +40 -94
- package/src/tools/gemini-image.ts +1 -0
- package/src/tools/render-utils.ts +1 -1
- package/src/utils/image-resize.ts +73 -37
- package/src/web/scrapers/types.ts +50 -32
- package/src/web/search/providers/codex.ts +21 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
const cache = new Map<bigint, string>();
|
|
4
|
-
const failed = new Set<bigint>();
|
|
3
|
+
const cache = new Map<bigint | number, string | null>();
|
|
5
4
|
|
|
6
5
|
let onRenderNeeded: (() => void) | null = null;
|
|
7
6
|
|
|
@@ -16,7 +15,7 @@ export function setMermaidRenderCallback(callback: (() => void) | null): void {
|
|
|
16
15
|
* Get a pre-rendered mermaid ASCII diagram by hash.
|
|
17
16
|
* Returns null if not cached or rendering failed.
|
|
18
17
|
*/
|
|
19
|
-
export function getMermaidAscii(hash: bigint): string | null {
|
|
18
|
+
export function getMermaidAscii(hash: bigint | number): string | null {
|
|
20
19
|
return cache.get(hash) ?? null;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -31,14 +30,14 @@ export function prerenderMermaid(markdown: string): void {
|
|
|
31
30
|
let hasNew = false;
|
|
32
31
|
|
|
33
32
|
for (const { source, hash } of blocks) {
|
|
34
|
-
if (cache.has(hash)
|
|
33
|
+
if (cache.has(hash)) continue;
|
|
35
34
|
|
|
36
35
|
const ascii = renderMermaidAsciiSafe(source);
|
|
37
36
|
if (ascii) {
|
|
38
37
|
cache.set(hash, ascii);
|
|
39
38
|
hasNew = true;
|
|
40
39
|
} else {
|
|
41
|
-
|
|
40
|
+
cache.set(hash, null);
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -58,7 +57,7 @@ export function prerenderMermaid(markdown: string): void {
|
|
|
58
57
|
*/
|
|
59
58
|
export function hasPendingMermaid(markdown: string): boolean {
|
|
60
59
|
const blocks = extractMermaidBlocks(markdown);
|
|
61
|
-
return blocks.some(({ hash }) => !cache.has(hash)
|
|
60
|
+
return blocks.some(({ hash }) => !cache.has(hash));
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
/**
|
|
@@ -66,5 +65,4 @@ export function hasPendingMermaid(markdown: string): boolean {
|
|
|
66
65
|
*/
|
|
67
66
|
export function clearMermaidCache(): void {
|
|
68
67
|
cache.clear();
|
|
69
|
-
failed.clear();
|
|
70
68
|
}
|
package/src/priority.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: designer
|
|
3
3
|
description: UI/UX specialist for design implementation, review, visual refinement
|
|
4
|
-
|
|
5
|
-
model: google-gemini-cli/gemini-3.1-pro, google-gemini-cli/gemini-3-pro, gemini-3.1-pro, gemini-3-1-pro, gemini-3-pro, gemini-3, pi/default
|
|
4
|
+
model: pi/designer
|
|
6
5
|
---
|
|
7
6
|
|
|
8
7
|
You are an expert UI/UX designer implementing and reviewing UI designs.
|
|
@@ -3,67 +3,66 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
|
|
|
3
3
|
<rules>
|
|
4
4
|
- **MUST** `read` first. Never invent chunk paths or CRCs. Copy them from the latest `read` output or edit response.
|
|
5
5
|
- `sel` format:
|
|
6
|
-
- insertions: `chunk` or `chunk
|
|
7
|
-
- replacements: `chunk#CRC` or `chunk#CRC
|
|
8
|
-
- Without a
|
|
6
|
+
- insertions: `chunk`, `chunk~`, or `chunk^`
|
|
7
|
+
- replacements: `chunk#CRC`, `chunk#CRC~`, or `chunk#CRC^`
|
|
8
|
+
- Without a suffix it defaults to the entire chunk including leading trivia. `~` targets the body, `^` targets the head.
|
|
9
9
|
- If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
|
|
10
10
|
{{#if chunkAutoIndent}}
|
|
11
|
-
- Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace
|
|
11
|
+
- Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0:
|
|
12
12
|
```
|
|
13
13
|
content: "if (x) {\n\treturn true;\n}"
|
|
14
14
|
```
|
|
15
15
|
The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
|
|
16
16
|
{{else}}
|
|
17
17
|
- Match the file's literal tabs/spaces in `content`. Do not convert indentation to canonical `\t`.
|
|
18
|
-
- Write content at indent-level 0 relative to the target region. For example, to replace
|
|
18
|
+
- Write content at indent-level 0 relative to the target region. For example, to replace `~` of a method, write:
|
|
19
19
|
```
|
|
20
20
|
content: "if (x) {\n return true;\n}"
|
|
21
21
|
```
|
|
22
22
|
The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
|
|
23
23
|
{{/if}}
|
|
24
|
-
-
|
|
24
|
+
- Region suffixes only apply to container chunks (classes, functions, impl blocks, sections). On leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` silently fall back to whole-chunk replacement — prefer the unsuffixed form and always supply the complete replacement (condition + body, not just the body) to avoid dropping structural parts.
|
|
25
25
|
- `replace` requires the current CRC. Insertions do not.
|
|
26
|
-
- **CRCs change after every edit.**
|
|
26
|
+
- **CRCs change after every edit.** The edit response always carries the new CRCs — use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse a CRC from before the latest edit.
|
|
27
27
|
</rules>
|
|
28
28
|
|
|
29
29
|
<critical>
|
|
30
30
|
You **MUST** use the narrowest region that covers your change. Replacing without a region replaces the **entire chunk including leading comments, decorators, and attributes** — omitting them from `content` deletes them.
|
|
31
31
|
|
|
32
|
-
**`replace` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before replacing
|
|
32
|
+
**`replace` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before replacing `~` on any chunk, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent.
|
|
33
33
|
|
|
34
|
-
**Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). Replacing
|
|
34
|
+
**Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). Replacing `~` on a group chunk replaces **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not replace the parent group.
|
|
35
35
|
</critical>
|
|
36
36
|
|
|
37
37
|
<regions>
|
|
38
|
+
Given a chunk like:
|
|
38
39
|
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
/// doc comment <-- leading trivia
|
|
41
|
+
#[attr] <-- leading trivia
|
|
42
|
+
fn foo(x: i32) { <-- signature + opening delimiter
|
|
43
|
+
body(); <-- body
|
|
44
|
+
} <-- closing delimiter
|
|
44
45
|
```
|
|
45
|
-
- `@body` — the interior only. **Use for most edits.**
|
|
46
|
-
- `@head` — leading trivia + signature + opening delimiter.
|
|
47
|
-
- `@tail` — the closing delimiter.
|
|
48
|
-
- `@decl` — everything except leading trivia (signature + body + closing delimiter).
|
|
49
|
-
- *(no region)* — the entire chunk including leading trivia. Same as `@head` + `@body` + `@tail`.
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
Append `~` to target the body, `^` to target the head (trivia + signature), or nothing for the whole chunk:
|
|
48
|
+
- `fn_foo#CRC~` — body only. **Use for most edits.** On leaf chunks, falls back to whole chunk.
|
|
49
|
+
- `fn_foo#CRC^` — head (decorators, attributes, doc comments, signature, opening delimiter).
|
|
50
|
+
- `fn_foo#CRC` — entire chunk including leading trivia.
|
|
51
|
+
- `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- `
|
|
56
|
-
- `class_Foo` + `append` → adds after the entire class (after `}`)
|
|
53
|
+
**Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#CRC` in the `?` listing) rather than as part of the function's `^`. If you need to rewrite a decorator, check the `?` listing for a sibling `chunk#CRC` directly above your target.
|
|
54
|
+
|
|
55
|
+
**Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, fenced code blocks, frontmatter), `^` and `~` fall back to the whole chunk. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers) in your `content` — omitting them deletes them. For markdown sections (`sect_*`), always use unsuffixed whole-chunk replace — `^` and `~` on section containers also fall back to whole-chunk replace. When editing fenced code blocks in markdown, use the exact whitespace from the file (read with `raw` first) — the tool preserves literal indentation inside fenced blocks, but any content you supply is written verbatim. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
|
|
57
56
|
</regions>
|
|
58
57
|
|
|
59
58
|
<ops>
|
|
60
59
|
|op|sel|effect|
|
|
61
60
|
|---|---|---|
|
|
62
|
-
|`replace`|`chunk#CRC
|
|
63
|
-
|`before`|`chunk
|
|
64
|
-
|`after`|`chunk
|
|
65
|
-
|`prepend`|`chunk
|
|
66
|
-
|`append`|`chunk
|
|
61
|
+
|`replace`|`chunk#CRC`, `chunk#CRC~`, or `chunk#CRC^`|rewrite the addressed region|
|
|
62
|
+
|`before`|`chunk`, `chunk~`, or `chunk^`|insert before the region span|
|
|
63
|
+
|`after`|`chunk`, `chunk~`, or `chunk^`|insert after the region span|
|
|
64
|
+
|`prepend`|`chunk`, `chunk~`, or `chunk^`|insert at the start inside the region|
|
|
65
|
+
|`append`|`chunk`, `chunk~`, or `chunk^`|insert at the end inside the region|
|
|
67
66
|
</ops>
|
|
68
67
|
|
|
69
68
|
<examples>
|
|
@@ -137,9 +136,9 @@ function makeCounter(start: number): Counter {
|
|
|
137
136
|
}
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
**Replace a method body** (
|
|
139
|
+
**Replace a method body** (`~`):
|
|
141
140
|
```
|
|
142
|
-
{ "sel": "class_Counter.fn_increment#NQWY
|
|
141
|
+
{ "sel": "class_Counter.fn_increment#NQWY~", "op": "replace", "content": "this.value += 1;\nconsole.log('incremented to', this.value);\n" }
|
|
143
142
|
```
|
|
144
143
|
Result — only the body changes, signature and braces are kept:
|
|
145
144
|
```
|
|
@@ -149,9 +148,9 @@ Result — only the body changes, signature and braces are kept:
|
|
|
149
148
|
}
|
|
150
149
|
```
|
|
151
150
|
|
|
152
|
-
**Replace a function header** (
|
|
151
|
+
**Replace a function header** (`^` — signature and doc comment):
|
|
153
152
|
```
|
|
154
|
-
{ "sel": "fn_createCounter#PQQY
|
|
153
|
+
{ "sel": "fn_createCounter#PQQY^", "op": "replace", "content": "/** Creates a counter with the given start value. */\nfunction createCounter(initial: number, label?: string): Counter {\n" }
|
|
155
154
|
```
|
|
156
155
|
Result — adds a doc comment and updates the signature, body untouched:
|
|
157
156
|
```
|
|
@@ -197,9 +196,9 @@ function isActive(s: Status): boolean {
|
|
|
197
196
|
function createCounter(initial: number): Counter {
|
|
198
197
|
```
|
|
199
198
|
|
|
200
|
-
**Prepend inside a container** (
|
|
199
|
+
**Prepend inside a container** (`~` + `prepend`):
|
|
201
200
|
```
|
|
202
|
-
{ "sel": "class_Counter
|
|
201
|
+
{ "sel": "class_Counter~", "op": "prepend", "content": "label: string = 'default';\n\n" }
|
|
203
202
|
```
|
|
204
203
|
Result — a new field is added at the top of the class body, before existing members:
|
|
205
204
|
```
|
|
@@ -209,12 +208,12 @@ class Counter {
|
|
|
209
208
|
value: number = 0;
|
|
210
209
|
```
|
|
211
210
|
|
|
212
|
-
**Append inside a container** (
|
|
211
|
+
**Append inside a container** (`~` + `append`):
|
|
213
212
|
~~~json
|
|
214
213
|
{{#if chunkAutoIndent}}
|
|
215
|
-
{ "sel": "class_Counter
|
|
214
|
+
{ "sel": "class_Counter~", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
|
|
216
215
|
{{else}}
|
|
217
|
-
{ "sel": "class_Counter
|
|
216
|
+
{ "sel": "class_Counter~", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
|
|
218
217
|
{{/if}}
|
|
219
218
|
~~~
|
|
220
219
|
Result — a new method is added at the end of the class body, before the closing `}`:
|
|
@@ -241,8 +240,8 @@ Result — the method is removed from the class.
|
|
|
241
240
|
- Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
|
|
242
241
|
{{/if}}
|
|
243
242
|
- Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
|
|
244
|
-
- For
|
|
245
|
-
- For
|
|
243
|
+
- For `~` of a function: write at column 0, and use `\t` for *relative* nesting. Flat body: `"return x;\n"`. Nested body: `"if (cond) {\n\treturn x;\n}\n"` — the `if` is at column 0, the `return` is one tab in, and the tool adds the method's base indent to both.
|
|
244
|
+
- For `^`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
|
|
246
245
|
{{#if chunkAutoIndent}}
|
|
247
246
|
- For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
|
|
248
247
|
{{else}}
|
|
@@ -2,13 +2,15 @@ Reads files using syntax-aware chunks.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- `path` — file path or URL; may include `:selector` suffix
|
|
5
|
-
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD
|
|
5
|
+
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD~`, `?`, `L50`, `L50-L120`, or `raw`
|
|
6
6
|
- `timeout` — seconds, for URLs only
|
|
7
7
|
|
|
8
8
|
Each opening anchor `[< full.chunk.path#CCCC ]` in the default output identifies a chunk. Use `full.chunk.path#CCCC` as-is to read truncated chunks.
|
|
9
9
|
If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with CRCs.
|
|
10
10
|
Line numbers in the gutter are absolute file line numbers.
|
|
11
11
|
|
|
12
|
+
`L20` (single line, no explicit end) is shorthand for `L20` to end-of-file. Use `L20-L20` for a one-line window.
|
|
13
|
+
|
|
12
14
|
{{#if chunkAutoIndent}}
|
|
13
15
|
Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
|
|
14
16
|
{{else}}
|
|
@@ -20,4 +22,5 @@ Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
|
|
|
20
22
|
|
|
21
23
|
<critical>
|
|
22
24
|
- **MUST** `read` before editing — never invent chunk names or CRCs.
|
|
25
|
+
- Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers.
|
|
23
26
|
</critical>
|
|
@@ -997,6 +997,16 @@ export class AgentSession {
|
|
|
997
997
|
this.#lastAssistantMessage = undefined;
|
|
998
998
|
if (!msg) return;
|
|
999
999
|
|
|
1000
|
+
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
1001
|
+
// aren't reused on the next request
|
|
1002
|
+
if (
|
|
1003
|
+
msg.stopReason === "error" &&
|
|
1004
|
+
msg.provider === "github-copilot" &&
|
|
1005
|
+
msg.errorMessage?.includes("GitHub Copilot authentication failed")
|
|
1006
|
+
) {
|
|
1007
|
+
await this.#modelRegistry.authStorage.remove("github-copilot");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1000
1010
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
1001
1011
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
1002
1012
|
return;
|
|
@@ -761,7 +761,7 @@ function buildOpenAiNativeHistory(
|
|
|
761
761
|
if (!msgId) {
|
|
762
762
|
msgId = `msg_${msgIndex}`;
|
|
763
763
|
} else if (msgId.length > 64) {
|
|
764
|
-
msgId = `msg_${Bun.hash
|
|
764
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
765
765
|
}
|
|
766
766
|
input.push({
|
|
767
767
|
type: "message",
|
package/src/tools/browser.ts
CHANGED
|
@@ -501,6 +501,83 @@ export interface ReadableResult {
|
|
|
501
501
|
markdown?: string;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
type ReadableFormat = "text" | "markdown";
|
|
505
|
+
|
|
506
|
+
/** Trim to non-empty string or undefined. */
|
|
507
|
+
function normalize(text: string | null | undefined): string | undefined {
|
|
508
|
+
const trimmed = text?.trim();
|
|
509
|
+
return trimmed || undefined;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Extract readable content from raw HTML.
|
|
514
|
+
* Tries Readability (article-isolation scoring) first, then falls back to a
|
|
515
|
+
* CSS selector chain over the same pre-parsed DOM. Returns null if neither
|
|
516
|
+
* path yields usable content.
|
|
517
|
+
*/
|
|
518
|
+
export function extractReadableFromHtml(html: string, url: string, format: ReadableFormat): ReadableResult | null {
|
|
519
|
+
const { document } = parseHTML(html);
|
|
520
|
+
|
|
521
|
+
// --- Primary: Readability article extraction ---
|
|
522
|
+
const article = new Readability(document).parse();
|
|
523
|
+
if (article) {
|
|
524
|
+
const result = toReadableResult(url, format, article.textContent, article.content, {
|
|
525
|
+
title: article.title,
|
|
526
|
+
byline: article.byline,
|
|
527
|
+
excerpt: article.excerpt,
|
|
528
|
+
length: article.length,
|
|
529
|
+
});
|
|
530
|
+
if (result) return result;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- Fallback: CSS selector chain ---
|
|
534
|
+
const candidates = [
|
|
535
|
+
document.querySelector("[data-pagefind-body]"),
|
|
536
|
+
document.querySelector("main article"),
|
|
537
|
+
document.querySelector("article"),
|
|
538
|
+
document.querySelector("main"),
|
|
539
|
+
document.querySelector("[role='main']"),
|
|
540
|
+
document.body,
|
|
541
|
+
];
|
|
542
|
+
for (const el of candidates) {
|
|
543
|
+
if (!el) continue;
|
|
544
|
+
const innerHTML = el.innerHTML?.trim();
|
|
545
|
+
const textContent = el.textContent?.trim();
|
|
546
|
+
if (!innerHTML || !textContent) continue;
|
|
547
|
+
const result = toReadableResult(url, format, textContent, innerHTML, {
|
|
548
|
+
title: document.title,
|
|
549
|
+
excerpt: textContent.slice(0, 240),
|
|
550
|
+
length: textContent.length,
|
|
551
|
+
});
|
|
552
|
+
if (result) return result;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Shared builder for both extraction paths. */
|
|
559
|
+
function toReadableResult(
|
|
560
|
+
url: string,
|
|
561
|
+
format: ReadableFormat,
|
|
562
|
+
textContent: string | null | undefined,
|
|
563
|
+
htmlContent: string | null | undefined,
|
|
564
|
+
meta: { title?: string | null; byline?: string | null; excerpt?: string | null; length?: number | null },
|
|
565
|
+
): ReadableResult | null {
|
|
566
|
+
const text = normalize(textContent);
|
|
567
|
+
const markdown = format === "markdown" ? (normalize(htmlToBasicMarkdown(htmlContent ?? "")) ?? text) : undefined;
|
|
568
|
+
const normalizedText = format === "text" ? text : undefined;
|
|
569
|
+
if (!normalizedText && !markdown) return null;
|
|
570
|
+
return {
|
|
571
|
+
url,
|
|
572
|
+
title: normalize(meta.title),
|
|
573
|
+
byline: normalize(meta.byline),
|
|
574
|
+
excerpt: normalize(meta.excerpt),
|
|
575
|
+
contentLength: meta.length ?? text?.length ?? markdown?.length ?? 0,
|
|
576
|
+
text: normalizedText,
|
|
577
|
+
markdown,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
504
581
|
function ensureParam<T>(value: T | undefined, name: string, action: string): T {
|
|
505
582
|
if (value === undefined || value === null || value === "") {
|
|
506
583
|
throw new ToolError(`Missing required parameter '${name}' for action '${action}'.`);
|
|
@@ -1365,26 +1442,13 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1365
1442
|
const format = params.format ?? "markdown";
|
|
1366
1443
|
const html = (await untilAborted(signal, () => page.content())) as string;
|
|
1367
1444
|
const url = page.url();
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
const article = reader.parse();
|
|
1371
|
-
if (!article) {
|
|
1445
|
+
const readable = extractReadableFromHtml(html, url, format);
|
|
1446
|
+
if (!readable) {
|
|
1372
1447
|
throw new ToolError("Readable content not found");
|
|
1373
1448
|
}
|
|
1374
|
-
const markdown = format === "markdown" ? htmlToBasicMarkdown(article.content ?? "") : undefined;
|
|
1375
|
-
const text = format === "text" ? (article.textContent ?? "") : undefined;
|
|
1376
|
-
const readable: ReadableResult = {
|
|
1377
|
-
url,
|
|
1378
|
-
title: article.title ?? undefined,
|
|
1379
|
-
byline: article.byline ?? undefined,
|
|
1380
|
-
excerpt: article.excerpt ?? undefined,
|
|
1381
|
-
contentLength: article.length ?? article.textContent?.length ?? 0,
|
|
1382
|
-
text,
|
|
1383
|
-
markdown,
|
|
1384
|
-
};
|
|
1385
1449
|
details.url = url;
|
|
1386
1450
|
details.readable = readable;
|
|
1387
|
-
details.result = format === "markdown" ? (markdown ?? "") : (text ?? "");
|
|
1451
|
+
details.result = format === "markdown" ? (readable.markdown ?? "") : (readable.text ?? "");
|
|
1388
1452
|
return toolResult(details)
|
|
1389
1453
|
.text(JSON.stringify(readable, null, 2))
|
|
1390
1454
|
.done();
|
|
@@ -1407,13 +1471,12 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
1407
1471
|
buffer = (await untilAborted(signal, () => page.screenshot({ type: "png", fullPage }))) as Buffer;
|
|
1408
1472
|
}
|
|
1409
1473
|
|
|
1410
|
-
// Compress for API content
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
// Use a tighter budget than the global per-image limit to avoid 413 request_too_large.
|
|
1474
|
+
// Compress aggressively for API content — screenshots are the most
|
|
1475
|
+
// frequent image source and land directly in the next LLM request.
|
|
1476
|
+
// 1024px is plenty for OCR/UI inspection; 150KB keeps payloads lean.
|
|
1414
1477
|
const resized = await resizeImage(
|
|
1415
1478
|
{ type: "image", data: buffer.toBase64(), mimeType: "image/png" },
|
|
1416
|
-
{
|
|
1479
|
+
{ maxWidth: 1024, maxHeight: 1024, maxBytes: 150 * 1024, jpegQuality: 70 },
|
|
1417
1480
|
);
|
|
1418
1481
|
// Resolve destination: user-defined path > screenshotDir (auto-named) > temp file.
|
|
1419
1482
|
const screenshotDir = (() => {
|
package/src/tools/fetch.ts
CHANGED
|
@@ -84,7 +84,7 @@ const IMAGE_MIME_BY_EXTENSION = new Map<string, string>([
|
|
|
84
84
|
]);
|
|
85
85
|
const SUPPORTED_INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
86
86
|
const MAX_INLINE_IMAGE_SOURCE_BYTES = 20 * 1024 * 1024;
|
|
87
|
-
const MAX_INLINE_IMAGE_OUTPUT_BYTES =
|
|
87
|
+
const MAX_INLINE_IMAGE_OUTPUT_BYTES = 300 * 1024;
|
|
88
88
|
|
|
89
89
|
// =============================================================================
|
|
90
90
|
// Utilities
|
package/src/tools/find.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
-
import
|
|
4
|
+
import * as natives from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -124,46 +124,15 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
124
124
|
throw new ToolError("Limit must be a positive number");
|
|
125
125
|
}
|
|
126
126
|
const includeHidden = hidden ?? true;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
throw new ToolError(`Path not found: ${scopePath}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!hasGlob && this.#customOps.stat) {
|
|
135
|
-
const stat = await this.#customOps.stat(searchPath);
|
|
136
|
-
if (stat.isFile()) {
|
|
137
|
-
const files = [scopePath];
|
|
138
|
-
const details: FindToolDetails = {
|
|
139
|
-
scopePath,
|
|
140
|
-
fileCount: 1,
|
|
141
|
-
files,
|
|
142
|
-
truncated: false,
|
|
143
|
-
};
|
|
144
|
-
return toolResult(details).text(files.join("\n")).done();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const results = await this.#customOps.glob(globPattern, searchPath, {
|
|
149
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
150
|
-
limit: effectiveLimit,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
if (results.length === 0) {
|
|
127
|
+
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
128
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
129
|
+
const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
|
|
130
|
+
if (files.length === 0) {
|
|
154
131
|
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
155
132
|
return toolResult(details).text("No files found matching pattern").done();
|
|
156
133
|
}
|
|
157
134
|
|
|
158
|
-
|
|
159
|
-
const relativized = results.map(p => {
|
|
160
|
-
if (p.startsWith(searchPath)) {
|
|
161
|
-
return p.slice(searchPath.length + 1);
|
|
162
|
-
}
|
|
163
|
-
return path.relative(searchPath, p);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const listLimit = applyListLimit(relativized, { limit: effectiveLimit });
|
|
135
|
+
const listLimit = applyListLimit(files, { limit: effectiveLimit });
|
|
167
136
|
const limited = listLimit.items;
|
|
168
137
|
const limitMeta = listLimit.meta;
|
|
169
138
|
const rawOutput = limited.join("\n");
|
|
@@ -186,6 +155,32 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
186
155
|
}
|
|
187
156
|
|
|
188
157
|
return resultBuilder.done();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (this.#customOps?.glob) {
|
|
161
|
+
if (!(await this.#customOps.exists(searchPath))) {
|
|
162
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!hasGlob && this.#customOps.stat) {
|
|
166
|
+
const stat = await this.#customOps.stat(searchPath);
|
|
167
|
+
if (stat.isFile()) {
|
|
168
|
+
return buildResult([scopePath]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const results = await this.#customOps.glob(globPattern, searchPath, {
|
|
173
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
174
|
+
limit: effectiveLimit,
|
|
175
|
+
});
|
|
176
|
+
const relativized = results.map(p => {
|
|
177
|
+
if (p.startsWith(searchPath)) {
|
|
178
|
+
return p.slice(searchPath.length + 1);
|
|
179
|
+
}
|
|
180
|
+
return path.relative(searchPath, p);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return buildResult(relativized);
|
|
189
184
|
}
|
|
190
185
|
|
|
191
186
|
let searchStat: fs.Stats;
|
|
@@ -199,20 +194,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
if (!hasGlob && searchStat.isFile()) {
|
|
202
|
-
|
|
203
|
-
const details: FindToolDetails = {
|
|
204
|
-
scopePath,
|
|
205
|
-
fileCount: 1,
|
|
206
|
-
files,
|
|
207
|
-
truncated: false,
|
|
208
|
-
};
|
|
209
|
-
return toolResult(details).text(files.join("\n")).done();
|
|
197
|
+
return buildResult([scopePath]);
|
|
210
198
|
}
|
|
211
199
|
if (!searchStat.isDirectory()) {
|
|
212
200
|
throw new ToolError(`Path is not a directory: ${searchPath}`);
|
|
213
201
|
}
|
|
214
202
|
|
|
215
|
-
let matches: GlobMatch[];
|
|
203
|
+
let matches: natives.GlobMatch[];
|
|
216
204
|
const onUpdateMatches: string[] = [];
|
|
217
205
|
const updateIntervalMs = 200;
|
|
218
206
|
let lastUpdate = 0;
|
|
@@ -233,27 +221,25 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
233
221
|
});
|
|
234
222
|
};
|
|
235
223
|
const onMatch = onUpdate
|
|
236
|
-
? (err: Error | null, match: GlobMatch | null) => {
|
|
224
|
+
? (err: Error | null, match: natives.GlobMatch | null) => {
|
|
237
225
|
if (err || signal?.aborted || !match) return;
|
|
238
226
|
let relativePath = match.path;
|
|
239
227
|
if (!relativePath) return;
|
|
240
|
-
if (match.fileType === FileType.Dir && !relativePath.endsWith("/")) {
|
|
228
|
+
if (match.fileType === natives.FileType.Dir && !relativePath.endsWith("/")) {
|
|
241
229
|
relativePath += "/";
|
|
242
230
|
}
|
|
243
231
|
onUpdateMatches.push(relativePath);
|
|
244
232
|
emitUpdate();
|
|
245
233
|
}
|
|
246
234
|
: undefined;
|
|
247
|
-
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
248
|
-
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
249
235
|
|
|
250
236
|
const doGlob = async (useGitignore: boolean) =>
|
|
251
237
|
untilAborted(combinedSignal, () =>
|
|
252
|
-
glob(
|
|
238
|
+
natives.glob(
|
|
253
239
|
{
|
|
254
240
|
pattern: globPattern,
|
|
255
241
|
path: searchPath,
|
|
256
|
-
fileType: FileType.File,
|
|
242
|
+
fileType: natives.FileType.File,
|
|
257
243
|
hidden: includeHidden,
|
|
258
244
|
maxResults: effectiveLimit,
|
|
259
245
|
sortByMtime: true,
|
|
@@ -266,7 +252,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
266
252
|
|
|
267
253
|
try {
|
|
268
254
|
let result = await doGlob(true);
|
|
269
|
-
// If gitignore filtering yielded nothing, retry without it
|
|
270
255
|
if (result.matches.length === 0) {
|
|
271
256
|
result = await doGlob(false);
|
|
272
257
|
}
|
|
@@ -282,12 +267,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
282
267
|
throw error;
|
|
283
268
|
}
|
|
284
269
|
|
|
285
|
-
if (matches.length === 0) {
|
|
286
|
-
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
287
|
-
return toolResult(details).text("No files found matching pattern").done();
|
|
288
|
-
}
|
|
289
270
|
const relativized: string[] = [];
|
|
290
|
-
|
|
291
271
|
for (const match of matches) {
|
|
292
272
|
throwIfAborted(signal);
|
|
293
273
|
const line = match.path;
|
|
@@ -297,9 +277,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
297
277
|
|
|
298
278
|
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
299
279
|
let relativePath = line;
|
|
300
|
-
|
|
301
|
-
const isDirectory = match.fileType === FileType.Dir;
|
|
302
|
-
|
|
280
|
+
const isDirectory = match.fileType === natives.FileType.Dir;
|
|
303
281
|
if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
304
282
|
relativePath += "/";
|
|
305
283
|
}
|
|
@@ -307,39 +285,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
307
285
|
relativized.push(relativePath);
|
|
308
286
|
}
|
|
309
287
|
|
|
310
|
-
|
|
311
|
-
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
312
|
-
return toolResult(details).text("No files found matching pattern").done();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Results are already sorted by mtime from native (sortByMtime: true)
|
|
316
|
-
|
|
317
|
-
const listLimit = applyListLimit(relativized, { limit: effectiveLimit });
|
|
318
|
-
const limited = listLimit.items;
|
|
319
|
-
const limitMeta = listLimit.meta;
|
|
320
|
-
|
|
321
|
-
// Apply byte truncation (no line limit since we already have result limit)
|
|
322
|
-
const rawOutput = limited.join("\n");
|
|
323
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
324
|
-
|
|
325
|
-
const resultOutput = truncation.content;
|
|
326
|
-
const details: FindToolDetails = {
|
|
327
|
-
scopePath,
|
|
328
|
-
fileCount: limited.length,
|
|
329
|
-
files: limited,
|
|
330
|
-
truncated: Boolean(limitMeta.resultLimit || truncation.truncated),
|
|
331
|
-
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
332
|
-
truncation: truncation.truncated ? truncation : undefined,
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const resultBuilder = toolResult(details)
|
|
336
|
-
.text(resultOutput)
|
|
337
|
-
.limits({ resultLimit: limitMeta.resultLimit?.reached });
|
|
338
|
-
if (truncation.truncated) {
|
|
339
|
-
resultBuilder.truncation(truncation, { direction: "head" });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return resultBuilder.done();
|
|
288
|
+
return buildResult(relativized);
|
|
343
289
|
});
|
|
344
290
|
}
|
|
345
291
|
}
|
|
@@ -728,6 +728,7 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
728
728
|
headers: {
|
|
729
729
|
"Content-Type": "application/json",
|
|
730
730
|
Authorization: `Bearer ${apiKey.apiKey}`,
|
|
731
|
+
"X-Title": "Oh-My-Pi",
|
|
731
732
|
},
|
|
732
733
|
body: JSON.stringify(requestBody),
|
|
733
734
|
signal: requestSignal,
|