@oh-my-pi/pi-coding-agent 15.1.6 → 15.1.7
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 +17 -0
- package/dist/types/config/settings-schema.d.ts +15 -7
- package/dist/types/hashline/hash.d.ts +4 -4
- package/dist/types/hashline/recovery.d.ts +5 -0
- package/dist/types/lsp/edits.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/session/client-bridge.d.ts +1 -0
- package/dist/types/tools/find.d.ts +4 -0
- package/dist/types/tools/resolve.d.ts +5 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/package.json +7 -7
- package/src/config/settings-schema.ts +22 -7
- package/src/dap/session.ts +58 -5
- package/src/edit/modes/patch.ts +46 -0
- package/src/eval/js/context-manager.ts +11 -7
- package/src/eval/js/shared/rewrite-imports.ts +21 -9
- package/src/eval/js/shared/runtime.ts +2 -1
- package/src/hashline/hash.ts +11 -8
- package/src/hashline/parser.ts +23 -6
- package/src/hashline/recovery.ts +44 -3
- package/src/lsp/edits.ts +92 -38
- package/src/lsp/index.ts +110 -7
- package/src/lsp/utils.ts +13 -0
- package/src/modes/acp/acp-client-bridge.ts +1 -0
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/prompts/tools/bash.md +14 -0
- package/src/prompts/tools/debug.md +4 -1
- package/src/prompts/tools/find.md +10 -0
- package/src/prompts/tools/hashline.md +5 -3
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +2 -1
- package/src/prompts/tools/task.md +4 -0
- package/src/prompts/tools/todo-write.md +2 -0
- package/src/session/agent-session.ts +116 -8
- package/src/session/client-bridge.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/index.ts +33 -5
- package/src/task/render.ts +4 -1
- package/src/tools/browser/tab-supervisor.ts +23 -3
- package/src/tools/browser/tab-worker.ts +4 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +19 -2
- package/src/tools/find.ts +80 -24
- package/src/tools/read.ts +3 -6
- package/src/tools/resolve.ts +54 -22
- package/src/tools/search.ts +31 -0
- package/src/tools/todo-write.ts +11 -4
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/tools-manager.ts +29 -22
- package/src/web/search/providers/codex.ts +3 -0
package/src/tools/find.ts
CHANGED
|
@@ -38,14 +38,41 @@ const findSchema = z
|
|
|
38
38
|
.object({
|
|
39
39
|
paths: z.array(z.string().describe("glob including search path")).min(1).describe("globs including search paths"),
|
|
40
40
|
hidden: z.boolean().default(true).describe("include hidden files").optional(),
|
|
41
|
+
gitignore: z.boolean().default(true).describe("respect gitignore").optional(),
|
|
41
42
|
limit: z.number().default(1000).describe("max results").optional(),
|
|
43
|
+
timeout: z.number().min(0.5).max(60).default(5).describe("timeout in seconds (0.5–60)").optional(),
|
|
42
44
|
})
|
|
43
45
|
.strict();
|
|
44
46
|
|
|
45
47
|
export type FindToolInput = z.infer<typeof findSchema>;
|
|
46
48
|
|
|
47
49
|
const DEFAULT_LIMIT = 1000;
|
|
48
|
-
const
|
|
50
|
+
const DEFAULT_GLOB_TIMEOUT_MS = 5000;
|
|
51
|
+
const MIN_GLOB_TIMEOUT_MS = 500;
|
|
52
|
+
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reject comma-separated path lists packed into a single array element
|
|
56
|
+
* (`["a.py,b.py"]`). The schema is array-of-string; agents that pass a
|
|
57
|
+
* single comma-joined element get silent no-matches otherwise.
|
|
58
|
+
*
|
|
59
|
+
* Commas inside brace expansion (`{a,b}`) are legitimate glob syntax and
|
|
60
|
+
* must pass through.
|
|
61
|
+
*/
|
|
62
|
+
function validateFindPathInputs(paths: readonly string[]): void {
|
|
63
|
+
for (const entry of paths) {
|
|
64
|
+
let braceDepth = 0;
|
|
65
|
+
for (let i = 0; i < entry.length; i++) {
|
|
66
|
+
const ch = entry.charCodeAt(i);
|
|
67
|
+
if (ch === 0x7b /* { */) braceDepth++;
|
|
68
|
+
else if (ch === 0x7d /* } */) {
|
|
69
|
+
if (braceDepth > 0) braceDepth--;
|
|
70
|
+
} else if (ch === 0x2c /* , */ && braceDepth === 0) {
|
|
71
|
+
throw new ToolError(`paths is an array — pass ["a", "b"] not ["a,b"] (got ${JSON.stringify(entry)})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
49
76
|
|
|
50
77
|
export interface FindToolDetails {
|
|
51
78
|
truncation?: TruncationResult;
|
|
@@ -109,10 +136,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
109
136
|
onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
|
|
110
137
|
_context?: AgentToolContext,
|
|
111
138
|
): Promise<AgentToolResult<FindToolDetails>> {
|
|
112
|
-
const { paths, limit, hidden } = params;
|
|
139
|
+
const { paths, limit, hidden, gitignore, timeout } = params;
|
|
113
140
|
|
|
114
141
|
return untilAborted(signal, async () => {
|
|
115
142
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
143
|
+
validateFindPathInputs(paths);
|
|
116
144
|
const rawPatterns = paths.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
117
145
|
const internalRouter = InternalUrlRouter.instance();
|
|
118
146
|
const normalizedPatterns: string[] = [];
|
|
@@ -165,7 +193,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
165
193
|
throw new ToolError("Limit must be a positive number");
|
|
166
194
|
}
|
|
167
195
|
const includeHidden = hidden ?? true;
|
|
168
|
-
const
|
|
196
|
+
const useGitignore = gitignore ?? true;
|
|
197
|
+
const requestedTimeoutMs = timeout != null ? Math.round(timeout * 1000) : DEFAULT_GLOB_TIMEOUT_MS;
|
|
198
|
+
const timeoutMs = Math.min(MAX_GLOB_TIMEOUT_MS, Math.max(MIN_GLOB_TIMEOUT_MS, requestedTimeoutMs));
|
|
199
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
169
200
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
170
201
|
const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
|
|
171
202
|
const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
|
|
@@ -178,33 +209,41 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
178
209
|
const missingPathsNote =
|
|
179
210
|
missingPaths.length > 0 ? `Skipped missing paths: ${missingPaths.join(", ")}` : undefined;
|
|
180
211
|
|
|
181
|
-
const buildResult = (
|
|
212
|
+
const buildResult = (
|
|
213
|
+
files: string[],
|
|
214
|
+
opts?: { notice?: string; forceTruncated?: boolean },
|
|
215
|
+
): AgentToolResult<FindToolDetails> => {
|
|
216
|
+
const notice = opts?.notice;
|
|
217
|
+
const forceTruncated = opts?.forceTruncated ?? false;
|
|
182
218
|
if (files.length === 0) {
|
|
183
219
|
const details: FindToolDetails = {
|
|
184
220
|
scopePath,
|
|
185
221
|
fileCount: 0,
|
|
186
222
|
files: [],
|
|
187
|
-
truncated:
|
|
223
|
+
truncated: forceTruncated,
|
|
188
224
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
189
225
|
};
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return toolResult(details).text(
|
|
226
|
+
const parts = ["No files found matching pattern"];
|
|
227
|
+
if (notice) parts.push(notice);
|
|
228
|
+
if (missingPathsNote) parts.push(missingPathsNote);
|
|
229
|
+
return toolResult(details).text(parts.join("\n")).done();
|
|
194
230
|
}
|
|
195
231
|
|
|
196
232
|
const listLimit = applyListLimit(files, { limit: effectiveLimit });
|
|
197
233
|
const limited = listLimit.items;
|
|
198
234
|
const limitMeta = listLimit.meta;
|
|
199
235
|
const baseOutput = limited.join("\n");
|
|
200
|
-
const
|
|
236
|
+
const trailingNotes: string[] = [];
|
|
237
|
+
if (notice) trailingNotes.push(notice);
|
|
238
|
+
if (missingPathsNote) trailingNotes.push(missingPathsNote);
|
|
239
|
+
const rawOutput = trailingNotes.length > 0 ? `${baseOutput}\n\n${trailingNotes.join("\n")}` : baseOutput;
|
|
201
240
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
202
241
|
|
|
203
242
|
const details: FindToolDetails = {
|
|
204
243
|
scopePath,
|
|
205
244
|
fileCount: limited.length,
|
|
206
245
|
files: limited,
|
|
207
|
-
truncated: Boolean(limitMeta.resultLimit || truncation.truncated),
|
|
246
|
+
truncated: Boolean(forceTruncated || limitMeta.resultLimit || truncation.truncated),
|
|
208
247
|
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
209
248
|
truncation: truncation.truncated ? truncation : undefined,
|
|
210
249
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
@@ -278,14 +317,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
278
317
|
details,
|
|
279
318
|
});
|
|
280
319
|
};
|
|
281
|
-
const onMatch =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
: undefined;
|
|
320
|
+
const onMatch = (err: Error | null, match: natives.GlobMatch | null) => {
|
|
321
|
+
if (err || signal?.aborted || !match?.path) return;
|
|
322
|
+
const relativePath = formatMatchPath(match.path, match.fileType);
|
|
323
|
+
onUpdateMatches.push(relativePath);
|
|
324
|
+
emitUpdate();
|
|
325
|
+
};
|
|
289
326
|
|
|
290
327
|
const doGlob = async (useGitignore: boolean) =>
|
|
291
328
|
untilAborted(combinedSignal, () =>
|
|
@@ -304,8 +341,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
304
341
|
),
|
|
305
342
|
);
|
|
306
343
|
|
|
344
|
+
let timedOut = false;
|
|
307
345
|
try {
|
|
308
|
-
const result = await doGlob(
|
|
346
|
+
const result = await doGlob(useGitignore);
|
|
309
347
|
// Sort by mtime descending (most recent first) in JS instead of native.
|
|
310
348
|
// This allows native glob to early-terminate at maxResults.
|
|
311
349
|
result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
@@ -313,12 +351,30 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
313
351
|
} catch (error) {
|
|
314
352
|
if (error instanceof Error && error.name === "AbortError") {
|
|
315
353
|
if (timeoutSignal.aborted && !signal?.aborted) {
|
|
316
|
-
|
|
317
|
-
|
|
354
|
+
timedOut = true;
|
|
355
|
+
matches = [];
|
|
356
|
+
} else {
|
|
357
|
+
throw new ToolAbortError();
|
|
318
358
|
}
|
|
319
|
-
|
|
359
|
+
} else {
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (timedOut) {
|
|
365
|
+
// Drain the partial matches accumulated during streaming and return them
|
|
366
|
+
// instead of throwing — empty results after a multi-second wait force the
|
|
367
|
+
// caller to retry blind, which is the worst possible outcome.
|
|
368
|
+
const seen = new Set<string>();
|
|
369
|
+
const partial: string[] = [];
|
|
370
|
+
for (const entry of onUpdateMatches) {
|
|
371
|
+
if (seen.has(entry)) continue;
|
|
372
|
+
seen.add(entry);
|
|
373
|
+
partial.push(entry);
|
|
320
374
|
}
|
|
321
|
-
|
|
375
|
+
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
376
|
+
const notice = `find timed out after ${seconds}s; returning ${partial.length} partial matches — increase timeout or narrow pattern`;
|
|
377
|
+
return buildResult(partial, { notice, forceTruncated: true });
|
|
322
378
|
}
|
|
323
379
|
|
|
324
380
|
const relativized: string[] = [];
|
package/src/tools/read.ts
CHANGED
|
@@ -800,10 +800,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
800
800
|
// context (added below if offset is explicit).
|
|
801
801
|
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
802
802
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
803
|
-
const requestedEnd =
|
|
804
|
-
limit !== undefined && !ignoreResultLimits
|
|
805
|
-
? Math.min(requestedStart + limit, allLines.length)
|
|
806
|
-
: allLines.length;
|
|
803
|
+
const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length;
|
|
807
804
|
// Expand only on sides the user actually constrained: leading context
|
|
808
805
|
// when offset>1, trailing context when a finite limit was set.
|
|
809
806
|
const expanded = expandRangeWithContext(
|
|
@@ -811,7 +808,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
811
808
|
requestedEnd,
|
|
812
809
|
allLines.length,
|
|
813
810
|
offset !== undefined && offset > 1,
|
|
814
|
-
limit !== undefined
|
|
811
|
+
limit !== undefined,
|
|
815
812
|
);
|
|
816
813
|
const startLine = expanded.startLine;
|
|
817
814
|
const endLineExpanded = expanded.endLine;
|
|
@@ -842,7 +839,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
842
839
|
|
|
843
840
|
const endLine = endLineExpanded;
|
|
844
841
|
const selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
845
|
-
const userLimitedLines = limit !== undefined
|
|
842
|
+
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
846
843
|
const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent);
|
|
847
844
|
|
|
848
845
|
const shouldAddHashLines = displayMode.hashLines;
|
package/src/tools/resolve.ts
CHANGED
|
@@ -51,28 +51,43 @@ export function queueResolveHandler(
|
|
|
51
51
|
const forced = session.buildToolChoice?.("resolve");
|
|
52
52
|
if (!queue || !forced || typeof forced === "string") return;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
54
|
+
const steerReminder = (): void => {
|
|
55
|
+
session.steer?.({
|
|
56
|
+
customType: "resolve-reminder",
|
|
57
|
+
content: [
|
|
58
|
+
"<system-reminder>",
|
|
59
|
+
"This is a preview. Call the `resolve` tool to apply or discard these changes.",
|
|
60
|
+
"</system-reminder>",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
details: { toolName: options.sourceToolName },
|
|
63
|
+
});
|
|
64
|
+
};
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
const pushDirective = (): void => {
|
|
67
|
+
queue.pushOnce(forced, {
|
|
68
|
+
label: `pending-action:${options.sourceToolName}`,
|
|
69
|
+
now: true,
|
|
70
|
+
onRejected: () => "requeue",
|
|
71
|
+
onInvoked: async (input: unknown) =>
|
|
72
|
+
runResolveInvocation(input as ResolveParams, {
|
|
73
|
+
sourceToolName: options.sourceToolName,
|
|
74
|
+
label: options.label,
|
|
75
|
+
apply: options.apply,
|
|
76
|
+
reject: options.reject,
|
|
77
|
+
onApplyError: () => {
|
|
78
|
+
// Apply threw (e.g. ast_edit overlapping replacements). Re-push the
|
|
79
|
+
// same directive so the preview remains pending and the model can
|
|
80
|
+
// `discard` or fix-and-retry on the next turn instead of being
|
|
81
|
+
// stranded with no pending action to address.
|
|
82
|
+
pushDirective();
|
|
83
|
+
steerReminder();
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
pushDirective();
|
|
90
|
+
steerReminder();
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
/**
|
|
@@ -89,6 +104,11 @@ export async function runResolveInvocation(
|
|
|
89
104
|
label: string;
|
|
90
105
|
apply(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown>>;
|
|
91
106
|
reject?(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown> | undefined>;
|
|
107
|
+
/** Invoked synchronously when `apply()` throws, before the error is rethrown.
|
|
108
|
+
* The queued caller uses this to re-push the resolve directive so the
|
|
109
|
+
* pending preview survives a failed apply (e.g. overlapping ast_edit
|
|
110
|
+
* replacements) and the model can `discard` or fix-and-retry. */
|
|
111
|
+
onApplyError?(error: unknown): void;
|
|
92
112
|
},
|
|
93
113
|
): Promise<AgentToolResult<ResolveToolDetails>> {
|
|
94
114
|
const baseDetails: ResolveToolDetails = {
|
|
@@ -99,7 +119,19 @@ export async function runResolveInvocation(
|
|
|
99
119
|
...(params.extra != null ? { extra: params.extra } : {}),
|
|
100
120
|
};
|
|
101
121
|
if (params.action === "apply") {
|
|
102
|
-
|
|
122
|
+
let result: AgentToolResult<unknown>;
|
|
123
|
+
try {
|
|
124
|
+
result = await options.apply(params.reason, params.extra);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
options.onApplyError?.(error);
|
|
128
|
+
} catch {
|
|
129
|
+
// Requeue hook must not mask the original apply failure.
|
|
130
|
+
}
|
|
131
|
+
if (error instanceof ToolError) throw error;
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
throw new ToolError(`Apply failed: ${message}`);
|
|
134
|
+
}
|
|
103
135
|
return {
|
|
104
136
|
...result,
|
|
105
137
|
details: {
|
package/src/tools/search.ts
CHANGED
|
@@ -63,6 +63,29 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
63
63
|
* pagination headroom so the caller can see total file count. */
|
|
64
64
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
|
|
68
|
+
* `paths: ["a,b"]` mistakes where the caller flattened multiple entries
|
|
69
|
+
* into a single string instead of passing a JSON array of strings.
|
|
70
|
+
*/
|
|
71
|
+
function containsTopLevelComma(entry: string): boolean {
|
|
72
|
+
let depth = 0;
|
|
73
|
+
for (let i = 0; i < entry.length; i++) {
|
|
74
|
+
const ch = entry[i];
|
|
75
|
+
if (ch === "\\" && i + 1 < entry.length) {
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === "{") depth++;
|
|
80
|
+
else if (ch === "}") {
|
|
81
|
+
if (depth > 0) depth--;
|
|
82
|
+
} else if (ch === "," && depth === 0) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
export interface SearchToolDetails {
|
|
67
90
|
truncation?: TruncationResult;
|
|
68
91
|
fileLimitReached?: number;
|
|
@@ -124,6 +147,11 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
124
147
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
125
148
|
throw new ToolError("Skip must be a non-negative number");
|
|
126
149
|
}
|
|
150
|
+
for (const entry of paths) {
|
|
151
|
+
if (containsTopLevelComma(entry)) {
|
|
152
|
+
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
127
155
|
const normalizedContextBefore = this.session.settings.get("search.contextBefore");
|
|
128
156
|
const normalizedContextAfter = this.session.settings.get("search.contextAfter");
|
|
129
157
|
const ignoreCase = i ?? false;
|
|
@@ -148,6 +176,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
148
176
|
missingPaths,
|
|
149
177
|
immutableSourcePaths,
|
|
150
178
|
} = scope;
|
|
179
|
+
if (missingPaths.length > 0 && missingPaths.length === paths.length) {
|
|
180
|
+
throw new ToolError(`Path not found: ${missingPaths.join(", ")}; pass each path as its own array element`);
|
|
181
|
+
}
|
|
151
182
|
const { globFilter } = scope;
|
|
152
183
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
153
184
|
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -150,9 +150,15 @@ function resolveTaskOrError(
|
|
|
150
150
|
}
|
|
151
151
|
const hit = findTaskByContent(phases, content);
|
|
152
152
|
if (!hit) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
if (/^task-\d+$/.test(content)) {
|
|
154
|
+
errors.push(
|
|
155
|
+
`Task "${content}" not found. Tasks are referenced by content, not by IDs — pass the task's full text from the previous result.`,
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
|
|
159
|
+
const hint = totalTasks === 0 ? " (todo list is empty — was it replaced or not yet created?)" : "";
|
|
160
|
+
errors.push(`Task "${content}" not found${hint}`);
|
|
161
|
+
}
|
|
156
162
|
}
|
|
157
163
|
return hit;
|
|
158
164
|
}
|
|
@@ -209,7 +215,7 @@ function appendItems(phases: TodoPhase[], entry: TodoOpEntryValue, errors: strin
|
|
|
209
215
|
for (const content of entry.items) {
|
|
210
216
|
if (findTaskByContent(phases, content)) {
|
|
211
217
|
errors.push(`Task "${content}" already exists`);
|
|
212
|
-
|
|
218
|
+
return phases;
|
|
213
219
|
}
|
|
214
220
|
phase.tasks.push({ content, status: "pending" });
|
|
215
221
|
}
|
|
@@ -513,6 +519,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
513
519
|
return {
|
|
514
520
|
content: [{ type: "text", text: formatSummary(updated, errors) }],
|
|
515
521
|
details: { phases: updated, storage },
|
|
522
|
+
isError: errors.length > 0 ? true : undefined,
|
|
516
523
|
};
|
|
517
524
|
}
|
|
518
525
|
}
|
|
@@ -10,7 +10,7 @@ export interface ToolTimeoutConfig {
|
|
|
10
10
|
export const TOOL_TIMEOUTS = {
|
|
11
11
|
bash: { default: 300, min: 1, max: 3600 },
|
|
12
12
|
eval: { default: 30, min: 1, max: 600 },
|
|
13
|
-
browser: { default: 30, min: 1, max:
|
|
13
|
+
browser: { default: 30, min: 1, max: 300 },
|
|
14
14
|
ssh: { default: 60, min: 1, max: 3600 },
|
|
15
15
|
fetch: { default: 20, min: 1, max: 45 },
|
|
16
16
|
lsp: { default: 20, min: 5, max: 60 },
|
|
@@ -248,31 +248,38 @@ async function downloadTool(tool: ToolName, signal?: AbortSignal): Promise<strin
|
|
|
248
248
|
|
|
249
249
|
// Install a Python package via uv (preferred) or pip
|
|
250
250
|
async function installPythonPackage(pkg: string, signal?: AbortSignal): Promise<boolean> {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
251
|
+
try {
|
|
252
|
+
// Try uv first (faster, better isolation)
|
|
253
|
+
const uv = $which("uv");
|
|
254
|
+
if (uv) {
|
|
255
|
+
const result = await ptree.exec([uv, "tool", "install", pkg], {
|
|
256
|
+
signal,
|
|
257
|
+
allowNonZero: true,
|
|
258
|
+
allowAbort: true,
|
|
259
|
+
stderr: "full",
|
|
260
|
+
});
|
|
261
|
+
if (result.exitCode === 0) return true;
|
|
262
|
+
}
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
264
|
+
// Fall back to pip
|
|
265
|
+
const pip = $which("pip3") || $which("pip");
|
|
266
|
+
if (pip) {
|
|
267
|
+
const result = await ptree.exec([pip, "install", "--user", pkg], {
|
|
268
|
+
signal,
|
|
269
|
+
allowNonZero: true,
|
|
270
|
+
allowAbort: true,
|
|
271
|
+
stderr: "full",
|
|
272
|
+
});
|
|
273
|
+
return result.exitCode === 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return false;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
logger.warn(`Failed to install Python package ${pkg}`, {
|
|
279
|
+
error: error instanceof Error ? error.message : String(error),
|
|
271
280
|
});
|
|
272
|
-
return
|
|
281
|
+
return false;
|
|
273
282
|
}
|
|
274
|
-
|
|
275
|
-
return false;
|
|
276
283
|
}
|
|
277
284
|
|
|
278
285
|
// Termux package names for tools
|
|
@@ -425,6 +425,9 @@ async function callCodexSearch(
|
|
|
425
425
|
|
|
426
426
|
const finalAnswer = answerParts.join("\n\n").trim();
|
|
427
427
|
const streamedAnswer = streamedAnswerParts.join("").trim();
|
|
428
|
+
if (isImagePlaceholderAnswer(finalAnswer) && streamedAnswer.length === 0) {
|
|
429
|
+
throw new SearchProviderError("codex", "Codex returned image-only response", 502);
|
|
430
|
+
}
|
|
428
431
|
const answer =
|
|
429
432
|
finalAnswer.length > 0 && !isImagePlaceholderAnswer(finalAnswer)
|
|
430
433
|
? finalAnswer
|