@oh-my-pi/pi-coding-agent 17.1.1 → 17.1.2
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 +31 -0
- package/dist/cli.js +6216 -3984
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -6
- package/dist/types/modes/types.d.ts +8 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/web-search-cli.ts +7 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +5 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/export/share.ts +4 -3
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/modes/controllers/input-controller.ts +8 -8
- package/src/modes/interactive-mode.ts +60 -5
- package/src/modes/types.ts +9 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +8 -4
- package/src/session/agent-session.ts +14 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
const INDENT = " ";
|
|
2
|
+
|
|
3
|
+
const OPENING_KEYWORDS: Record<string, true> = {
|
|
4
|
+
begin: true,
|
|
5
|
+
case: true,
|
|
6
|
+
class: true,
|
|
7
|
+
def: true,
|
|
8
|
+
for: true,
|
|
9
|
+
if: true,
|
|
10
|
+
module: true,
|
|
11
|
+
unless: true,
|
|
12
|
+
until: true,
|
|
13
|
+
while: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const BRANCH_KEYWORDS: Record<string, true> = {
|
|
17
|
+
else: true,
|
|
18
|
+
elsif: true,
|
|
19
|
+
ensure: true,
|
|
20
|
+
rescue: true,
|
|
21
|
+
when: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PAIRED_DELIMITERS: Record<string, string> = {
|
|
25
|
+
"(": ")",
|
|
26
|
+
"[": "]",
|
|
27
|
+
"{": "}",
|
|
28
|
+
"<": ">",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Quote = "'" | '"' | "`";
|
|
32
|
+
|
|
33
|
+
interface QuotedContext {
|
|
34
|
+
kind: "quoted";
|
|
35
|
+
quote: Quote;
|
|
36
|
+
interpolated: boolean;
|
|
37
|
+
escaped: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PercentContext {
|
|
41
|
+
kind: "percent";
|
|
42
|
+
open: string;
|
|
43
|
+
close: string;
|
|
44
|
+
depth: number;
|
|
45
|
+
interpolated: boolean;
|
|
46
|
+
escaped: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface InterpolationContext {
|
|
50
|
+
kind: "interpolation";
|
|
51
|
+
braceDepth: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CommentContext {
|
|
55
|
+
kind: "comment";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type LexicalContext = QuotedContext | PercentContext | InterpolationContext | CommentContext;
|
|
59
|
+
|
|
60
|
+
interface PercentLiteralStart {
|
|
61
|
+
end: number;
|
|
62
|
+
open: string;
|
|
63
|
+
close: string;
|
|
64
|
+
interpolated: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isHorizontalWhitespace(character: string): boolean {
|
|
68
|
+
return character === " " || character === "\t" || character === "\f" || character === "\v";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isIdentifierStart(character: string | undefined): boolean {
|
|
72
|
+
if (character === undefined) return false;
|
|
73
|
+
const code = character.charCodeAt(0);
|
|
74
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || character === "_";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isIdentifierPart(character: string | undefined): boolean {
|
|
78
|
+
if (character === undefined) return false;
|
|
79
|
+
const code = character.charCodeAt(0);
|
|
80
|
+
return isIdentifierStart(character) || (code >= 48 && code <= 57);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findPercentLiteral(source: string, start: number): PercentLiteralStart | undefined {
|
|
84
|
+
let delimiterIndex = start + 1;
|
|
85
|
+
let type = "";
|
|
86
|
+
const candidateType = source[delimiterIndex];
|
|
87
|
+
if (candidateType !== undefined && "qQwWiIxrs".includes(candidateType)) {
|
|
88
|
+
type = candidateType;
|
|
89
|
+
delimiterIndex++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const open = source[delimiterIndex];
|
|
93
|
+
if (open === undefined || isIdentifierPart(open) || /\s/.test(open)) return undefined;
|
|
94
|
+
return {
|
|
95
|
+
end: delimiterIndex + 1,
|
|
96
|
+
open,
|
|
97
|
+
close: PAIRED_DELIMITERS[open] ?? open,
|
|
98
|
+
interpolated: type === "" || type === "Q" || type === "W" || type === "I" || type === "x" || type === "r",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatRubyPrefix(source: string): string {
|
|
103
|
+
const output: string[] = [];
|
|
104
|
+
const contexts: LexicalContext[] = [];
|
|
105
|
+
const delimiters: string[] = [];
|
|
106
|
+
let lineParts: string[] = [];
|
|
107
|
+
let lineHasVisibleText = false;
|
|
108
|
+
let preserveLeadingWhitespace = false;
|
|
109
|
+
let pendingSemicolonBreak = false;
|
|
110
|
+
let blockDepth = 0;
|
|
111
|
+
let hasSignificantToken = false;
|
|
112
|
+
let leadingWord: string | null = null;
|
|
113
|
+
let previousToken = "";
|
|
114
|
+
let hasDo = false;
|
|
115
|
+
let endCount = 0;
|
|
116
|
+
let endlessDefinition = false;
|
|
117
|
+
|
|
118
|
+
const append = (text: string): void => {
|
|
119
|
+
lineParts.push(text);
|
|
120
|
+
if (lineHasVisibleText) return;
|
|
121
|
+
for (let index = 0; index < text.length; index++) {
|
|
122
|
+
if (!isHorizontalWhitespace(text[index])) {
|
|
123
|
+
lineHasVisibleText = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const observeOtherToken = (value: string): void => {
|
|
130
|
+
if (!hasSignificantToken) {
|
|
131
|
+
hasSignificantToken = true;
|
|
132
|
+
leadingWord = null;
|
|
133
|
+
}
|
|
134
|
+
previousToken = value;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const resetLine = (): void => {
|
|
138
|
+
lineParts = [];
|
|
139
|
+
lineHasVisibleText = false;
|
|
140
|
+
preserveLeadingWhitespace = contexts.length > 0 || delimiters.length > 0;
|
|
141
|
+
hasSignificantToken = false;
|
|
142
|
+
leadingWord = null;
|
|
143
|
+
previousToken = "";
|
|
144
|
+
hasDo = false;
|
|
145
|
+
endCount = 0;
|
|
146
|
+
endlessDefinition = false;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const flushLine = (ending: string): void => {
|
|
150
|
+
const raw = lineParts.join("");
|
|
151
|
+
const leadingEnd = leadingWord === "end";
|
|
152
|
+
const branch = leadingWord !== null && BRANCH_KEYWORDS[leadingWord] === true;
|
|
153
|
+
const opens =
|
|
154
|
+
!endlessDefinition &&
|
|
155
|
+
((leadingWord !== null && OPENING_KEYWORDS[leadingWord] === true) || (!leadingEnd && !branch && hasDo));
|
|
156
|
+
const indentation = leadingEnd || branch ? Math.max(0, blockDepth - 1) : blockDepth;
|
|
157
|
+
blockDepth = Math.max(0, blockDepth + (opens ? 1 : 0) - endCount);
|
|
158
|
+
|
|
159
|
+
if (preserveLeadingWhitespace) {
|
|
160
|
+
output.push(raw, ending);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let contentStart = 0;
|
|
165
|
+
while (contentStart < raw.length && isHorizontalWhitespace(raw[contentStart])) contentStart++;
|
|
166
|
+
const content = raw.slice(contentStart);
|
|
167
|
+
output.push(content.length === 0 ? "" : INDENT.repeat(indentation) + content, ending);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (let index = 0; index < source.length; ) {
|
|
171
|
+
const character = source[index];
|
|
172
|
+
if (character === "\n" || character === "\r") {
|
|
173
|
+
const ending = character === "\r" && source[index + 1] === "\n" ? "\r\n" : character;
|
|
174
|
+
const top = contexts[contexts.length - 1];
|
|
175
|
+
if (top?.kind === "comment") {
|
|
176
|
+
contexts.pop();
|
|
177
|
+
} else if (top?.kind === "quoted" || top?.kind === "percent") {
|
|
178
|
+
top.escaped = false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (pendingSemicolonBreak && !lineHasVisibleText) {
|
|
182
|
+
pendingSemicolonBreak = false;
|
|
183
|
+
resetLine();
|
|
184
|
+
} else {
|
|
185
|
+
flushLine(ending);
|
|
186
|
+
pendingSemicolonBreak = false;
|
|
187
|
+
resetLine();
|
|
188
|
+
}
|
|
189
|
+
index += ending.length;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const top = contexts[contexts.length - 1];
|
|
194
|
+
if (top?.kind === "comment") {
|
|
195
|
+
append(character);
|
|
196
|
+
index++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (top?.kind === "quoted") {
|
|
201
|
+
append(character);
|
|
202
|
+
if (top.escaped) {
|
|
203
|
+
top.escaped = false;
|
|
204
|
+
index++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (character === "\\") {
|
|
208
|
+
top.escaped = true;
|
|
209
|
+
index++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (top.interpolated && character === "#" && source[index + 1] === "{") {
|
|
213
|
+
append("{");
|
|
214
|
+
contexts.push({ kind: "interpolation", braceDepth: 1 });
|
|
215
|
+
index += 2;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (character === top.quote) contexts.pop();
|
|
219
|
+
index++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (top?.kind === "percent") {
|
|
224
|
+
append(character);
|
|
225
|
+
if (top.escaped) {
|
|
226
|
+
top.escaped = false;
|
|
227
|
+
index++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (character === "\\") {
|
|
231
|
+
top.escaped = true;
|
|
232
|
+
index++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (top.interpolated && character === "#" && source[index + 1] === "{") {
|
|
236
|
+
append("{");
|
|
237
|
+
contexts.push({ kind: "interpolation", braceDepth: 1 });
|
|
238
|
+
index += 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (top.open !== top.close && character === top.open) {
|
|
242
|
+
top.depth++;
|
|
243
|
+
} else if (character === top.close) {
|
|
244
|
+
top.depth--;
|
|
245
|
+
if (top.depth === 0) contexts.pop();
|
|
246
|
+
}
|
|
247
|
+
index++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const interpolation = top?.kind === "interpolation" ? top : undefined;
|
|
252
|
+
const inRootCode = interpolation === undefined;
|
|
253
|
+
if (interpolation !== undefined && character === "}") {
|
|
254
|
+
append(character);
|
|
255
|
+
interpolation.braceDepth--;
|
|
256
|
+
if (interpolation.braceDepth === 0) contexts.pop();
|
|
257
|
+
index++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (character === "#") {
|
|
262
|
+
append(character);
|
|
263
|
+
contexts.push({ kind: "comment" });
|
|
264
|
+
index++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
269
|
+
if (inRootCode && delimiters.length === 0) observeOtherToken("literal");
|
|
270
|
+
append(character);
|
|
271
|
+
contexts.push({ kind: "quoted", quote: character, interpolated: character !== "'", escaped: false });
|
|
272
|
+
index++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (character === "%") {
|
|
277
|
+
const literal = findPercentLiteral(source, index);
|
|
278
|
+
if (literal !== undefined) {
|
|
279
|
+
if (inRootCode && delimiters.length === 0) observeOtherToken("literal");
|
|
280
|
+
append(source.slice(index, literal.end));
|
|
281
|
+
contexts.push({
|
|
282
|
+
kind: "percent",
|
|
283
|
+
open: literal.open,
|
|
284
|
+
close: literal.close,
|
|
285
|
+
depth: 1,
|
|
286
|
+
interpolated: literal.interpolated,
|
|
287
|
+
escaped: false,
|
|
288
|
+
});
|
|
289
|
+
index = literal.end;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (isIdentifierStart(character)) {
|
|
295
|
+
let end = index + 1;
|
|
296
|
+
while (isIdentifierPart(source[end])) end++;
|
|
297
|
+
if (source[end] === "?" || source[end] === "!") end++;
|
|
298
|
+
const word = source.slice(index, end);
|
|
299
|
+
append(word);
|
|
300
|
+
|
|
301
|
+
if (inRootCode && delimiters.length === 0) {
|
|
302
|
+
const blockedByPrefix =
|
|
303
|
+
previousToken === ":" || previousToken === "." || previousToken === "@" || previousToken === "$";
|
|
304
|
+
const label = source[end] === ":" && source[end + 1] !== ":";
|
|
305
|
+
const eligible = !blockedByPrefix && !label;
|
|
306
|
+
if (!hasSignificantToken) {
|
|
307
|
+
hasSignificantToken = true;
|
|
308
|
+
leadingWord = eligible ? word : null;
|
|
309
|
+
}
|
|
310
|
+
if (eligible && word === "do") hasDo = true;
|
|
311
|
+
if (eligible && word === "end") endCount++;
|
|
312
|
+
previousToken = "word";
|
|
313
|
+
}
|
|
314
|
+
index = end;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (interpolation !== undefined && character === "{") {
|
|
319
|
+
append(character);
|
|
320
|
+
interpolation.braceDepth++;
|
|
321
|
+
index++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (inRootCode && (character === "(" || character === "[" || character === "{")) {
|
|
326
|
+
if (delimiters.length === 0) observeOtherToken(character);
|
|
327
|
+
append(character);
|
|
328
|
+
delimiters.push(character);
|
|
329
|
+
index++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (inRootCode && (character === ")" || character === "]" || character === "}")) {
|
|
334
|
+
append(character);
|
|
335
|
+
const open = delimiters[delimiters.length - 1];
|
|
336
|
+
if (open !== undefined && PAIRED_DELIMITERS[open] === character) delimiters.pop();
|
|
337
|
+
if (delimiters.length === 0) observeOtherToken(character);
|
|
338
|
+
index++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (character === ";") {
|
|
343
|
+
append(character);
|
|
344
|
+
if (inRootCode && delimiters.length === 0) {
|
|
345
|
+
observeOtherToken(character);
|
|
346
|
+
flushLine("\n");
|
|
347
|
+
pendingSemicolonBreak = true;
|
|
348
|
+
resetLine();
|
|
349
|
+
}
|
|
350
|
+
index++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
append(character);
|
|
355
|
+
if (inRootCode && delimiters.length === 0 && !isHorizontalWhitespace(character)) {
|
|
356
|
+
if (
|
|
357
|
+
character === "=" &&
|
|
358
|
+
leadingWord === "def" &&
|
|
359
|
+
/\s/.test(source[index - 1] ?? "") &&
|
|
360
|
+
source[index + 1] !== "="
|
|
361
|
+
) {
|
|
362
|
+
endlessDefinition = true;
|
|
363
|
+
}
|
|
364
|
+
observeOtherToken(character);
|
|
365
|
+
}
|
|
366
|
+
index++;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (lineParts.length > 0 && !(pendingSemicolonBreak && !lineHasVisibleText)) flushLine("");
|
|
370
|
+
return output.join("");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Formats an arbitrary Ruby source prefix for display without requiring valid syntax. */
|
|
374
|
+
export function formatRubyForDisplay(source: string): string {
|
|
375
|
+
try {
|
|
376
|
+
return formatRubyPrefix(source);
|
|
377
|
+
} catch {
|
|
378
|
+
return source;
|
|
379
|
+
}
|
|
380
|
+
}
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { formatContextUsage } from "../modes/components/status-line/context-thre
|
|
|
19
19
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
20
20
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
21
21
|
import { markFramedBlockComponent, outputBlockContentWidth, renderCodeCell } from "../tui";
|
|
22
|
+
import { formatEvalCodeForDisplay } from "./eval-format";
|
|
22
23
|
import {
|
|
23
24
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
24
25
|
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
@@ -89,10 +90,11 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
|
89
90
|
const out: EvalRenderCell[] = [];
|
|
90
91
|
for (const cell of raw) {
|
|
91
92
|
if (!cell || typeof cell !== "object") continue;
|
|
93
|
+
const language = normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined);
|
|
92
94
|
const code = typeof cell.code === "string" ? cell.code : "";
|
|
93
95
|
out.push({
|
|
94
|
-
language
|
|
95
|
-
code,
|
|
96
|
+
language,
|
|
97
|
+
code: formatEvalCodeForDisplay(code, language),
|
|
96
98
|
title: typeof cell.title === "string" ? cell.title : undefined,
|
|
97
99
|
});
|
|
98
100
|
}
|
|
@@ -587,6 +589,10 @@ export const evalToolRenderer = {
|
|
|
587
589
|
|
|
588
590
|
const cellResults = details?.cells;
|
|
589
591
|
if (cellResults && cellResults.length > 0) {
|
|
592
|
+
const displayCells = cellResults.map(cell => {
|
|
593
|
+
const language = cell.language ?? details?.language ?? "python";
|
|
594
|
+
return { cell, code: formatEvalCodeForDisplay(cell.code, language), language };
|
|
595
|
+
});
|
|
590
596
|
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
591
597
|
|
|
592
598
|
return markFramedBlockComponent({
|
|
@@ -602,8 +608,8 @@ export const evalToolRenderer = {
|
|
|
602
608
|
}
|
|
603
609
|
|
|
604
610
|
const lines: string[] = [];
|
|
605
|
-
for (let i = 0; i <
|
|
606
|
-
const cell =
|
|
611
|
+
for (let i = 0; i < displayCells.length; i++) {
|
|
612
|
+
const { cell, code, language } = displayCells[i];
|
|
607
613
|
const allEvents = cell.statusEvents ?? [];
|
|
608
614
|
const agentEvents = allEvents.filter(e => e.op === "agent");
|
|
609
615
|
const otherEvents = agentEvents.length > 0 ? allEvents.filter(e => e.op !== "agent") : allEvents;
|
|
@@ -623,8 +629,8 @@ export const evalToolRenderer = {
|
|
|
623
629
|
}
|
|
624
630
|
const cellLines = renderCodeCell(
|
|
625
631
|
{
|
|
626
|
-
code
|
|
627
|
-
language: languageForHighlighter(
|
|
632
|
+
code,
|
|
633
|
+
language: languageForHighlighter(language),
|
|
628
634
|
showLanguage: true,
|
|
629
635
|
index: i,
|
|
630
636
|
total: cellResults.length,
|
package/src/tools/resolve.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import type { AgentToolResult, CustomMessage } from "@oh-my-pi/pi-agent-core";
|
|
18
18
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
20
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
20
21
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
21
22
|
import { parseXdUrl, XD_URL_PREFIX } from "../internal-urls/xd-protocol";
|
|
22
23
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -36,6 +37,14 @@ export const RESOLVE_DEVICE_PATH = `${XD_URL_PREFIX}${RESOLVE_DEVICE_NAME}`;
|
|
|
36
37
|
export const REJECT_DEVICE_PATH = `${XD_URL_PREFIX}${REJECT_DEVICE_NAME}`;
|
|
37
38
|
export const PROPOSE_DEVICE_PATH = `${XD_URL_PREFIX}${PROPOSE_DEVICE_NAME}`;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Model-visible banner prepended to a staged preview's tool result text. The
|
|
42
|
+
* TUI badge (`⟨proposed⟩`) never reaches the model, and preview diffs are
|
|
43
|
+
* byte-identical to applied-edit output — without this line the model reads
|
|
44
|
+
* the result as an already-applied change.
|
|
45
|
+
*/
|
|
46
|
+
export const PREVIEW_PENDING_NOTICE = `Staged as a proposal — files NOT modified yet. To apply: write a one-sentence reason to ${RESOLVE_DEVICE_PATH}. To discard: write to ${REJECT_DEVICE_PATH}.`;
|
|
47
|
+
|
|
39
48
|
export type ResolutionDeviceName = typeof RESOLVE_DEVICE_NAME | typeof REJECT_DEVICE_NAME | typeof PROPOSE_DEVICE_NAME;
|
|
40
49
|
|
|
41
50
|
/** Whether an xd:// device name is one of the plain-text resolution devices. */
|
|
@@ -204,7 +213,7 @@ export function buildResolveReminderMessage(sourceToolName: string): CustomMessa
|
|
|
204
213
|
return {
|
|
205
214
|
role: "custom",
|
|
206
215
|
customType: "resolve-reminder",
|
|
207
|
-
content: resolveReminderPrompt.trim(),
|
|
216
|
+
content: prompt.render(resolveReminderPrompt, { toolName: sourceToolName }).trim(),
|
|
208
217
|
display: false,
|
|
209
218
|
details: { toolName: sourceToolName },
|
|
210
219
|
attribution: "agent",
|
package/src/tools/todo.ts
CHANGED
|
@@ -524,7 +524,46 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
|
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
|
|
527
|
+
/**
|
|
528
|
+
* Infer a missing `op` from the raw argument shape. Only unambiguous shapes
|
|
529
|
+
* are inferred:
|
|
530
|
+
* - `list` → `init` (list is init-only)
|
|
531
|
+
* - `items` + `phase` → `append` (lazily creates the phase, so the result
|
|
532
|
+
* matches a single-phase init when nothing exists yet)
|
|
533
|
+
* - bare `items` with no existing todos → `init` (nothing to overwrite)
|
|
534
|
+
* Targeting args alone (`task`/`phase`) map to several ops and stay an error.
|
|
535
|
+
*/
|
|
536
|
+
function inferTodoOp(args: Record<string, unknown>, hasExistingPhases: boolean): TodoOperation | undefined {
|
|
537
|
+
if (Array.isArray(args.list) && args.list.length > 0) return "init";
|
|
538
|
+
if (Array.isArray(args.items) && args.items.length > 0) {
|
|
539
|
+
if (typeof args.phase === "string" && args.phase) return "append";
|
|
540
|
+
if (!hasExistingPhases) return "init";
|
|
541
|
+
}
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Validate execute-time arguments, repairing an omitted `op`. The tool sets
|
|
547
|
+
* `lenientArgValidation`, so the agent loop hands `execute()` the raw
|
|
548
|
+
* arguments when schema validation fails; the only failure repaired here is
|
|
549
|
+
* a missing `op` alongside an unambiguous payload (models routinely send
|
|
550
|
+
* `{list:[...]}` with no op). Anything else returns the schema error text
|
|
551
|
+
* for a normal model retry.
|
|
552
|
+
*/
|
|
553
|
+
function resolveTodoParams(raw: unknown, hasExistingPhases: boolean): TodoOpEntryValue | string {
|
|
554
|
+
const direct = todoSchema(raw);
|
|
555
|
+
if (!(direct instanceof type.errors)) return direct;
|
|
556
|
+
if (isRecord(raw) && raw.op === undefined) {
|
|
557
|
+
const inferred = inferTodoOp(raw, hasExistingPhases);
|
|
558
|
+
if (inferred) {
|
|
559
|
+
const repaired = todoSchema({ ...raw, op: inferred });
|
|
560
|
+
if (!(repaired instanceof type.errors)) return repaired;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return `Invalid todo arguments: ${direct.summary}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function applyParams(phases: TodoPhase[], params: TodoOpEntryValue): { phases: TodoPhase[]; errors: string[] } {
|
|
528
567
|
const errors: string[] = [];
|
|
529
568
|
const next = applyEntry(phases, params, errors);
|
|
530
569
|
normalizeInProgressTask(next);
|
|
@@ -534,7 +573,7 @@ function applyParams(phases: TodoPhase[], params: TodoParams): { phases: TodoPha
|
|
|
534
573
|
/** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
|
|
535
574
|
export function applyOpsToPhases(
|
|
536
575
|
currentPhases: TodoPhase[],
|
|
537
|
-
ops:
|
|
576
|
+
ops: TodoOpEntryValue[],
|
|
538
577
|
): { phases: TodoPhase[]; errors: string[] } {
|
|
539
578
|
const errors: string[] = [];
|
|
540
579
|
let next = clonePhases(currentPhases);
|
|
@@ -731,6 +770,9 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
|
|
|
731
770
|
readonly parameters = todoSchema;
|
|
732
771
|
readonly concurrency = "exclusive";
|
|
733
772
|
readonly strict = true;
|
|
773
|
+
// Raw args reach execute() on schema failure; resolveTodoParams re-validates
|
|
774
|
+
// and repairs the one recoverable shape (missing `op`, unambiguous payload).
|
|
775
|
+
readonly lenientArgValidation = true;
|
|
734
776
|
|
|
735
777
|
readonly examples: readonly ToolExample<typeof todoSchema.infer>[] = [
|
|
736
778
|
{
|
|
@@ -789,11 +831,22 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
|
|
|
789
831
|
_context?: AgentToolContext,
|
|
790
832
|
): Promise<AgentToolResult<TodoToolDetails>> {
|
|
791
833
|
const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
|
|
834
|
+
const storage = this.session.getSessionFile() ? "session" : "memory";
|
|
835
|
+
const resolved = resolveTodoParams(params, previousPhases.length > 0);
|
|
836
|
+
if (typeof resolved === "string") {
|
|
837
|
+
return {
|
|
838
|
+
content: [{ type: "text", text: resolved }],
|
|
839
|
+
details: { phases: previousPhases, storage },
|
|
840
|
+
isError: true,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const entry = resolved;
|
|
844
|
+
const op = entry.op;
|
|
792
845
|
// Pure-view calls are reads: no normalization, no state write.
|
|
793
|
-
const readOnly =
|
|
846
|
+
const readOnly = op === "view";
|
|
794
847
|
const { phases: updated, errors } = readOnly
|
|
795
848
|
? { phases: previousPhases, errors: [] as string[] }
|
|
796
|
-
: applyParams(clonePhases(previousPhases),
|
|
849
|
+
: applyParams(clonePhases(previousPhases), entry);
|
|
797
850
|
// A batch with any error is discarded wholesale: persisting a
|
|
798
851
|
// half-applied batch makes the natural retry hit "already exists" for
|
|
799
852
|
// the ops that did land. State and rendered summary stay at previous.
|
|
@@ -801,8 +854,7 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
|
|
|
801
854
|
const effective = failed ? previousPhases : updated;
|
|
802
855
|
const completedTasks = readOnly || failed ? [] : getCompletionTransitions(previousPhases, updated);
|
|
803
856
|
if (!readOnly && !failed) this.session.setTodoPhases?.(updated);
|
|
804
|
-
const
|
|
805
|
-
const details: TodoToolDetails = { op: params.op, phases: effective, storage };
|
|
857
|
+
const details: TodoToolDetails = { op, phases: effective, storage };
|
|
806
858
|
if (completedTasks.length > 0) details.completedTasks = completedTasks;
|
|
807
859
|
|
|
808
860
|
return {
|
package/src/web/search/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type SearchProvider,
|
|
28
28
|
type SearchProviderCandidate,
|
|
29
29
|
} from "./provider";
|
|
30
|
+
import { applyQueryConstraints, parseSearchQuery } from "./query";
|
|
30
31
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
31
32
|
import type { SearchProviderId, SearchResponse } from "./types";
|
|
32
33
|
import { SearchProviderError } from "./types";
|
|
@@ -57,9 +58,12 @@ function formatCount(label: string, count: number): string {
|
|
|
57
58
|
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/** Format response for LLM consumption */
|
|
61
|
-
function formatForLLM(response: SearchResponse): string {
|
|
61
|
+
/** Format response for LLM consumption. `notes` lead the output (e.g. relaxed-constraint warnings). */
|
|
62
|
+
function formatForLLM(response: SearchResponse, notes: readonly string[] = []): string {
|
|
62
63
|
const parts: string[] = [];
|
|
64
|
+
for (const note of notes) {
|
|
65
|
+
parts.push(`Note: ${note}`);
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
if (response.answer) {
|
|
65
69
|
parts.push(response.answer);
|
|
@@ -141,6 +145,8 @@ async function executeSearch(
|
|
|
141
145
|
candidates = resolveProviderCandidates();
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
const parsedQuery = parseSearchQuery(params.query);
|
|
149
|
+
|
|
144
150
|
// Invariant across providers; read once and tolerate an uninitialized
|
|
145
151
|
// Settings singleton (e.g. `omp q ...` CLI path, unit tests) so the
|
|
146
152
|
// provider-fallback loop never aborts before any provider runs.
|
|
@@ -182,6 +188,7 @@ async function executeSearch(
|
|
|
182
188
|
|
|
183
189
|
const response = await provider.search({
|
|
184
190
|
query: params.query,
|
|
191
|
+
parsedQuery,
|
|
185
192
|
limit: params.limit,
|
|
186
193
|
recency: params.recency,
|
|
187
194
|
systemPrompt: webSearchSystemPrompt,
|
|
@@ -196,15 +203,31 @@ async function executeSearch(
|
|
|
196
203
|
geminiModel,
|
|
197
204
|
});
|
|
198
205
|
|
|
199
|
-
|
|
206
|
+
// Lenient constraint pass over whatever the provider returned: enforce
|
|
207
|
+
// site:/inurl:/intitle:/filetype:/date directives the provider could
|
|
208
|
+
// not (or only partially) honor natively, relaxing any dimension that
|
|
209
|
+
// would wipe out every result. Citations/answer text stay untouched.
|
|
210
|
+
let finalResponse = response;
|
|
211
|
+
const constraintNotes: string[] = [];
|
|
212
|
+
if (parsedQuery.hasConstraints && response.sources.length > 0) {
|
|
213
|
+
const filtered = applyQueryConstraints(response.sources, parsedQuery);
|
|
214
|
+
if (filtered.sources.length !== response.sources.length) {
|
|
215
|
+
finalResponse = { ...response, sources: filtered.sources };
|
|
216
|
+
}
|
|
217
|
+
for (const label of filtered.dropped) {
|
|
218
|
+
constraintNotes.push(`no results matched \`${label}\`; the constraint was relaxed`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!hasRenderableSearchContent(finalResponse)) {
|
|
200
223
|
throw new SearchProviderError(provider.id, `${provider.label} returned no renderable search content.`, 204);
|
|
201
224
|
}
|
|
202
225
|
|
|
203
|
-
const text = formatForLLM(
|
|
226
|
+
const text = formatForLLM(finalResponse, constraintNotes);
|
|
204
227
|
|
|
205
228
|
return {
|
|
206
229
|
content: [{ type: "text" as const, text }],
|
|
207
|
-
details: { response },
|
|
230
|
+
details: { response: finalResponse },
|
|
208
231
|
};
|
|
209
232
|
} catch (error) {
|
|
210
233
|
// Surface user-initiated cancellation immediately so the session sees
|