@oh-my-pi/pi-coding-agent 13.7.1 → 13.7.3
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 +32 -0
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +3 -12
- 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 +8 -9
- 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 +17 -6
- 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
|
@@ -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,7 +423,7 @@ 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,
|
|
@@ -440,7 +440,18 @@ async function renderHtmlToText(
|
|
|
440
440
|
signal,
|
|
441
441
|
};
|
|
442
442
|
|
|
443
|
-
// Try
|
|
443
|
+
// Try Kagi Universal Summarizer first (if KAGI_API_KEY is configured)
|
|
444
|
+
try {
|
|
445
|
+
const kagiSummary = await summarizeUrlWithKagi(url, { signal });
|
|
446
|
+
if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) {
|
|
447
|
+
return { content: kagiSummary, ok: true, method: "kagi" };
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
// Kagi failed, continue to next method
|
|
451
|
+
signal?.throwIfAborted();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Try jina next (reader API)
|
|
444
455
|
try {
|
|
445
456
|
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
446
457
|
const response = await fetch(jinaUrl, {
|
|
@@ -1066,8 +1077,8 @@ export function renderFetchResult(
|
|
|
1066
1077
|
if (contentPreviewLines === undefined || lastExpanded !== expanded) {
|
|
1067
1078
|
const previewLimit = expanded ? 12 : 3;
|
|
1068
1079
|
const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
|
|
1069
|
-
const previewLines = previewList.items.map(line =>
|
|
1070
|
-
const remaining = Math.max(0, contentLines.length -
|
|
1080
|
+
const previewLines = previewList.items.map(line => line.trimEnd());
|
|
1081
|
+
const remaining = Math.max(0, contentLines.length - previewList.items.length);
|
|
1071
1082
|
contentPreviewLines =
|
|
1072
1083
|
previewLines.length > 0
|
|
1073
1084
|
? 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
|
}
|