@oh-my-pi/pi-coding-agent 13.7.1 → 13.7.4
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 +41 -0
- package/package.json +7 -7
- package/src/cli.ts +29 -1
- package/src/config/prompt-templates.ts +3 -12
- package/src/config/settings-schema.ts +9 -0
- package/src/debug/log-formatting.ts +2 -4
- package/src/debug/log-viewer.ts +3 -3
- package/src/exa/factory.ts +1 -1
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/websets.ts +1 -1
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/tool-execution.ts +5 -5
- package/src/modes/controllers/command-controller.ts +4 -4
- package/src/modes/controllers/input-controller.ts +11 -12
- package/src/modes/controllers/selector-controller.ts +98 -94
- package/src/modes/interactive-mode.ts +4 -4
- package/src/modes/types.ts +3 -3
- package/src/patch/hashline.ts +15 -12
- package/src/patch/index.ts +5 -4
- package/src/prompts/tools/hashline.md +148 -66
- package/src/slash-commands/builtin-registry.ts +17 -1
- package/src/task/executor.ts +1 -1
- package/src/task/render.ts +3 -1
- package/src/tools/fetch.ts +30 -9
- package/src/tools/submit-result.ts +4 -51
- package/src/tui/output-block.ts +7 -8
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/kagi.ts +161 -0
- package/src/web/scrapers/youtube.ts +22 -4
- package/src/web/search/index.ts +16 -39
- package/src/web/search/providers/kagi.ts +26 -104
package/src/patch/hashline.ts
CHANGED
|
@@ -412,10 +412,14 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function isEscapedTabAutocorrectEnabled(): boolean {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
switch (Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS) {
|
|
416
|
+
case "0":
|
|
417
|
+
return false;
|
|
418
|
+
case "1":
|
|
419
|
+
return true;
|
|
420
|
+
default:
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
419
423
|
}
|
|
420
424
|
|
|
421
425
|
function maybeAutocorrectEscapedTabIndentation(edits: HashlineEdit[], warnings: string[]): void {
|
|
@@ -623,20 +627,19 @@ export function applyHashlineEdits(
|
|
|
623
627
|
} else {
|
|
624
628
|
const count = edit.end.line - edit.pos.line + 1;
|
|
625
629
|
const newLines = [...edit.lines];
|
|
626
|
-
const trailingReplacementLine = newLines[newLines.length - 1];
|
|
627
|
-
const nextSurvivingLine = fileLines[edit.end.line];
|
|
630
|
+
const trailingReplacementLine = newLines[newLines.length - 1]?.trimEnd();
|
|
631
|
+
const nextSurvivingLine = fileLines[edit.end.line]?.trimEnd();
|
|
628
632
|
if (
|
|
629
|
-
trailingReplacementLine
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
trailingReplacementLine.trim() === nextSurvivingLine.trim() &&
|
|
633
|
+
trailingReplacementLine &&
|
|
634
|
+
nextSurvivingLine &&
|
|
635
|
+
trailingReplacementLine === nextSurvivingLine &&
|
|
633
636
|
// Safety: only correct when end-line content differs from the duplicate.
|
|
634
637
|
// If end already points to the boundary, matching next line is coincidence.
|
|
635
|
-
fileLines[edit.end.line - 1]
|
|
638
|
+
fileLines[edit.end.line - 1]?.trimEnd() !== trailingReplacementLine
|
|
636
639
|
) {
|
|
637
640
|
newLines.pop();
|
|
638
641
|
warnings.push(
|
|
639
|
-
`Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine
|
|
642
|
+
`Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine}" that duplicated next surviving line`,
|
|
640
643
|
);
|
|
641
644
|
}
|
|
642
645
|
fileLines.splice(edit.pos.line - 1, count, ...newLines);
|
package/src/patch/index.ts
CHANGED
|
@@ -129,10 +129,11 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
129
129
|
|
|
130
130
|
export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
131
131
|
if (edit === null) return [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
if (typeof edit === "string") {
|
|
133
|
+
const normalizedEdit = edit.endsWith("\n") ? edit.slice(0, -1) : edit;
|
|
134
|
+
edit = normalizedEdit.replaceAll("\r", "").split("\n");
|
|
135
|
+
}
|
|
136
|
+
return stripNewLinePrefixes(edit);
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const hashlineEditSchema = Type.Object(
|
|
@@ -6,49 +6,31 @@ Applies precise file edits using `LINE#ID` tags from `read` output.
|
|
|
6
6
|
3. You **MUST** submit one `edit` call per file with all operations, think your changes through before submitting.
|
|
7
7
|
</workflow>
|
|
8
8
|
|
|
9
|
-
<prohibited>
|
|
10
|
-
You **MUST NOT** use this tool for formatting-only edits: reindenting, realigning, brace-style changes, whitespace normalization, or line-length wrapping. Any edit whose diff is purely whitespace is a formatting operation — run the appropriate formatter for the project instead.
|
|
11
|
-
</prohibited>
|
|
12
|
-
|
|
13
9
|
<operations>
|
|
14
|
-
|
|
15
|
-
**`
|
|
10
|
+
**`path`** — the path to the file to edit.
|
|
11
|
+
**`move`** — if set, move the file to the given path.
|
|
12
|
+
**`delete`** — if true, delete the file.
|
|
13
|
+
**`edits.[n].pos`** — the anchor line. Meaning depends on `op`:
|
|
16
14
|
- `replace`: start of range (or the single line to replace)
|
|
17
15
|
- `prepend`: insert new lines **before** this line; omit for beginning of file
|
|
18
16
|
- `append`: insert new lines **after** this line; omit for end of file
|
|
19
|
-
**`end`** — range replace only. The last line of the range (inclusive). Omit for single-line replace.
|
|
20
|
-
**`lines`** — the replacement content:
|
|
17
|
+
**`edits.[n].end`** — range replace only. The last line of the range (inclusive). Omit for single-line replace.
|
|
18
|
+
**`edits.[n].lines`** — the replacement content:
|
|
21
19
|
- `["line1", "line2"]` — replace with these lines (array of strings)
|
|
22
20
|
- `"line1"` — shorthand for `["line1"]` (single-line replace)
|
|
23
21
|
- `[""]` — replace content with a blank line (line preserved, content cleared)
|
|
24
22
|
- `null` or `[]` — **delete** the line(s) entirely
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- `{ path: "…", edits: [{ op: "replace", pos: "N#ID", end: "M#ID", lines: null }] }` — delete a range
|
|
29
|
-
- `{ path: "…", edits: [{ op: "replace", pos: "N#ID", lines: […] }] }` — replace one line
|
|
30
|
-
- `{ path: "…", edits: [{ op: "replace", pos: "N#ID", end: "M#ID", lines: […] }] }` — replace a range
|
|
31
|
-
|
|
32
|
-
### Insert new lines
|
|
33
|
-
- `{ path: "…", edits: [{ op: "prepend", pos: "N#ID", lines: […] }] }` — insert before tagged line
|
|
34
|
-
- `{ path: "…", edits: [{ op: "prepend", lines: […] }] }` — insert at beginning of file (no tag)
|
|
35
|
-
- `{ path: "…", edits: [{ op: "append", pos: "N#ID", lines: […] }] }` — insert after tagged line
|
|
36
|
-
- `{ path: "…", edits: [{ op: "append", lines: […] }] }` — insert at end of file (no tag)
|
|
37
|
-
|
|
38
|
-
### File-level controls
|
|
39
|
-
- `{ path: "…", delete: true, edits: [] }` — delete the file
|
|
40
|
-
- `{ path: "…", move: "new/path.ts", edits: […] }` — move file to new path (edits applied first)
|
|
41
|
-
**Atomicity:** all ops in one call validate against the same pre-edit snapshot; tags reference the last `read`. Edits are applied bottom-up, so earlier tags stay valid even when later ops add or remove lines.
|
|
24
|
+
Tags should be referenced from the last `read` output.
|
|
25
|
+
Edits are applied bottom-up, so earlier tags stay valid even when later ops add or remove lines.
|
|
42
26
|
</operations>
|
|
43
27
|
|
|
44
28
|
<rules>
|
|
45
29
|
1. **Minimize scope:** You **MUST** use one logical mutation per operation.
|
|
46
|
-
2.
|
|
47
|
-
3. **
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
- Does the first `lines` entry duplicate the line before `pos`? → the edit is wrong.
|
|
51
|
-
- For `prepend`/`append`: does new code land inside or outside the enclosing block? Trace the braces.
|
|
30
|
+
2. **Prefer insertion over neighbor rewrites:** You **SHOULD** anchor on structural boundaries (`}`, `]`, `},`), not interior lines.
|
|
31
|
+
3. **Range end tag (inclusive):** `end` is inclusive and **MUST** point to the final line being replaced.
|
|
32
|
+
- If `lines` includes a closing boundary token (`}`, `]`, `)`, `);`, `},`), `end` **MUST** include the original boundary line.
|
|
33
|
+
- You **MUST NOT** set `end` to an interior line and then re-add the boundary token in `lines`; that duplicates the next surviving line.
|
|
52
34
|
</rules>
|
|
53
35
|
|
|
54
36
|
<recovery>
|
|
@@ -61,18 +43,56 @@ Every edit has `op`, `pos`, and `lines`. Range replaces also have `end`. Both `p
|
|
|
61
43
|
{{hlinefull 23 " const timeout: number = 5000;"}}
|
|
62
44
|
```
|
|
63
45
|
```
|
|
64
|
-
{
|
|
46
|
+
{
|
|
47
|
+
path: "…",
|
|
48
|
+
edits: [{
|
|
49
|
+
op: "replace",
|
|
50
|
+
pos: {{hlineref 23 " const timeout: number = 5000;"}},
|
|
51
|
+
lines: [" const timeout: number = 30_000;"]
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
65
54
|
```
|
|
66
55
|
</example>
|
|
67
56
|
|
|
68
57
|
<example name="delete lines">
|
|
69
58
|
Single line — `lines: null` deletes entirely:
|
|
70
59
|
```
|
|
71
|
-
{
|
|
60
|
+
{
|
|
61
|
+
path: "…",
|
|
62
|
+
edits: [{
|
|
63
|
+
op: "replace",
|
|
64
|
+
pos: {{hlineref 7 "// @ts-ignore"}},
|
|
65
|
+
lines: null
|
|
66
|
+
}]
|
|
67
|
+
}
|
|
72
68
|
```
|
|
73
69
|
Range — add `end`:
|
|
74
70
|
```
|
|
75
|
-
{
|
|
71
|
+
{
|
|
72
|
+
path: "…",
|
|
73
|
+
edits: [{
|
|
74
|
+
op: "replace",
|
|
75
|
+
pos: {{hlineref 80 " // TODO: remove after migration"}},
|
|
76
|
+
end: {{hlineref 83 " }"}},
|
|
77
|
+
lines: null
|
|
78
|
+
}]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
</example>
|
|
82
|
+
|
|
83
|
+
<example name="clear text but keep the line break">
|
|
84
|
+
```ts
|
|
85
|
+
{{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
|
|
86
|
+
```
|
|
87
|
+
```
|
|
88
|
+
{
|
|
89
|
+
path: "…",
|
|
90
|
+
edits: [{
|
|
91
|
+
op: "replace",
|
|
92
|
+
pos: {{hlineref 14 " placeholder: \"DO NOT SHIP\","}},
|
|
93
|
+
lines: [""]
|
|
94
|
+
}]
|
|
95
|
+
}
|
|
76
96
|
```
|
|
77
97
|
</example>
|
|
78
98
|
|
|
@@ -83,37 +103,63 @@ Range — add `end`:
|
|
|
83
103
|
{{hlinefull 62 " return null;"}}
|
|
84
104
|
{{hlinefull 63 " }"}}
|
|
85
105
|
```
|
|
86
|
-
Include the closing `}` in the replaced range — stopping one line short orphans the brace or duplicates it.
|
|
87
106
|
```
|
|
88
|
-
{
|
|
107
|
+
{
|
|
108
|
+
path: "…",
|
|
109
|
+
edits: [{
|
|
110
|
+
op: "replace",
|
|
111
|
+
pos: {{hlineref 61 " console.error(err);"}},
|
|
112
|
+
end: {{hlineref 63 " }"}},
|
|
113
|
+
lines: [
|
|
114
|
+
" if (isEnoent(err)) return null;",
|
|
115
|
+
" throw err;",
|
|
116
|
+
" }"
|
|
117
|
+
]
|
|
118
|
+
}]
|
|
119
|
+
}
|
|
89
120
|
```
|
|
90
121
|
</example>
|
|
91
122
|
|
|
92
|
-
<example name="
|
|
93
|
-
Adding a method inside a class — anchor on the **closing brace**, not after it.
|
|
123
|
+
<example name="inclusive end avoids duplicate boundary">
|
|
94
124
|
```ts
|
|
95
|
-
{{hlinefull
|
|
96
|
-
{{hlinefull
|
|
97
|
-
{{hlinefull
|
|
98
|
-
{{hlinefull
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
{{hlinefull 70 "if (ok) {"}}
|
|
126
|
+
{{hlinefull 71 " run();"}}
|
|
127
|
+
{{hlinefull 72 "}"}}
|
|
128
|
+
{{hlinefull 73 "after();"}}
|
|
129
|
+
```
|
|
130
|
+
Bad — `end` stops before `}` while `lines` already includes `}`:
|
|
131
|
+
```
|
|
132
|
+
{
|
|
133
|
+
path: "…",
|
|
134
|
+
edits: [{
|
|
135
|
+
op: "replace",
|
|
136
|
+
pos: {{hlineref 70 "if (ok) {"}},
|
|
137
|
+
end: {{hlineref 71 " run();"}},
|
|
138
|
+
lines: [
|
|
139
|
+
"if (ok) {",
|
|
140
|
+
" runSafe();",
|
|
141
|
+
"}"
|
|
142
|
+
]
|
|
143
|
+
}]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
Good — include original `}` in the replaced range when replacement keeps `}`:
|
|
147
|
+
```
|
|
148
|
+
{
|
|
149
|
+
path: "…",
|
|
150
|
+
edits: [{
|
|
151
|
+
op: "replace",
|
|
152
|
+
pos: {{hlineref 70 "if (ok) {"}},
|
|
153
|
+
end: {{hlineref 72 "}"}},
|
|
154
|
+
lines: [
|
|
155
|
+
"if (ok) {",
|
|
156
|
+
" runSafe();",
|
|
157
|
+
"}"
|
|
158
|
+
]
|
|
159
|
+
}]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
Also apply the same rule to `);`, `],`, and `},` closers: if replacement includes the closer token, `end` must include the original closer line.
|
|
117
163
|
</example>
|
|
118
164
|
|
|
119
165
|
<example name="insert between sibling declarations">
|
|
@@ -126,26 +172,62 @@ Good — prepends **before** closing `}` (method stays inside the class):
|
|
|
126
172
|
{{hlinefull 49 " runY();"}}
|
|
127
173
|
{{hlinefull 50 "}"}}
|
|
128
174
|
```
|
|
129
|
-
Use a trailing `""` to preserve the blank line between top-level sibling declarations.
|
|
130
175
|
```
|
|
131
|
-
{
|
|
176
|
+
{
|
|
177
|
+
path: "…",
|
|
178
|
+
edits: [{
|
|
179
|
+
op: "prepend",
|
|
180
|
+
pos: {{hlineref 48 "function y() {"}},
|
|
181
|
+
lines: [
|
|
182
|
+
"function z() {",
|
|
183
|
+
" runZ();",
|
|
184
|
+
"}",
|
|
185
|
+
""
|
|
186
|
+
]
|
|
187
|
+
}]
|
|
188
|
+
}
|
|
132
189
|
```
|
|
190
|
+
Use a trailing `""` to preserve the blank line between top-level sibling declarations.
|
|
133
191
|
</example>
|
|
134
192
|
|
|
135
193
|
<example name="disambiguate anchors">
|
|
136
194
|
Blank lines and repeated patterns (`}`, `return null;`) appear many times — never anchor on them when a unique line exists nearby.
|
|
137
195
|
```ts
|
|
138
|
-
{{hlinefull
|
|
139
|
-
{{hlinefull
|
|
140
|
-
{{hlinefull
|
|
196
|
+
{{hlinefull 101 "}"}}
|
|
197
|
+
{{hlinefull 102 ""}}
|
|
198
|
+
{{hlinefull 103 "export function serialize(data: unknown): string {"}}
|
|
141
199
|
```
|
|
142
200
|
Bad — anchoring on the blank line (ambiguous, may shift):
|
|
143
201
|
```
|
|
144
|
-
{
|
|
202
|
+
{
|
|
203
|
+
path: "…",
|
|
204
|
+
edits: [{
|
|
205
|
+
op: "append",
|
|
206
|
+
pos: {{hlineref 102 ""}},
|
|
207
|
+
lines: [
|
|
208
|
+
"function validate(data: unknown): boolean {",
|
|
209
|
+
" return data != null && typeof data === \"object\";",
|
|
210
|
+
"}",
|
|
211
|
+
""
|
|
212
|
+
]
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
145
215
|
```
|
|
146
216
|
Good — anchor on the unique declaration line:
|
|
147
217
|
```
|
|
148
|
-
{
|
|
218
|
+
{
|
|
219
|
+
path: "…",
|
|
220
|
+
edits: [{
|
|
221
|
+
op: "prepend",
|
|
222
|
+
pos: {{hlineref 103 "export function serialize(data: unknown): string {"}},
|
|
223
|
+
lines: [
|
|
224
|
+
"function validate(data: unknown): boolean {",
|
|
225
|
+
" return data != null && typeof data === \"object\";",
|
|
226
|
+
"}",
|
|
227
|
+
""
|
|
228
|
+
]
|
|
229
|
+
}]
|
|
230
|
+
}
|
|
149
231
|
```
|
|
150
232
|
</example>
|
|
151
233
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai";
|
|
1
2
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
2
3
|
import { settings } from "../config/settings";
|
|
3
4
|
import type { InteractiveModeContext } from "../modes/types";
|
|
@@ -261,12 +262,27 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
261
262
|
{
|
|
262
263
|
name: "login",
|
|
263
264
|
description: "Login with OAuth provider",
|
|
264
|
-
inlineHint: "[redirect URL]",
|
|
265
|
+
inlineHint: "[provider|redirect URL]",
|
|
265
266
|
allowArgs: true,
|
|
266
267
|
handle: (command, runtime) => {
|
|
267
268
|
const manualInput = runtime.ctx.oauthManualInput;
|
|
268
269
|
const args = command.args.trim();
|
|
269
270
|
if (args.length > 0) {
|
|
271
|
+
const matchedProvider = getOAuthProviders().find(provider => provider.id === args);
|
|
272
|
+
if (matchedProvider) {
|
|
273
|
+
if (manualInput.hasPending()) {
|
|
274
|
+
const pendingProvider = manualInput.pendingProviderId;
|
|
275
|
+
const message = pendingProvider
|
|
276
|
+
? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
|
|
277
|
+
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
278
|
+
runtime.ctx.showWarning(message);
|
|
279
|
+
runtime.ctx.editor.setText("");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
void runtime.ctx.showOAuthSelector("login", matchedProvider.id);
|
|
283
|
+
runtime.ctx.editor.setText("");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
270
286
|
const submitted = manualInput.submit(args);
|
|
271
287
|
if (submitted) {
|
|
272
288
|
runtime.ctx.showStatus("OAuth callback received; completing login…");
|
package/src/task/executor.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "./types";
|
|
42
42
|
|
|
43
43
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
44
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
44
|
+
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
45
45
|
|
|
46
46
|
/** Agent event types to forward for progress tracking. */
|
|
47
47
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
package/src/task/render.ts
CHANGED
|
@@ -879,7 +879,9 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
879
879
|
|
|
880
880
|
// Error message
|
|
881
881
|
if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
|
|
882
|
-
lines.push(
|
|
882
|
+
lines.push(
|
|
883
|
+
`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(replaceTabs(result.error), 70))}`,
|
|
884
|
+
);
|
|
883
885
|
}
|
|
884
886
|
|
|
885
887
|
return lines;
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
4
|
-
import type
|
|
5
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
6
5
|
import { ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
7
6
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
7
|
import { parseHTML } from "linkedom";
|
|
@@ -14,6 +13,7 @@ import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
|
|
|
14
13
|
import { renderStatusLine } from "../tui";
|
|
15
14
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
15
|
import { ensureTool } from "../utils/tools-manager";
|
|
16
|
+
import { summarizeUrlWithKagi } from "../web/kagi";
|
|
17
17
|
import { specialHandlers } from "../web/scrapers";
|
|
18
18
|
import type { RenderResult } from "../web/scrapers/types";
|
|
19
19
|
import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
@@ -423,12 +423,13 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
/**
|
|
426
|
-
* Render HTML to markdown using
|
|
426
|
+
* Render HTML to markdown using kagi, jina, trafilatura, lynx (in order of preference)
|
|
427
427
|
*/
|
|
428
428
|
async function renderHtmlToText(
|
|
429
429
|
url: string,
|
|
430
430
|
html: string,
|
|
431
431
|
timeout: number,
|
|
432
|
+
useKagiSummarizer: boolean,
|
|
432
433
|
userSignal?: AbortSignal,
|
|
433
434
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
434
435
|
const signal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
@@ -440,7 +441,20 @@ async function renderHtmlToText(
|
|
|
440
441
|
signal,
|
|
441
442
|
};
|
|
442
443
|
|
|
443
|
-
// Try
|
|
444
|
+
// Try Kagi Universal Summarizer first (if enabled and KAGI_API_KEY is configured)
|
|
445
|
+
if (useKagiSummarizer) {
|
|
446
|
+
try {
|
|
447
|
+
const kagiSummary = await summarizeUrlWithKagi(url, { signal });
|
|
448
|
+
if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) {
|
|
449
|
+
return { content: kagiSummary, ok: true, method: "kagi" };
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
// Kagi failed, continue to next method
|
|
453
|
+
signal?.throwIfAborted();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Try jina next (reader API)
|
|
444
458
|
try {
|
|
445
459
|
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
446
460
|
const response = await fetch(jinaUrl, {
|
|
@@ -553,7 +567,13 @@ async function handleSpecialUrls(url: string, timeout: number, signal?: AbortSig
|
|
|
553
567
|
/**
|
|
554
568
|
* Main render function implementing the full pipeline
|
|
555
569
|
*/
|
|
556
|
-
async function renderUrl(
|
|
570
|
+
async function renderUrl(
|
|
571
|
+
url: string,
|
|
572
|
+
timeout: number,
|
|
573
|
+
raw: boolean,
|
|
574
|
+
useKagiSummarizer: boolean,
|
|
575
|
+
signal?: AbortSignal,
|
|
576
|
+
): Promise<RenderResult> {
|
|
557
577
|
const notes: string[] = [];
|
|
558
578
|
const fetchedAt = new Date().toISOString();
|
|
559
579
|
if (signal?.aborted) {
|
|
@@ -792,7 +812,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
|
|
|
792
812
|
}
|
|
793
813
|
|
|
794
814
|
// Step 6: Render HTML with lynx or html2text
|
|
795
|
-
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
|
|
815
|
+
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, useKagiSummarizer, signal);
|
|
796
816
|
if (!htmlResult.ok) {
|
|
797
817
|
notes.push("html rendering failed (lynx/html2text unavailable)");
|
|
798
818
|
const output = finalizeOutput(rawContent);
|
|
@@ -915,7 +935,8 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
915
935
|
throw new ToolAbortError();
|
|
916
936
|
}
|
|
917
937
|
|
|
918
|
-
const
|
|
938
|
+
const useKagiSummarizer = this.session.settings.get("fetch.useKagiSummarizer");
|
|
939
|
+
const result = await renderUrl(url, effectiveTimeout, raw, useKagiSummarizer, signal);
|
|
919
940
|
const truncation = truncateHead(result.content, {
|
|
920
941
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
921
942
|
maxLines: FETCH_DEFAULT_MAX_LINES,
|
|
@@ -1066,8 +1087,8 @@ export function renderFetchResult(
|
|
|
1066
1087
|
if (contentPreviewLines === undefined || lastExpanded !== expanded) {
|
|
1067
1088
|
const previewLimit = expanded ? 12 : 3;
|
|
1068
1089
|
const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
|
|
1069
|
-
const previewLines = previewList.items.map(line =>
|
|
1070
|
-
const remaining = Math.max(0, contentLines.length -
|
|
1090
|
+
const previewLines = previewList.items.map(line => line.trimEnd());
|
|
1091
|
+
const remaining = Math.max(0, contentLines.length - previewList.items.length);
|
|
1071
1092
|
contentPreviewLines =
|
|
1072
1093
|
previewLines.length > 0
|
|
1073
1094
|
? previewLines.map(line => uiTheme.fg("dim", line))
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Subagents must call this tool to finish and return structured JSON output.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
|
-
import { sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/schema";
|
|
7
|
+
import { dereferenceJsonSchema, sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/schema";
|
|
8
8
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
@@ -18,7 +18,7 @@ export interface SubmitResultDetails {
|
|
|
18
18
|
error?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
21
|
+
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
22
22
|
|
|
23
23
|
function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
24
24
|
if (schema === undefined || schema === null) return {};
|
|
@@ -52,53 +52,6 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
|
|
|
52
52
|
.join("; ");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* Resolve all $ref references in a JSON Schema by inlining definitions.
|
|
57
|
-
* Handles $defs and definitions at any nesting level.
|
|
58
|
-
* Removes $defs/definitions from the output since all refs are inlined.
|
|
59
|
-
*/
|
|
60
|
-
function resolveSchemaRefs(schema: Record<string, unknown>): Record<string, unknown> {
|
|
61
|
-
const defs: Record<string, Record<string, unknown>> = {};
|
|
62
|
-
const defsObj = schema.$defs ?? schema.definitions;
|
|
63
|
-
if (defsObj && typeof defsObj === "object" && !Array.isArray(defsObj)) {
|
|
64
|
-
for (const [name, def] of Object.entries(defsObj as Record<string, unknown>)) {
|
|
65
|
-
if (def && typeof def === "object" && !Array.isArray(def)) {
|
|
66
|
-
defs[name] = def as Record<string, unknown>;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (Object.keys(defs).length === 0) return schema;
|
|
71
|
-
|
|
72
|
-
const inlining = new Set<string>();
|
|
73
|
-
function inline(node: unknown): unknown {
|
|
74
|
-
if (node === null || typeof node !== "object") return node;
|
|
75
|
-
if (Array.isArray(node)) return node.map(inline);
|
|
76
|
-
const obj = node as Record<string, unknown>;
|
|
77
|
-
const ref = obj.$ref;
|
|
78
|
-
if (typeof ref === "string") {
|
|
79
|
-
const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
80
|
-
if (match) {
|
|
81
|
-
const name = match[1];
|
|
82
|
-
const def = defs[name];
|
|
83
|
-
if (def) {
|
|
84
|
-
if (inlining.has(name)) return {};
|
|
85
|
-
inlining.add(name);
|
|
86
|
-
const resolved = inline(def);
|
|
87
|
-
inlining.delete(name);
|
|
88
|
-
return resolved;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const result: Record<string, unknown> = {};
|
|
93
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
94
|
-
if (key === "$defs" || key === "definitions") continue;
|
|
95
|
-
result[key] = inline(value);
|
|
96
|
-
}
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
return inline(schema) as Record<string, unknown>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
55
|
export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails> {
|
|
103
56
|
readonly name = "submit_result";
|
|
104
57
|
readonly label = "Submit Result";
|
|
@@ -168,11 +121,11 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
|
|
|
168
121
|
: undefined;
|
|
169
122
|
|
|
170
123
|
if (sanitizedSchema !== undefined) {
|
|
171
|
-
const resolved =
|
|
124
|
+
const resolved = dereferenceJsonSchema({
|
|
172
125
|
...sanitizedSchema,
|
|
173
126
|
description: schemaDescription,
|
|
174
127
|
});
|
|
175
|
-
dataSchema = Type.Unsafe(resolved);
|
|
128
|
+
dataSchema = Type.Unsafe(resolved as Record<string, unknown>);
|
|
176
129
|
} else {
|
|
177
130
|
dataSchema = Type.Record(Type.String(), Type.Any(), {
|
|
178
131
|
description: schemaError ? schemaDescription : "Structured JSON output (no schema specified)",
|
package/src/tui/output-block.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bordered output container with optional header and sections.
|
|
3
3
|
*/
|
|
4
|
-
import { ImageProtocol, padding, TERMINAL, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { ImageProtocol, padding, TERMINAL, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import type { Theme } from "../modes/theme/theme";
|
|
6
6
|
import { getSixelLineMask } from "../utils/sixel";
|
|
7
7
|
import type { State } from "./types";
|
|
@@ -88,13 +88,12 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
88
88
|
lines.push(line);
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
lines.push(padToWidth(fullLine, lineWidth, bgFn));
|
|
91
|
+
const wrappedLines = wrapTextWithAnsi(line.trimEnd(), contentWidth);
|
|
92
|
+
for (const wrappedLine of wrappedLines) {
|
|
93
|
+
const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(wrappedLine)));
|
|
94
|
+
const fullLine = `${contentPrefix}${wrappedLine}${innerPadding}${contentSuffix}`;
|
|
95
|
+
lines.push(padToWidth(fullLine, lineWidth, bgFn));
|
|
96
|
+
}
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -296,7 +296,7 @@ type EnsureToolOptions = {
|
|
|
296
296
|
|
|
297
297
|
export async function ensureTool(tool: ToolName, silentOrOptions?: EnsureToolOptions): Promise<string | undefined> {
|
|
298
298
|
const { signal, silent = false, notify } = silentOrOptions ?? {};
|
|
299
|
-
const existingPath =
|
|
299
|
+
const existingPath = getToolPath(tool);
|
|
300
300
|
if (existingPath) {
|
|
301
301
|
return existingPath;
|
|
302
302
|
}
|