pi-readseek 0.5.18 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/package.json +2 -2
- package/prompts/def.md +8 -11
- package/prompts/edit.md +25 -77
- package/prompts/hover.md +5 -9
- package/prompts/read.md +16 -15
- package/prompts/refs.md +8 -4
- package/prompts/rename.md +9 -18
- package/prompts/sg.md +9 -10
- package/prompts/write.md +10 -35
- package/src/edit.ts +4 -4
- package/src/grep.ts +10 -6
- package/src/hover.ts +13 -5
- package/src/read.ts +116 -32
- package/src/readseek-client.ts +108 -1
- package/src/readseek-settings.ts +3 -4
- package/src/rename.ts +21 -14
- package/src/tool-prompt-metadata.ts +2 -2
- package/src/tui-render-utils.ts +37 -11
- package/src/write.ts +4 -4
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
23
23
|
|
|
24
24
|
## Tools
|
|
25
25
|
|
|
26
|
-
- **readSeek_read:** reads text
|
|
27
|
-
|
|
26
|
+
- **readSeek_read:** reads text with `LINE:HASH` anchors; images and PDFs can
|
|
27
|
+
be returned as base64 or analyzed locally.
|
|
28
28
|
- **readSeek_edit:** edits existing text files using fresh `LINE:HASH` anchors.
|
|
29
29
|
- **readSeek_grep:** searches text and returns edit-ready anchors.
|
|
30
30
|
- **readSeek_search:** searches code by structural AST pattern.
|
|
@@ -49,7 +49,7 @@ optional (defaults shown):
|
|
|
49
49
|
{
|
|
50
50
|
"readseek": {
|
|
51
51
|
"replacedTools": [],
|
|
52
|
-
"imageMode": "
|
|
52
|
+
"imageMode": "auto",
|
|
53
53
|
"syntaxValidation": "warn",
|
|
54
54
|
"timeoutMs": 120000,
|
|
55
55
|
"grep": {
|
|
@@ -63,10 +63,9 @@ optional (defaults shown):
|
|
|
63
63
|
- **replacedTools:** built-in tool names to replace with their `readSeek_*` equivalents.
|
|
64
64
|
Valid values are `"read"`, `"edit"`, `"write"`, and `"grep"`. For a
|
|
65
65
|
readseek-only file surface, use `["read", "edit", "write", "grep"]`.
|
|
66
|
-
- **imageMode:**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
native image input.
|
|
66
|
+
- **imageMode:** `"auto"` exposes `none`, `ocr`, `caption`, and `objects`;
|
|
67
|
+
`"on"` omits `none`; `"off"` skips image/PDF files. Omitting `image` also
|
|
68
|
+
skips visual files.
|
|
70
69
|
- **syntaxValidation:** pre-write syntax-regression check in `readSeek_edit`:
|
|
71
70
|
`"warn"` writes with a warning, `"block"` aborts without writing, `"off"`
|
|
72
71
|
skips the check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jarkkojs/readseek": "^0.
|
|
42
|
+
"@jarkkojs/readseek": "^0.6.0",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"xxhash-wasm": "^1.1.0"
|
|
45
45
|
},
|
package/prompts/def.md
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
Find structural
|
|
2
|
-
|
|
1
|
+
Find structural definitions by qualified or unqualified name across a file or
|
|
2
|
+
directory.
|
|
3
3
|
|
|
4
4
|
## Parameters
|
|
5
5
|
|
|
6
|
-
- `path`
|
|
7
|
-
- `name
|
|
8
|
-
- `lang`
|
|
9
|
-
- `cached`
|
|
10
|
-
- `others` (optional): Search untracked files.
|
|
11
|
-
- `ignored` (optional): Include ignored untracked files.
|
|
6
|
+
- `path` — file or directory, default `.`.
|
|
7
|
+
- `name` — qualified or unqualified symbol name.
|
|
8
|
+
- `lang` — language override.
|
|
9
|
+
- `cached`, `others`, `ignored` — Git file selection; `ignored` requires `others`.
|
|
12
10
|
|
|
13
11
|
## When to use
|
|
14
12
|
|
|
15
|
-
- After
|
|
16
|
-
- When
|
|
17
|
-
- To find a function/class/type definition by its qualified name.
|
|
13
|
+
- After `readSeek_hover`, use its qualified symbol name.
|
|
14
|
+
- When asked where a function, class, or type is defined.
|
package/prompts/edit.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Edit existing text files with fresh `LINE:HASH` anchors from `readSeek_read`,
|
|
2
|
+
`readSeek_grep`, `readSeek_search`, or `readSeek_write`. The file must have
|
|
3
|
+
been anchored in this session; on `file-not-read`, read or search it first.
|
|
4
4
|
|
|
5
5
|
## Variants
|
|
6
6
|
|
|
@@ -9,12 +9,11 @@ Surgically edit existing text files. Prefer hash-verified anchors from fresh `re
|
|
|
9
9
|
| `set_line` | Replace or delete one line | 1 |
|
|
10
10
|
| `replace_lines` | Replace or delete one contiguous range | 2 |
|
|
11
11
|
| `insert_after` | Insert after an existing line | 1 |
|
|
12
|
-
| `replace_symbol` | Replace one
|
|
13
|
-
| `replace` | Exact string replacement
|
|
14
|
-
|
|
15
|
-
Set `new_text` (or `replace_lines.new_text`) to `""` to delete anchored line(s). For an intentionally blank line, use `"\n"` or whitespace content, not `""`.
|
|
12
|
+
| `replace_symbol` | Replace one mapped symbol | 0 (`symbol`) |
|
|
13
|
+
| `replace` | Exact string replacement; one match unless `all: true` | 0 |
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
Set `new_text` to `""` to delete lines; use `"\n"` for a blank line. Prefer
|
|
16
|
+
anchored variants. Use `replace` only when anchors are impractical.
|
|
18
17
|
|
|
19
18
|
## Input shape
|
|
20
19
|
|
|
@@ -31,92 +30,41 @@ Prefer `set_line`, `replace_lines`, and `insert_after`: they verify that the fil
|
|
|
31
30
|
}
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
Use only
|
|
33
|
+
Use only needed variants. Each `edits[]` entry has exactly one key. `new_text`
|
|
34
|
+
and `new_body` are plain file content, not diffs or hashlines.
|
|
35
35
|
|
|
36
36
|
## Exact and fuzzy replacement
|
|
37
37
|
|
|
38
|
-
`replace` is exact
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`fuzzy: true` is a narrow fallback after exact matching fails. It normalizes whitespace and confusable Unicode such as smart hyphens; it is **not** approximate, Levenshtein, or semantic matching. Fuzzy successes return a warning.
|
|
38
|
+
`replace` is exact by default; missing text fails. `fuzzy: true` only normalizes
|
|
39
|
+
whitespace and confusable Unicode after exact matching fails. Verify warned fuzzy
|
|
40
|
+
matches before continuing.
|
|
43
41
|
|
|
44
42
|
## `replace_symbol`
|
|
45
43
|
|
|
46
|
-
Use `replace_symbol`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- Use an exact name, dotted path, or `@<line>`. If `readSeek_read({ symbol })` returned a fuzzy match, confirm the exact symbol first.
|
|
51
|
-
- Supported for TypeScript, JavaScript, Rust, and Java. For other languages, use anchored edits.
|
|
52
|
-
- `new_body` must not be empty or whitespace-only.
|
|
53
|
-
- Write `new_body` without extra leading indentation; `readSeek_edit` re-indents it to match the original symbol.
|
|
54
|
-
- If `new_body` appears to declare a different symbol name, the edit still applies but returns a `name-mismatch` warning.
|
|
55
|
-
- Do not combine `replace_symbol` with anchored edits that touch the same lines. Duplicate or overlapping `replace_symbol` ranges are rejected.
|
|
44
|
+
Use `replace_symbol` for one whole mapped `Name`, `Class.method`, or
|
|
45
|
+
`Name@<line>` in TypeScript, JavaScript, Rust, or Java. `new_body` must be
|
|
46
|
+
non-empty and unindented. Confirm fuzzy symbol matches first; do not overlap it
|
|
47
|
+
with anchored edits.
|
|
56
48
|
|
|
57
49
|
## Stale anchors
|
|
58
50
|
|
|
59
|
-
|
|
51
|
+
On `hash-mismatch`, nearby lines marked `>>>` include fresh anchors:
|
|
60
52
|
|
|
61
53
|
```text
|
|
62
54
|
>>> 41:b34| const renamed = 3;
|
|
63
55
|
```
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
If `readSeek_edit` auto-relocates an anchor, read the warning and verify that the edit landed in the intended place.
|
|
57
|
+
Retry with those anchors, or read/search again. Verify any auto-relocation warning.
|
|
68
58
|
|
|
69
59
|
## Validation and warnings
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- A `replace`-only success may remind you to prefer anchored edits next time.
|
|
76
|
-
|
|
77
|
-
Syntax validation runs before writing when supported:
|
|
78
|
-
|
|
79
|
-
- Supported: Rust, C++, C headers, Java.
|
|
80
|
-
- Default `warn`: write succeeds, but warnings include `syntax-regression: lines X-Y`.
|
|
81
|
-
- `block`: aborts without writing.
|
|
82
|
-
- `off`: skips validation.
|
|
83
|
-
- The `readseek.syntaxValidation` setting can change the default mode.
|
|
84
|
-
|
|
85
|
-
Existing syntax errors are tolerated; warnings are for newly introduced parser errors.
|
|
61
|
+
All edits validate before writing; hard failures write nothing. Anchored edits run
|
|
62
|
+
bottom-up. `no-op` means no change. Syntax validation for Rust, C++, C headers,
|
|
63
|
+
and Java follows `readseek.syntaxValidation`: `warn` (default), `block`, or `off`.
|
|
64
|
+
It reports only newly introduced parser errors.
|
|
86
65
|
|
|
87
66
|
## Optional post-edit verification
|
|
88
67
|
|
|
89
|
-
`postEditVerify: true`
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Successful `readSeek_edit` results include:
|
|
94
|
-
|
|
95
|
-
- `details.diff` and `details.readSeekValue.diff`: compact human-readable hashline diff strings.
|
|
96
|
-
- `details.patch`: standard unified diff with file and hunk headers.
|
|
97
|
-
- `details.diffData` and `details.readSeekValue.diffData`: stable structured diff data.
|
|
98
|
-
|
|
99
|
-
`diffData` shape:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
type DiffData = {
|
|
103
|
-
version: 1;
|
|
104
|
-
entries: Array<
|
|
105
|
-
| { kind: "context"; oldLine: number; newLine: number; text: string }
|
|
106
|
-
| { kind: "add"; newLine: number; text: string }
|
|
107
|
-
| { kind: "remove"; oldLine: number; text: string }
|
|
108
|
-
| { kind: "meta"; text: string }
|
|
109
|
-
>;
|
|
110
|
-
stats: { added: number; removed: number; context: number };
|
|
111
|
-
language?: string;
|
|
112
|
-
blockRanges?: Array<{ kind: "add" | "remove"; startLine: number; endLine: number }>;
|
|
113
|
-
inlineDiffs?: Array<{
|
|
114
|
-
removeLineIndex: number;
|
|
115
|
-
addLineIndex: number;
|
|
116
|
-
removeSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
|
|
117
|
-
addSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
|
|
118
|
-
}>;
|
|
119
|
-
};
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
For compact one-line hashline diffs, `details.diff` remains compact while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
|
|
68
|
+
`postEditVerify: true` reads back the written file and compares the persisted
|
|
69
|
+
content, including BOM and line endings. Results provide a compact hashline diff,
|
|
70
|
+
a unified patch, and structured `details.diffData`.
|
package/prompts/hover.md
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
so unsaved editor content is included.
|
|
1
|
+
Identify the identifier and enclosing symbol at a cursor. Unsaved editor content
|
|
2
|
+
is included.
|
|
4
3
|
|
|
5
4
|
## Parameters
|
|
6
5
|
|
|
7
|
-
- `path`
|
|
8
|
-
- `
|
|
9
|
-
- `column` (optional): One-based cursor byte column.
|
|
6
|
+
- `path`, `line` — required file path and one-based cursor line.
|
|
7
|
+
- `column` — optional one-based cursor byte column.
|
|
10
8
|
|
|
11
9
|
## When to use
|
|
12
10
|
|
|
13
|
-
- Before
|
|
14
|
-
- Before a go-to-definition, to get the qualified symbol name.
|
|
15
|
-
- To inspect what symbol a specific line belongs to.
|
|
11
|
+
- Before rename or go-to-definition, or to identify a line's enclosing symbol.
|
package/prompts/read.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
Read files through readseek. Text
|
|
1
|
+
Read files through readseek. Text results use `LINE:HASH|content` anchors for `readSeek_edit`. Images and PDFs require an explicit available `image` mode; omitting it skips them. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}.
|
|
2
2
|
|
|
3
3
|
## Choose the right read
|
|
4
4
|
|
|
5
|
-
- Normal read:
|
|
6
|
-
- `map: true`: append a structural map
|
|
7
|
-
- `symbol: "Name"`: read one
|
|
8
|
-
- `bundle: "local"`:
|
|
5
|
+
- Normal read: a small file or an `offset` / `limit` range.
|
|
6
|
+
- `map: true`: append a structural map.
|
|
7
|
+
- `symbol: "Name"`: read one mapped symbol.
|
|
8
|
+
- `bundle: "local"`: include direct same-file support for a symbol.
|
|
9
9
|
|
|
10
10
|
## Parameters
|
|
11
11
|
|
|
12
12
|
- `path` — file path.
|
|
13
|
-
- `offset` / `limit` — positive
|
|
14
|
-
- `map` —
|
|
15
|
-
- `symbol` —
|
|
16
|
-
- `bundle` — only `"local"`; requires `symbol` and
|
|
13
|
+
- `offset` / `limit` — positive lines; `offset` is 1-indexed.
|
|
14
|
+
- `map` — full-file map; incompatible with `symbol` or `bundle`.
|
|
15
|
+
- `symbol` — `Name`, `Class.method`, or `Name@<line>`; incompatible with `offset` / `limit`.
|
|
16
|
+
- `bundle` — only `"local"`; requires `symbol` and excludes `map`.
|
|
17
|
+
- `image` — an exposed image/PDF mode.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Truncated full-file reads append a map when available. Use its ranges for follow-up reads.
|
|
19
20
|
|
|
20
21
|
## Symbol examples
|
|
21
22
|
|
|
@@ -29,13 +30,13 @@ When a full-file read is truncated, readseek appends a structural map automatica
|
|
|
29
30
|
|
|
30
31
|
## Symbol resolution
|
|
31
32
|
|
|
32
|
-
`@<line>` only
|
|
33
|
+
`@<line>` is only a trailing suffix, as in `Foo.bar@42`; `foo@bar` is an ordinary name. Resolution prefers the containing range, then the nearest symbol at or after the line, then one above it.
|
|
33
34
|
|
|
34
35
|
Result behavior:
|
|
35
36
|
|
|
36
|
-
- **Found
|
|
37
|
-
- **Ambiguous
|
|
38
|
-
- **Fuzzy
|
|
39
|
-
- **Not found
|
|
37
|
+
- **Found:** the symbol range.
|
|
38
|
+
- **Ambiguous:** candidates and `name@<startLine>` retry hints.
|
|
39
|
+
- **Fuzzy:** a warned best match; verify before editing.
|
|
40
|
+
- **Not found/unmappable:** normal read with a warning and suggestions when available.
|
|
40
41
|
|
|
41
42
|
Hash anchors from normal, symbol, and bundled reads are valid for `readSeek_edit` until the file changes.
|
package/prompts/refs.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
Find
|
|
1
|
+
Find identifier references before renaming, deleting, or changing a symbol when
|
|
2
|
+
text search is too broad. Results include edit-ready anchors and enclosing symbols.
|
|
2
3
|
|
|
3
4
|
## Parameters
|
|
4
5
|
|
|
5
6
|
- `name` — identifier to find references for.
|
|
6
7
|
- `path` — file or directory, default cwd.
|
|
7
|
-
- `lang` — language hint
|
|
8
|
+
- `lang` — language hint for ambiguous, extensionless, or generated code.
|
|
8
9
|
- `scope` — restrict results to the binding under `line`/`column` in a single file. Requires `line`.
|
|
9
10
|
- `line` — one-based cursor line, used with `scope`.
|
|
10
11
|
- `column` — one-based cursor byte column, used with `scope`.
|
|
@@ -14,10 +15,13 @@ Find binding-accurate references to an identifier with readseek. Use it when you
|
|
|
14
15
|
|
|
15
16
|
## Scope
|
|
16
17
|
|
|
17
|
-
Without `scope`, references match by
|
|
18
|
+
Without `scope`, references match by name. With `scope` and `line` (optionally
|
|
19
|
+
`column`), results are limited to the cursor binding in one file and exclude
|
|
20
|
+
shadows.
|
|
18
21
|
|
|
19
22
|
## Git selection
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
In Git repositories, directory search includes tracked/indexed and untracked
|
|
25
|
+
non-ignored files. `ignored` requires `others`.
|
|
22
26
|
|
|
23
27
|
Use `readSeek_grep` for plain text, `readSeek_search` for code shape, and `readSeek_refs` for identifier usage.
|
package/prompts/rename.md
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
lexical binding under the cursor and renames every occurrence that binds
|
|
4
|
-
to the same declaration.
|
|
1
|
+
Rename an identifier with binding accuracy. The cursor binding is resolved and
|
|
2
|
+
only occurrences of that declaration are changed.
|
|
5
3
|
|
|
6
4
|
## Parameters
|
|
7
5
|
|
|
8
|
-
- `path`
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
12
|
-
- `workspace` (optional): When true, expands the rename across the project
|
|
13
|
-
root. The cursor file remains binding-accurate; other files are matched
|
|
14
|
-
by name (free uses only, local shadows excluded).
|
|
15
|
-
- `apply` (optional, default true): When true, writes the edits to disk
|
|
16
|
-
after verifying line hashes. When false, returns the plan only.
|
|
6
|
+
- `path`, `line`, `to` — required file, one-based cursor line, and plain new name.
|
|
7
|
+
- `column` — optional one-based cursor byte column.
|
|
8
|
+
- `workspace` — expand across the project; local shadows in other files are excluded.
|
|
9
|
+
- `apply` — default `true`; set `false` to return only the verified plan.
|
|
17
10
|
|
|
18
11
|
## When to use
|
|
19
12
|
|
|
20
|
-
-
|
|
21
|
-
- The cursor position (line, optionally column) is known from an identify
|
|
22
|
-
call or from the user's editor context.
|
|
13
|
+
- Use when the user requests a symbol rename and the cursor is known.
|
|
23
14
|
|
|
24
15
|
## When not to use
|
|
25
16
|
|
|
26
|
-
-
|
|
27
|
-
|
|
17
|
+
- Use `readSeek_grep`/`readSeek_search` for broad replacement and
|
|
18
|
+
`readSeek_edit` for other refactors.
|
package/prompts/sg.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
Search code with
|
|
1
|
+
Search code with AST patterns when text search is too broad. Use it for calls,
|
|
2
|
+
imports, declarations, JSX, object fields, and control flow. Results include
|
|
3
|
+
edit-ready anchors.
|
|
2
4
|
|
|
3
5
|
## Parameters
|
|
4
6
|
|
|
5
7
|
- `pattern` — ast-grep-style pattern to match.
|
|
6
|
-
- `lang` — language hint
|
|
8
|
+
- `lang` — language hint for ambiguous, extensionless, generated, or JSX-like code.
|
|
7
9
|
- `path` — file or directory, default cwd.
|
|
8
10
|
- `cached` — in a Git repository, search tracked/indexed files.
|
|
9
11
|
- `others` — in a Git repository, search untracked files.
|
|
@@ -16,7 +18,8 @@ Search code with readseek AST patterns. Use it when text search is too broad or
|
|
|
16
18
|
- `$$$ARGS` matches zero or more sibling nodes. Use it for function args, body statements, object fields, JSX children, etc.
|
|
17
19
|
- Reusing a metavariable name requires every occurrence to match the same source text.
|
|
18
20
|
|
|
19
|
-
Patterns are
|
|
21
|
+
Patterns are code, not text: formatting is mostly ignored, but syntax and required
|
|
22
|
+
punctuation must be valid for the selected language.
|
|
20
23
|
|
|
21
24
|
## Examples
|
|
22
25
|
|
|
@@ -27,14 +30,10 @@ Patterns are parsed as code, not text. Formatting is mostly ignored, but syntax
|
|
|
27
30
|
- `<$TAG $$$ATTRS>$$$CHILDREN</$TAG>` — JSX/TSX elements.
|
|
28
31
|
- `if ($COND) { $$$BODY }` — control-flow blocks.
|
|
29
32
|
|
|
30
|
-
## Languages
|
|
31
|
-
|
|
32
|
-
Useful `lang` values include `assembly`, `bash`, `c`, `cpp`, `csharp`, `css`, `dockerfile`, `gdscript`, `go`, `html`, `java`, `javascript`, `json`, `jsx`, `just`, `kconfig`, `latex`, `lua`, `make`, `markdown`, `meson`, `nix`, `perl`, `php`, `puppet`, `python`, `riscv`, `ruby`, `rust`, `sql`, `swift`, `toml`, `tsx`, `typescript`, `typst`, `xml`, `yaml`, `zig`, and `unknown`.
|
|
33
|
-
|
|
34
|
-
`unknown` forces text-only handling and is not useful for parser-backed search.
|
|
35
|
-
|
|
36
33
|
## Git selection
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
In Git repositories, directory search includes tracked/indexed and untracked
|
|
36
|
+
non-ignored files. Use `cached`, `others`, and `ignored` to choose; `ignored`
|
|
37
|
+
requires `others`.
|
|
39
38
|
|
|
40
39
|
Use `readSeek_grep` for plain text and `readSeek_search` for structure.
|
package/prompts/write.md
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
|
-
Create or
|
|
1
|
+
Create or replace a whole file and return `LINE:HASH` anchors for follow-up edits.
|
|
2
2
|
|
|
3
3
|
## Use / avoid
|
|
4
4
|
|
|
5
|
-
Use
|
|
5
|
+
Use it for new, generated, or intentionally replaced files. For small changes or
|
|
6
|
+
appends, read/search first and use `readSeek_edit`.
|
|
6
7
|
|
|
7
|
-
Existing files are overwritten without confirmation. Binary-looking content
|
|
8
|
+
Existing files are overwritten without confirmation. Binary-looking content gets
|
|
9
|
+
no anchors.
|
|
8
10
|
|
|
9
11
|
## Parameters
|
|
10
12
|
|
|
11
|
-
- `path` —
|
|
13
|
+
- `path` — file path.
|
|
12
14
|
- `content` — complete file contents.
|
|
13
|
-
- `map` —
|
|
15
|
+
- `map` — append a best-effort structural map.
|
|
14
16
|
|
|
15
17
|
## Output
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Successful text `readSeek_write` results include additive final `details.diff`, `details.readSeekValue.diff`, `details.diffData`, and `details.readSeekValue.diffData` fields. String diff fields remain the backward-compatible human-readable fallback.
|
|
22
|
-
|
|
23
|
-
`diffData` is a stable versioned contract:
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
type DiffData = {
|
|
27
|
-
version: 1;
|
|
28
|
-
entries: Array<
|
|
29
|
-
| { kind: "context"; oldLine: number; newLine: number; text: string }
|
|
30
|
-
| { kind: "add"; newLine: number; text: string }
|
|
31
|
-
| { kind: "remove"; oldLine: number; text: string }
|
|
32
|
-
| { kind: "meta"; text: string }
|
|
33
|
-
>;
|
|
34
|
-
stats: { added: number; removed: number; context: number };
|
|
35
|
-
language?: string;
|
|
36
|
-
blockRanges?: Array<{ kind: "add" | "remove"; startLine: number; endLine: number }>;
|
|
37
|
-
inlineDiffs?: Array<{
|
|
38
|
-
removeLineIndex: number;
|
|
39
|
-
addLineIndex: number;
|
|
40
|
-
removeSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
|
|
41
|
-
addSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
|
|
42
|
-
}>;
|
|
43
|
-
};
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
For compact one-line hashline diffs, `details.diff` remains compact while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
|
|
19
|
+
Text writes return `LINE:HASH|content`. Visible output is capped at 2000 lines or
|
|
20
|
+
50 KB; full anchors remain in `readSeekValue`. Results also include a compact
|
|
21
|
+
diff, unified patch, and structured `details.diffData`.
|
package/src/edit.ts
CHANGED
|
@@ -627,7 +627,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
627
627
|
const { isPartial, isError, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
628
628
|
|
|
629
629
|
if (isPartial) {
|
|
630
|
-
return renderPendingResult("pending edit", width);
|
|
630
|
+
return renderPendingResult("pending edit", width, theme);
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
// Extract data from result
|
|
@@ -659,16 +659,16 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
659
659
|
let text = "";
|
|
660
660
|
|
|
661
661
|
if (info.noOp) {
|
|
662
|
-
text = summaryLine("no-op");
|
|
662
|
+
text = summaryLine("no-op", { theme, style: "dim" });
|
|
663
663
|
if (expanded && info.errorText) text += `\n${theme.fg("error", info.errorText)}`;
|
|
664
664
|
} else if (info.errorText) {
|
|
665
665
|
const firstLine = info.errorText.split("\n")[0] || "Error";
|
|
666
|
-
text = summaryLine(expanded ? info.errorText : firstLine);
|
|
666
|
+
text = summaryLine(expanded ? info.errorText : firstLine, { theme, style: "error" });
|
|
667
667
|
} else {
|
|
668
668
|
const badges: string[] = [`edited +${stats.added} -${stats.removed}`];
|
|
669
669
|
if (info.semanticBadge) badges.push(info.semanticBadge.replace(/^✓\s*/, ""));
|
|
670
670
|
if (info.warningsBadge) badges.push(info.warningsBadge);
|
|
671
|
-
text = summaryLine(badges.join(" • "), { hidden: !!diffData && !expanded });
|
|
671
|
+
text = summaryLine(badges.join(" • "), { hidden: !!diffData && !expanded, theme, style: "success" });
|
|
672
672
|
if (expanded && diffData) {
|
|
673
673
|
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
674
674
|
}
|
package/src/grep.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
19
19
|
import { throwIfAborted } from "./runtime.js";
|
|
20
20
|
import { formatGrepCallText, formatGrepResultText, GREP_TRUNCATION_THRESHOLD } from "./grep-render-helpers.js";
|
|
21
21
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
22
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
22
|
+
import { clampLineToWidth, clampLinesToWidth, linkedPathLine, linkToolPath, renderErrorResult, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
23
23
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
24
24
|
import { optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
25
25
|
import { searchPathParam } from "./readseek-params.js";
|
|
@@ -554,12 +554,12 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
554
554
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
555
555
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
556
556
|
|
|
557
|
-
if (isPartial) return renderPendingResult("pending grep", width);
|
|
557
|
+
if (isPartial) return renderPendingResult("pending grep", width, theme);
|
|
558
558
|
|
|
559
559
|
const content = result.content?.[0];
|
|
560
560
|
const textContent = content?.type === "text" ? content.text : "";
|
|
561
561
|
|
|
562
|
-
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
562
|
+
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width, theme });
|
|
563
563
|
|
|
564
564
|
const readSeekValue = (result.details as any)?.readSeekValue as {
|
|
565
565
|
tool: "grep";
|
|
@@ -583,17 +583,21 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
583
583
|
hasBinaryWarning,
|
|
584
584
|
});
|
|
585
585
|
|
|
586
|
-
if (info.noMatches && !hasBinaryWarning) return new Text(summaryLine("no matches"), 0, 0);
|
|
586
|
+
if (info.noMatches && !hasBinaryWarning) return new Text(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
|
|
587
587
|
const matchCount = readSeekValue?.totalMatches ?? 0;
|
|
588
588
|
const matchWord = matchCount === 1 ? "match" : "matches";
|
|
589
|
-
let text = summaryLine(`${matchCount} ${matchWord} returned`, {
|
|
589
|
+
let text = summaryLine(`${matchCount} ${matchWord} returned`, {
|
|
590
|
+
hidden: !!textContent && !expanded,
|
|
591
|
+
theme,
|
|
592
|
+
style: "success",
|
|
593
|
+
});
|
|
590
594
|
for (const badge of info.badges) text += theme.fg(badge.startsWith("⚠") ? "warning" : "dim", ` ${badge}`);
|
|
591
595
|
if (expanded && readSeekValue?.records) {
|
|
592
596
|
const fileCounts = new Map<string, number>();
|
|
593
597
|
for (const r of readSeekValue.records) if (r.path && r.kind === "match") fileCounts.set(r.path, (fileCounts.get(r.path) ?? 0) + 1);
|
|
594
598
|
for (const [filePath, count] of [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)) {
|
|
595
599
|
const display = path.relative(cwd, filePath) || filePath;
|
|
596
|
-
text +=
|
|
600
|
+
text += `\n${linkedPathLine(theme, filePath, display, cwd, ` (${count})`, width)}`;
|
|
597
601
|
}
|
|
598
602
|
if (fileCounts.size > 20) text += "\n" + theme.fg("muted", ` … and ${fileCounts.size - 20} more files`);
|
|
599
603
|
}
|
package/src/hover.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { formatFsError } from "./fs-error.js";
|
|
|
11
11
|
import { classifyReadSeekFailure, readSeekIdentify } from "./readseek-client.js";
|
|
12
12
|
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
13
13
|
|
|
14
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
14
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
15
15
|
|
|
16
16
|
const HOVER_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
17
|
promptUrl: new URL("../prompts/hover.md", import.meta.url),
|
|
@@ -112,18 +112,26 @@ export function registerHoverTool(pi: ExtensionAPI) {
|
|
|
112
112
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
113
113
|
},
|
|
114
114
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
115
|
-
const { isPartial, isError, width } = resolveRenderResultContext(options, rest);
|
|
115
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
116
116
|
|
|
117
|
-
if (isPartial) return renderPendingResult("pending hover", width);
|
|
117
|
+
if (isPartial) return renderPendingResult("pending hover", width, theme);
|
|
118
118
|
|
|
119
119
|
const content = result.content?.[0];
|
|
120
120
|
const textContent = content?.type === "text" ? content.text : "";
|
|
121
121
|
|
|
122
122
|
if (isError || result.isError) {
|
|
123
|
-
|
|
123
|
+
return renderErrorResult(textContent, { expanded, width, fallback: "hover failed", theme });
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const output = (result.details as any)?.readSeekValue?.output;
|
|
127
|
+
const label = output?.identifier?.text
|
|
128
|
+
? `identified ${output.identifier.text}`
|
|
129
|
+
: output?.symbol?.name
|
|
130
|
+
? `symbol ${output.symbol.name}`
|
|
131
|
+
: "identified cursor";
|
|
132
|
+
let text = summaryLine(label, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
133
|
+
if (expanded && textContent) text += `\n${textContent}`;
|
|
134
|
+
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
127
135
|
},
|
|
128
136
|
});
|
|
129
137
|
}
|
package/src/read.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { readFile as fsReadFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI, ToolRenderResultOptions, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
|
-
createReadTool,
|
|
6
5
|
truncateHead,
|
|
7
6
|
DEFAULT_MAX_BYTES,
|
|
8
7
|
DEFAULT_MAX_LINES,
|
|
@@ -26,7 +25,7 @@ import { buildReadOutput } from "./read-output.js";
|
|
|
26
25
|
|
|
27
26
|
import { buildLocalBundle } from "./read-local-bundle.js";
|
|
28
27
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
29
|
-
import { readSeekRead, readSeekDetect, readSeekImage, type ReadSeekDetection } from "./readseek-client.js";
|
|
28
|
+
import { readSeekRead, readSeekDetect, readSeekImage, readSeekPdf, readSeekPreparedImage, type ReadSeekDetection, type ReadSeekPdfOutput } from "./readseek-client.js";
|
|
30
29
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
31
30
|
import { resolveReadSeekImageMode } from "./readseek-settings.js";
|
|
32
31
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidthCached } from "./tui-render-utils.js";
|
|
@@ -41,6 +40,7 @@ interface ReadParams {
|
|
|
41
40
|
symbol?: string;
|
|
42
41
|
map?: boolean;
|
|
43
42
|
bundle?: string;
|
|
43
|
+
image?: "none" | "ocr" | "caption" | "objects";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
interface ReadToolOptions {
|
|
@@ -55,8 +55,6 @@ export interface ExecuteReadOptions {
|
|
|
55
55
|
onUpdate: any;
|
|
56
56
|
cwd: string;
|
|
57
57
|
onSuccessfulRead?: FileAnchoredCallback;
|
|
58
|
-
/** Whether the active model accepts image input natively. Used when imageMode is auto. */
|
|
59
|
-
modelSupportsImages?: boolean;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
@@ -82,8 +80,32 @@ function formatImageAnalysis(detection: ReadSeekDetection): string | undefined {
|
|
|
82
80
|
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
83
81
|
}
|
|
84
82
|
|
|
83
|
+
function formatPdfAnalysis(pdf: ReadSeekPdfOutput): string {
|
|
84
|
+
const sections = [pdf.markdown.trimEnd()];
|
|
85
|
+
for (const image of pdf.images) {
|
|
86
|
+
const details: string[] = [];
|
|
87
|
+
const ocr = image.ocr?.trim();
|
|
88
|
+
if (ocr) details.push(`OCR text:\n${ocr}`);
|
|
89
|
+
const caption = image.caption?.trim();
|
|
90
|
+
if (caption) details.push(`Caption:\n${caption}`);
|
|
91
|
+
if (image.objects?.length) {
|
|
92
|
+
const lines = image.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
93
|
+
details.push(`Detected objects:\n${lines.join("\n")}`);
|
|
94
|
+
}
|
|
95
|
+
if (details.length > 0) sections.push(`PDF page ${image.page} image:\n${details.join("\n\n")}`);
|
|
96
|
+
}
|
|
97
|
+
return sections.filter(Boolean).join("\n\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function skippedVisualFile(path: string): AgentToolResult<any> {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: `[Skipped image/PDF: ${path}; no image mode selected]` }],
|
|
103
|
+
details: {},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
|
86
|
-
const {
|
|
108
|
+
const { params, signal, cwd, onSuccessfulRead } = opts;
|
|
87
109
|
await ensureHashInit();
|
|
88
110
|
const rawParams = params as ReadParams;
|
|
89
111
|
const offset = coerceObviousBase10Int(rawParams.offset, "offset");
|
|
@@ -165,41 +187,82 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
165
187
|
}
|
|
166
188
|
|
|
167
189
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
168
|
-
|
|
190
|
+
const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
|
|
191
|
+
if (hasBinaryContent || ext === "pdf" || hasPdfHeader) {
|
|
169
192
|
let detection: ReadSeekDetection | undefined;
|
|
170
193
|
try {
|
|
171
194
|
detection = await readSeekDetect(absolutePath, { signal });
|
|
172
195
|
} catch {
|
|
196
|
+
return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
|
|
197
|
+
path: rawParams.path,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!detection) {
|
|
201
|
+
return buildToolErrorResult("read", "read-error", `Visual file detection returned no data for ${absolutePath}.`, {
|
|
202
|
+
path: rawParams.path,
|
|
203
|
+
});
|
|
173
204
|
}
|
|
174
|
-
if (detection
|
|
175
|
-
const builtinRead = createReadTool(cwd);
|
|
176
|
-
const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
|
|
205
|
+
if (detection.kind === "image" || detection.type === "application/pdf") {
|
|
177
206
|
const imageMode = resolveReadSeekImageMode();
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
if (imageMode === "off" || p.image === undefined) {
|
|
208
|
+
return skippedVisualFile(rawParams.path);
|
|
209
|
+
}
|
|
210
|
+
if (imageMode === "on" && p.image === "none") {
|
|
211
|
+
return buildToolErrorResult("read", "invalid-params-combo", 'image="none" is only available when imageMode is "auto".', {
|
|
212
|
+
path: rawParams.path,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (p.image === "none") {
|
|
216
|
+
try {
|
|
217
|
+
if (detection.kind === "pdf") {
|
|
218
|
+
const pdf = await readSeekPdf(absolutePath, p.image, { signal });
|
|
219
|
+
const content: AgentToolResult<any>["content"] = [{ type: "text", text: pdf.markdown }];
|
|
220
|
+
for (const image of pdf.images) {
|
|
221
|
+
if (image.encoding !== "base64" || image.data === undefined) continue;
|
|
222
|
+
content.push({ type: "text", text: `[PDF page ${image.page} image]` });
|
|
223
|
+
content.push({ type: "image", data: image.data, mimeType: image.mime });
|
|
224
|
+
}
|
|
225
|
+
return succeed({ content, details: {} });
|
|
226
|
+
}
|
|
227
|
+
const image = await readSeekPreparedImage(absolutePath, { signal });
|
|
228
|
+
return succeed({
|
|
229
|
+
content: [{ type: "image" as const, data: image.data, mimeType: image.mime }],
|
|
230
|
+
details: {},
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
|
|
235
|
+
path: rawParams.path,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
180
239
|
|
|
181
240
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
241
|
+
if (detection.kind === "pdf") {
|
|
242
|
+
const pdf = await readSeekPdf(absolutePath, p.image, { signal });
|
|
243
|
+
return succeed({
|
|
244
|
+
content: [{ type: "text" as const, text: formatPdfAnalysis(pdf) }],
|
|
245
|
+
details: {},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const analysis = await readSeekImage(absolutePath, [p.image], { signal });
|
|
249
|
+
const imageAnalysis = formatImageAnalysis(analysis);
|
|
184
250
|
if (imageAnalysis) {
|
|
185
251
|
return succeed({
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
...(builtinResult.content ?? []),
|
|
189
|
-
{ type: "text" as const, text: imageAnalysis },
|
|
190
|
-
],
|
|
252
|
+
content: [{ type: "text" as const, text: imageAnalysis }],
|
|
253
|
+
details: {},
|
|
191
254
|
});
|
|
192
255
|
}
|
|
193
|
-
} catch {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{ type: "text" as const, text: "[Warning: local image analysis unavailable — showing image attachment only]" },
|
|
199
|
-
],
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
const label = detection.kind === "pdf" ? "PDF" : "Image";
|
|
259
|
+
return buildToolErrorResult("read", "read-error", `${label} analysis unavailable: ${message}`, {
|
|
260
|
+
path: rawParams.path,
|
|
200
261
|
});
|
|
201
262
|
}
|
|
202
|
-
return
|
|
263
|
+
return buildToolErrorResult("read", "read-error", "Image analysis returned no content.", {
|
|
264
|
+
path: rawParams.path,
|
|
265
|
+
});
|
|
203
266
|
}
|
|
204
267
|
}
|
|
205
268
|
throwIfAborted(signal);
|
|
@@ -457,6 +520,8 @@ function splitReadSeekLines(text: string): string[] {
|
|
|
457
520
|
|
|
458
521
|
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
459
522
|
const name = options.name ?? "readSeek_read";
|
|
523
|
+
const imageMode = resolveReadSeekImageMode();
|
|
524
|
+
const imageModes = imageMode === "auto" ? ["none", "ocr", "caption", "objects"] as const : ["ocr", "caption", "objects"] as const;
|
|
460
525
|
const promptMetadata = defineToolPromptMetadata({
|
|
461
526
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
462
527
|
promptSnippet: "Read files or images with anchors, maps, symbols, and OCR",
|
|
@@ -467,7 +532,12 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
467
532
|
label: "Read",
|
|
468
533
|
description: promptMetadata.description,
|
|
469
534
|
promptSnippet: promptMetadata.promptSnippet,
|
|
470
|
-
promptGuidelines:
|
|
535
|
+
promptGuidelines: [
|
|
536
|
+
...promptMetadata.promptGuidelines,
|
|
537
|
+
imageMode === "off"
|
|
538
|
+
? "Image and PDF reads are disabled."
|
|
539
|
+
: `For an image or PDF, explicitly choose image: ${imageModes.join(", ")}; omitting image skips the file.`,
|
|
540
|
+
],
|
|
471
541
|
parameters: Type.Object({
|
|
472
542
|
path: filePathParam(),
|
|
473
543
|
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
@@ -479,6 +549,13 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
479
549
|
description: "Include same-file local support",
|
|
480
550
|
}),
|
|
481
551
|
),
|
|
552
|
+
...(imageMode === "off"
|
|
553
|
+
? {}
|
|
554
|
+
: {
|
|
555
|
+
image: Type.Optional(Type.Union(imageModes.map((mode) => Type.Literal(mode)), {
|
|
556
|
+
description: `Image/PDF mode: ${imageModes.join(", ")}. Must be selected explicitly.`,
|
|
557
|
+
})),
|
|
558
|
+
}),
|
|
482
559
|
}),
|
|
483
560
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
484
561
|
return executeRead({
|
|
@@ -488,7 +565,6 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
488
565
|
onUpdate,
|
|
489
566
|
cwd: ctx.cwd,
|
|
490
567
|
onSuccessfulRead: options.onSuccessfulRead,
|
|
491
|
-
modelSupportsImages: ctx.model?.input.includes("image") ?? false,
|
|
492
568
|
});
|
|
493
569
|
},
|
|
494
570
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
@@ -509,20 +585,28 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
509
585
|
},
|
|
510
586
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
511
587
|
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
512
|
-
if (isPartial) return renderPendingResult("pending read", width);
|
|
588
|
+
if (isPartial) return renderPendingResult("pending read", width, theme);
|
|
513
589
|
|
|
514
590
|
const content = result.content?.[0];
|
|
515
591
|
const textContent = content?.type === "text" ? content.text : "";
|
|
516
592
|
if (isError || result.isError) {
|
|
517
593
|
const firstLine = textContent.split("\n")[0] || "Error";
|
|
518
594
|
const errorText = expanded ? (textContent || firstLine) : firstLine;
|
|
519
|
-
return new Text(clampLinesToWidth([summaryLine(errorText)], width).join("\n"), 0, 0);
|
|
595
|
+
return new Text(clampLinesToWidth([summaryLine(errorText, { theme, style: "error" })], width).join("\n"), 0, 0);
|
|
520
596
|
}
|
|
521
597
|
|
|
522
598
|
const readSeekValue = (result.details as any)?.readSeekValue as { range: { startLine: number; endLine: number; totalLines: number }; truncation: any; symbol: any; map: any; warnings: ReadSeekWarning[] } | undefined;
|
|
523
599
|
if (!readSeekValue) {
|
|
524
600
|
const lines = textContent.split("\n").filter(Boolean).length || textContent.split("\n").length;
|
|
525
|
-
return new Text(
|
|
601
|
+
return new Text(
|
|
602
|
+
summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
|
|
603
|
+
hidden: !!textContent && !expanded,
|
|
604
|
+
theme,
|
|
605
|
+
style: "success",
|
|
606
|
+
}),
|
|
607
|
+
0,
|
|
608
|
+
0,
|
|
609
|
+
);
|
|
526
610
|
}
|
|
527
611
|
|
|
528
612
|
const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
|
|
@@ -532,7 +616,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
532
616
|
if (info.symbolBadge) summaryParts.push(info.symbolBadge);
|
|
533
617
|
for (const badge of info.badges) summaryParts.push(badge);
|
|
534
618
|
const summary = summaryParts.join(" • ");
|
|
535
|
-
let text = summaryLine(summary, { hidden: !!textContent && !expanded });
|
|
619
|
+
let text = summaryLine(summary, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
536
620
|
if (expanded && textContent) text += "\n" + wrapReadHashlinesForWidthCached(content, textContent, width);
|
|
537
621
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
538
622
|
},
|
package/src/readseek-client.ts
CHANGED
|
@@ -118,6 +118,32 @@ export interface ReadSeekDetectedObject {
|
|
|
118
118
|
|
|
119
119
|
export type ReadSeekImageMode = "ocr" | "caption" | "objects";
|
|
120
120
|
|
|
121
|
+
export interface ReadSeekPreparedImage {
|
|
122
|
+
mime: string;
|
|
123
|
+
encoding: "base64";
|
|
124
|
+
data: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ReadSeekPdfImage {
|
|
128
|
+
page: number;
|
|
129
|
+
width: number;
|
|
130
|
+
height: number;
|
|
131
|
+
mime: string;
|
|
132
|
+
mode: "none" | ReadSeekImageMode;
|
|
133
|
+
encoding?: "base64";
|
|
134
|
+
data?: string;
|
|
135
|
+
ocr?: ReadSeekOcrText;
|
|
136
|
+
caption?: string;
|
|
137
|
+
objects?: ReadSeekDetectedObject[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ReadSeekPdfOutput {
|
|
141
|
+
format: "pdf";
|
|
142
|
+
pages: number;
|
|
143
|
+
markdown: string;
|
|
144
|
+
images: ReadSeekPdfImage[];
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
export type ReadSeekDetection =
|
|
122
148
|
| {
|
|
123
149
|
kind: "source";
|
|
@@ -138,6 +164,8 @@ export type ReadSeekDetection =
|
|
|
138
164
|
width: number;
|
|
139
165
|
height: number;
|
|
140
166
|
animated: boolean;
|
|
167
|
+
encoding?: "base64";
|
|
168
|
+
data?: string;
|
|
141
169
|
ocr?: ReadSeekOcrText;
|
|
142
170
|
caption?: string;
|
|
143
171
|
objects?: ReadSeekDetectedObject[];
|
|
@@ -147,6 +175,14 @@ export type ReadSeekDetection =
|
|
|
147
175
|
type: string;
|
|
148
176
|
file: string;
|
|
149
177
|
mime?: string;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
kind: "pdf";
|
|
181
|
+
type: "application/pdf";
|
|
182
|
+
file: string;
|
|
183
|
+
mime?: string;
|
|
184
|
+
format: "pdf";
|
|
185
|
+
pages: number;
|
|
150
186
|
};
|
|
151
187
|
|
|
152
188
|
type ReadSeekImageDetection = Extract<ReadSeekDetection, { kind: "image" }>;
|
|
@@ -219,6 +255,7 @@ function readSeekPackageDir(): string {
|
|
|
219
255
|
|
|
220
256
|
const READSEEK_PLATFORM_PACKAGES: Record<string, string> = {
|
|
221
257
|
"darwin-arm64": "@jarkkojs/readseek-darwin-arm64",
|
|
258
|
+
"linux-arm64": "@jarkkojs/readseek-linux-arm64",
|
|
222
259
|
"linux-x64": "@jarkkojs/readseek-linux-x64",
|
|
223
260
|
"win32-x64": "@jarkkojs/readseek-win32-x64",
|
|
224
261
|
};
|
|
@@ -725,7 +762,18 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
725
762
|
// carry `format`/`width`/`height`/`animated`; source detections carry
|
|
726
763
|
// `language`. Text and binary are byte-identical on the wire and collapse
|
|
727
764
|
// to the text variant.
|
|
728
|
-
if (
|
|
765
|
+
if (type === "application/pdf") {
|
|
766
|
+
if (output.format !== "pdf") throw new Error("invalid readseek PDF format");
|
|
767
|
+
return {
|
|
768
|
+
kind: "pdf",
|
|
769
|
+
type,
|
|
770
|
+
file,
|
|
771
|
+
mime,
|
|
772
|
+
format: output.format,
|
|
773
|
+
pages: requireNumber(output.pages, "pages"),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (output.width !== undefined || output.height !== undefined) {
|
|
729
777
|
return {
|
|
730
778
|
kind: "image",
|
|
731
779
|
type,
|
|
@@ -735,6 +783,8 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
735
783
|
width: requireNumber(output.width, "width"),
|
|
736
784
|
height: requireNumber(output.height, "height"),
|
|
737
785
|
animated: requireBoolean(output.animated, "animated"),
|
|
786
|
+
encoding: output.encoding === undefined ? undefined : parseImageEncoding(output.encoding),
|
|
787
|
+
data: output.data === undefined ? undefined : requireString(output.data, "data"),
|
|
738
788
|
ocr: parseOcrText(output.ocr),
|
|
739
789
|
caption: optionalString(output.caption, "caption"),
|
|
740
790
|
objects: parseDetectedObjects(output.objects),
|
|
@@ -755,6 +805,11 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
755
805
|
return { kind: "text", type, file, mime };
|
|
756
806
|
}
|
|
757
807
|
|
|
808
|
+
function parseImageEncoding(value: unknown): "base64" {
|
|
809
|
+
if (value === "base64") return value;
|
|
810
|
+
throw new Error("invalid image encoding");
|
|
811
|
+
}
|
|
812
|
+
|
|
758
813
|
export async function readSeekDetect(
|
|
759
814
|
filePath: string,
|
|
760
815
|
options: { signal?: AbortSignal } = {},
|
|
@@ -799,6 +854,58 @@ export async function readSeekImage(
|
|
|
799
854
|
throw new Error(`readseek returned no image analysis for ${filePath}`);
|
|
800
855
|
}
|
|
801
856
|
|
|
857
|
+
export async function readSeekPreparedImage(
|
|
858
|
+
filePath: string,
|
|
859
|
+
options: { signal?: AbortSignal } = {},
|
|
860
|
+
): Promise<ReadSeekPreparedImage> {
|
|
861
|
+
const output = parseDetectOutput(await runReadSeek(["read", "--image", "none", filePath], { signal: options.signal }));
|
|
862
|
+
if (output.kind !== "image" || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
|
|
863
|
+
throw new Error(`readseek returned no prepared image for ${filePath}`);
|
|
864
|
+
}
|
|
865
|
+
return { mime: output.mime, encoding: output.encoding, data: output.data };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function parsePdfImage(value: unknown): ReadSeekPdfImage {
|
|
869
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek PDF image");
|
|
870
|
+
const image = value as Record<string, unknown>;
|
|
871
|
+
const mode = requireString(image.mode, "PDF image.mode");
|
|
872
|
+
if (mode !== "none" && mode !== "ocr" && mode !== "caption" && mode !== "objects") {
|
|
873
|
+
throw new Error("invalid readseek PDF image.mode");
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
page: requireNumber(image.page, "PDF image.page"),
|
|
877
|
+
width: requireNumber(image.width, "PDF image.width"),
|
|
878
|
+
height: requireNumber(image.height, "PDF image.height"),
|
|
879
|
+
mime: requireString(image.mime, "PDF image.mime"),
|
|
880
|
+
mode,
|
|
881
|
+
encoding: image.encoding === undefined ? undefined : parseImageEncoding(image.encoding),
|
|
882
|
+
data: optionalString(image.data, "PDF image.data"),
|
|
883
|
+
ocr: parseOcrText(image.ocr),
|
|
884
|
+
caption: optionalString(image.caption, "PDF image.caption"),
|
|
885
|
+
objects: parseDetectedObjects(image.objects),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function parsePdfOutput(value: unknown): ReadSeekPdfOutput {
|
|
890
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek PDF output");
|
|
891
|
+
const output = value as Record<string, unknown>;
|
|
892
|
+
if (output.format !== "pdf" || !Array.isArray(output.images)) throw new Error("invalid readseek PDF output");
|
|
893
|
+
return {
|
|
894
|
+
format: output.format,
|
|
895
|
+
pages: requireNumber(output.pages, "PDF pages"),
|
|
896
|
+
markdown: requireString(output.markdown, "PDF markdown"),
|
|
897
|
+
images: output.images.map(parsePdfImage),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export async function readSeekPdf(
|
|
902
|
+
filePath: string,
|
|
903
|
+
mode: "none" | ReadSeekImageMode,
|
|
904
|
+
options: { signal?: AbortSignal } = {},
|
|
905
|
+
): Promise<ReadSeekPdfOutput> {
|
|
906
|
+
return parsePdfOutput(await runReadSeek(["read", "--image", mode, filePath], { signal: options.signal }));
|
|
907
|
+
}
|
|
908
|
+
|
|
802
909
|
// --- Rename ---
|
|
803
910
|
|
|
804
911
|
export interface RenameConflict {
|
package/src/readseek-settings.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, statSync, type Stats } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
export type ReadSeekImageAnalysisMode = "
|
|
5
|
+
export type ReadSeekImageAnalysisMode = "on" | "off" | "auto";
|
|
6
6
|
export type ReadSeekSyntaxValidationMode = "warn" | "block" | "off";
|
|
7
7
|
interface ReadSeekGrepSettings {
|
|
8
8
|
maxLines?: number;
|
|
@@ -106,8 +106,7 @@ function readImageMode(
|
|
|
106
106
|
): ReadSeekImageAnalysisMode | undefined {
|
|
107
107
|
if (!("imageMode" in raw)) return undefined;
|
|
108
108
|
const val = raw.imageMode;
|
|
109
|
-
if (val === "on") return
|
|
110
|
-
if (val === "force" || val === "off" || val === "auto") return val;
|
|
109
|
+
if (val === "on" || val === "off" || val === "auto") return val;
|
|
111
110
|
warnings.push(invalid(source, "readseek.imageMode"));
|
|
112
111
|
return undefined;
|
|
113
112
|
}
|
|
@@ -252,7 +251,7 @@ export function resolveReadSeekJsonSettings(): ReadSeekSettingsResult {
|
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
export function resolveReadSeekImageMode(): ReadSeekImageAnalysisMode {
|
|
255
|
-
return resolveReadSeekJsonSettings().settings.imageMode ?? "
|
|
254
|
+
return resolveReadSeekJsonSettings().settings.imageMode ?? "auto";
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
export function resolveReadSeekSyntaxValidation(): ReadSeekSyntaxValidationMode | undefined {
|
package/src/rename.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
9
9
|
import { classifyReadSeekFailure, readSeekRename, type RenameOutput } from "./readseek-client.js";
|
|
10
10
|
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
11
11
|
|
|
12
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
12
|
+
import { clampLineToWidth, clampLinesToWidth, linkedPathLine, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
13
13
|
|
|
14
14
|
const RENAME_PROMPT_METADATA = defineToolPromptMetadata({
|
|
15
15
|
promptUrl: new URL("../prompts/rename.md", import.meta.url),
|
|
@@ -121,16 +121,15 @@ export function registerRenameTool(pi: ExtensionAPI) {
|
|
|
121
121
|
const context = rest[0] ?? {};
|
|
122
122
|
const cwd = context.cwd ?? process.cwd();
|
|
123
123
|
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
124
|
-
const oldName = typeof args?.to === "string" ? "" : "";
|
|
125
124
|
let text = theme.fg("toolTitle", theme.bold("rename"));
|
|
126
125
|
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
127
126
|
if (args?.to) text += theme.fg("dim", ` → ${args.to}`);
|
|
128
127
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
129
128
|
},
|
|
130
129
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
131
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
130
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
132
131
|
|
|
133
|
-
if (isPartial) return renderPendingResult("pending rename", width);
|
|
132
|
+
if (isPartial) return renderPendingResult("pending rename", width, theme);
|
|
134
133
|
|
|
135
134
|
const content = result.content?.[0];
|
|
136
135
|
const textContent = content?.type === "text" ? content.text : "";
|
|
@@ -138,21 +137,29 @@ export function registerRenameTool(pi: ExtensionAPI) {
|
|
|
138
137
|
const output = readSeekValue?.output as RenameOutput | undefined;
|
|
139
138
|
|
|
140
139
|
if (isError || result.isError) {
|
|
141
|
-
|
|
140
|
+
return renderErrorResult(textContent, { expanded, width, fallback: "rename failed", theme });
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const outputs = output ? [output, ...output.others] : [];
|
|
144
|
+
const editCount = outputs.reduce((total, file) => total + file.edits.length, 0);
|
|
145
|
+
const conflictCount = outputs.reduce((total, file) => total + file.conflicts.length, 0);
|
|
146
|
+
const fileCount = outputs.length;
|
|
147
|
+
const action = output?.applied
|
|
146
148
|
? `renamed ${output.old_name} → ${output.new_name}`
|
|
147
|
-
: `rename plan for ${output?.old_name ?? "?"} → ${output?.new_name ?? "?"} (dry-run)
|
|
148
|
-
|
|
149
|
+
: `rename plan for ${output?.old_name ?? "?"} → ${output?.new_name ?? "?"} (dry-run)`;
|
|
150
|
+
const details = output
|
|
151
|
+
? ` • ${editCount} ${editCount === 1 ? "edit" : "edits"} in ${fileCount} ${fileCount === 1 ? "file" : "files"}`
|
|
152
|
+
: "";
|
|
153
|
+
const conflicts = conflictCount > 0 ? ` • ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}` : "";
|
|
154
|
+
let text = summaryLine(`${action}${details}${conflicts}`, {
|
|
155
|
+
theme,
|
|
156
|
+
style: conflictCount > 0 ? "warning" : "success",
|
|
157
|
+
});
|
|
149
158
|
|
|
150
159
|
if (expanded && output) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
for (const f of files) {
|
|
155
|
-
text += `\n ${f}`;
|
|
160
|
+
for (const file of outputs) {
|
|
161
|
+
const display = path.relative(cwd, file.file) || file.file;
|
|
162
|
+
text += `\n${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`, width)}`;
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
|
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
const COMPACT_DESCRIPTIONS: Record<string, string> = {
|
|
6
|
-
"read.md": "Read text files
|
|
6
|
+
"read.md": "Read text files, images, or PDFs by path; text has LINE:HASH anchors and visual files require an explicit image mode.",
|
|
7
7
|
"edit.md": "Edit existing text files using fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write.",
|
|
8
8
|
"grep.md": "Search file contents; non-summary results include LINE:HASH anchors for edits.",
|
|
9
9
|
|
|
@@ -38,7 +38,7 @@ const REPLACEABLE_TOOL_GUIDELINES: Record<string, { readSeekName: string; builtI
|
|
|
38
38
|
const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
39
39
|
"read.md": [
|
|
40
40
|
"Use readSeek_read map or symbol mode before pulling large code files into context.",
|
|
41
|
-
"
|
|
41
|
+
"For images and PDFs, explicitly choose one of the image modes exposed by readSeek_read.",
|
|
42
42
|
],
|
|
43
43
|
"edit.md": [
|
|
44
44
|
"With readSeek_edit, prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical.",
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -28,12 +28,30 @@ export function linkToolPath(styledText: string, rawPath: string, cwd: string):
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function linkedPathLine(
|
|
32
|
+
theme: RendererTheme,
|
|
33
|
+
rawPath: string,
|
|
34
|
+
displayPath: string,
|
|
35
|
+
cwd: string,
|
|
36
|
+
suffix: string,
|
|
37
|
+
width: number | undefined,
|
|
38
|
+
): string {
|
|
39
|
+
const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
|
|
40
|
+
const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
|
|
41
|
+
const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
|
|
42
|
+
return ` ${linkedPath}${theme.fg("dim", suffix)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export function appendExpandHint(text: string, hidden: boolean): string {
|
|
32
46
|
return hidden ? `${text}${EXPAND_HINT}` : text;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
export function summaryLine(
|
|
36
|
-
|
|
49
|
+
export function summaryLine(
|
|
50
|
+
summary: string,
|
|
51
|
+
options: { hidden?: boolean; theme?: RendererTheme; style?: string } = {},
|
|
52
|
+
): string {
|
|
53
|
+
const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
|
|
54
|
+
return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
export function isRendererExpanded(options?: { expanded?: boolean }, context?: { expanded?: boolean }): boolean {
|
|
@@ -172,8 +190,8 @@ export function wrapReadHashlinesForWidthCached(
|
|
|
172
190
|
/**
|
|
173
191
|
* Render the `isPartial` placeholder line shared by every tool's `renderResult`.
|
|
174
192
|
*/
|
|
175
|
-
export function renderPendingResult(pendingLabel: string, width: number | undefined): Text {
|
|
176
|
-
return new Text(clampLinesToWidth([summaryLine(pendingLabel)], width).join("\n"), 0, 0);
|
|
193
|
+
export function renderPendingResult(pendingLabel: string, width: number | undefined, theme?: RendererTheme): Text {
|
|
194
|
+
return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join("\n"), 0, 0);
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
/**
|
|
@@ -183,11 +201,15 @@ export function renderPendingResult(pendingLabel: string, width: number | undefi
|
|
|
183
201
|
*/
|
|
184
202
|
export function renderErrorResult(
|
|
185
203
|
textContent: string,
|
|
186
|
-
options: { expanded: boolean; width: number | undefined; fallback?: string },
|
|
204
|
+
options: { expanded: boolean; width: number | undefined; fallback?: string; theme?: RendererTheme },
|
|
187
205
|
): Text {
|
|
188
206
|
const firstLine = textContent.split("\n")[0] || (options.fallback ?? "Error");
|
|
189
207
|
const body = options.expanded && textContent ? textContent : firstLine;
|
|
190
|
-
return new Text(
|
|
208
|
+
return new Text(
|
|
209
|
+
clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split("\n"), options.width).join("\n"),
|
|
210
|
+
0,
|
|
211
|
+
0,
|
|
212
|
+
);
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
export interface AnchoredFilesLabels {
|
|
@@ -232,27 +254,31 @@ export function renderAnchoredFilesResult(
|
|
|
232
254
|
): Text {
|
|
233
255
|
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
234
256
|
|
|
235
|
-
if (isPartial) return renderPendingResult(labels.pendingLabel, width);
|
|
257
|
+
if (isPartial) return renderPendingResult(labels.pendingLabel, width, theme);
|
|
236
258
|
|
|
237
259
|
const content = result.content?.[0];
|
|
238
260
|
const textContent = content?.type === "text" ? content.text : "";
|
|
239
|
-
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
|
|
261
|
+
if (isError || result.isError) return renderErrorResult(textContent, { expanded, width, theme });
|
|
240
262
|
|
|
241
263
|
const readSeekValue = (result.details as any)?.readSeekValue as
|
|
242
264
|
| { files: Array<{ path: string; lines: any[] }> }
|
|
243
265
|
| undefined;
|
|
244
266
|
const files = readSeekValue?.files ?? [];
|
|
245
|
-
if (files.length === 0) return new Text(summaryLine(labels.emptyLabel), 0, 0);
|
|
267
|
+
if (files.length === 0) return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
|
|
246
268
|
|
|
247
269
|
const fileCount = files.length;
|
|
248
270
|
const total = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
|
|
249
271
|
const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
|
|
250
272
|
const fileWord = fileCount === 1 ? "file" : "files";
|
|
251
|
-
let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
|
|
273
|
+
let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
|
|
274
|
+
hidden: !expanded,
|
|
275
|
+
theme,
|
|
276
|
+
style: "success",
|
|
277
|
+
});
|
|
252
278
|
if (expanded) {
|
|
253
279
|
for (const file of files.slice(0, 20)) {
|
|
254
280
|
const display = relative(cwd, file.path) || file.path;
|
|
255
|
-
text +=
|
|
281
|
+
text += `\n${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
|
|
256
282
|
}
|
|
257
283
|
if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
258
284
|
}
|
package/src/write.ts
CHANGED
|
@@ -322,11 +322,11 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
322
322
|
},
|
|
323
323
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
324
324
|
const { isPartial, expanded, width, context } = resolveRenderResultContext(options, rest);
|
|
325
|
-
if (isPartial) return renderPendingResult("pending write", width);
|
|
325
|
+
if (isPartial) return renderPendingResult("pending write", width, theme);
|
|
326
326
|
const details = result.details ?? {};
|
|
327
327
|
const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
328
328
|
if (result.isError || details.readSeekValue?.ok === false) {
|
|
329
|
-
return renderErrorResult(output, { expanded, width, fallback: "write failed" });
|
|
329
|
+
return renderErrorResult(output, { expanded, width, fallback: "write failed", theme });
|
|
330
330
|
}
|
|
331
331
|
const diffData = details.diffData;
|
|
332
332
|
const state = details.writeState === "overwritten" ? "overwritten" : "created";
|
|
@@ -336,7 +336,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
336
336
|
if (state === "created") {
|
|
337
337
|
const readSeekLines = (details.readSeekValue?.lines ?? []) as Array<{ raw: string }>;
|
|
338
338
|
const hasContent = readSeekLines.length > 0;
|
|
339
|
-
const header = summaryLine(state, { hidden: hasContent && !expanded });
|
|
339
|
+
const header = summaryLine(state, { hidden: hasContent && !expanded, theme, style: "success" });
|
|
340
340
|
const lines = header.split("\n");
|
|
341
341
|
if (expanded && hasContent) {
|
|
342
342
|
const content = readSeekLines.map((l) => l.raw).join("\n");
|
|
@@ -346,7 +346,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
346
346
|
}
|
|
347
347
|
// Overwrite: the old vs new comparison still carries signal — keep the diff UI.
|
|
348
348
|
const hasExpandableDiff = !!diffData;
|
|
349
|
-
let text = summaryLine(state, { hidden: hasExpandableDiff && !expanded });
|
|
349
|
+
let text = summaryLine(state, { hidden: hasExpandableDiff && !expanded, theme, style: "success" });
|
|
350
350
|
if (expanded && hasExpandableDiff) {
|
|
351
351
|
return upsertDiffComponent(context.lastComponent, { prefixLines: text.split("\n"), diffData, theme, expanded: true });
|
|
352
352
|
}
|