@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +42 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +17 -6
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
package/src/task/index.ts
CHANGED
|
@@ -530,8 +530,8 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
530
530
|
});
|
|
531
531
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
532
532
|
|
|
533
|
-
// Output schema priority:
|
|
534
|
-
const effectiveOutputSchema = effectiveAgent.output ??
|
|
533
|
+
// Output schema priority: caller params > agent frontmatter > inherited from parent session
|
|
534
|
+
const effectiveOutputSchema = outputSchema ?? effectiveAgent.output ?? this.session.outputSchema;
|
|
535
535
|
|
|
536
536
|
// Handle empty or missing tasks
|
|
537
537
|
if (!params.tasks || params.tasks.length === 0) {
|
|
@@ -787,7 +787,6 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
787
787
|
},
|
|
788
788
|
authStorage: this.session.authStorage,
|
|
789
789
|
modelRegistry: this.session.modelRegistry,
|
|
790
|
-
searchDb: this.session.searchDb,
|
|
791
790
|
settings: this.session.settings,
|
|
792
791
|
mcpManager: this.session.mcpManager,
|
|
793
792
|
contextFiles,
|
|
@@ -841,7 +840,6 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
841
840
|
},
|
|
842
841
|
authStorage: this.session.authStorage,
|
|
843
842
|
modelRegistry: this.session.modelRegistry,
|
|
844
|
-
searchDb: this.session.searchDb,
|
|
845
843
|
settings: this.session.settings,
|
|
846
844
|
mcpManager: this.session.mcpManager,
|
|
847
845
|
contextFiles,
|
|
@@ -1116,8 +1114,8 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
1116
1114
|
}
|
|
1117
1115
|
|
|
1118
1116
|
// Build final output - match plugin format
|
|
1119
|
-
const successCount = results.filter(r => r.exitCode === 0 && !r.error).length;
|
|
1120
1117
|
const cancelledCount = results.filter(r => r.aborted).length;
|
|
1118
|
+
const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
|
|
1121
1119
|
const totalDuration = Date.now() - startTime;
|
|
1122
1120
|
|
|
1123
1121
|
const summaries = results.map(r => {
|
package/src/task/types.ts
CHANGED
|
@@ -82,9 +82,9 @@ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
|
|
|
82
82
|
}),
|
|
83
83
|
),
|
|
84
84
|
schema: Type.Optional(
|
|
85
|
-
Type.
|
|
85
|
+
Type.String({
|
|
86
86
|
description:
|
|
87
|
-
"JTD schema defining expected response structure.
|
|
87
|
+
"JSON-encoded JTD schema defining expected response structure. Output format belongs here — never in context or assignment.",
|
|
88
88
|
}),
|
|
89
89
|
),
|
|
90
90
|
tasks: Type.Array(taskItemSchema, {
|
package/src/tools/bash.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { clampTimeout } from "./tool-timeouts";
|
|
|
28
28
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
29
29
|
|
|
30
30
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
31
|
+
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
31
32
|
|
|
32
33
|
const bashSchemaBase = Type.Object({
|
|
33
34
|
command: Type.String({ description: "Command to execute" }),
|
|
@@ -72,6 +73,7 @@ export interface BashToolInput {
|
|
|
72
73
|
|
|
73
74
|
export interface BashToolDetails {
|
|
74
75
|
meta?: OutputMeta;
|
|
76
|
+
timeoutSeconds?: number;
|
|
75
77
|
async?: {
|
|
76
78
|
state: "running" | "completed" | "failed";
|
|
77
79
|
jobId: string;
|
|
@@ -81,6 +83,24 @@ export interface BashToolDetails {
|
|
|
81
83
|
|
|
82
84
|
export interface BashToolOptions {}
|
|
83
85
|
|
|
86
|
+
type ManagedBashJobCompletion =
|
|
87
|
+
| {
|
|
88
|
+
kind: "completed";
|
|
89
|
+
result: AgentToolResult<BashToolDetails>;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
kind: "failed";
|
|
93
|
+
error: unknown;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
interface ManagedBashJobHandle {
|
|
97
|
+
jobId: string;
|
|
98
|
+
label: string;
|
|
99
|
+
completion: Promise<ManagedBashJobCompletion>;
|
|
100
|
+
getLatestText: () => string;
|
|
101
|
+
setBackgrounded: (backgrounded: boolean) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
function normalizeResultOutput(result: BashResult | BashInteractiveResult): string {
|
|
85
105
|
return result.output || "";
|
|
86
106
|
}
|
|
@@ -212,12 +232,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
212
232
|
readonly concurrency = "exclusive";
|
|
213
233
|
readonly strict = true;
|
|
214
234
|
readonly #asyncEnabled: boolean;
|
|
235
|
+
readonly #autoBackgroundEnabled: boolean;
|
|
236
|
+
readonly #autoBackgroundThresholdMs: number;
|
|
215
237
|
|
|
216
238
|
constructor(private readonly session: ToolSession) {
|
|
217
239
|
this.#asyncEnabled = this.session.settings.get("async.enabled");
|
|
240
|
+
this.#autoBackgroundEnabled = this.session.settings.get("bash.autoBackground.enabled");
|
|
241
|
+
this.#autoBackgroundThresholdMs = Math.max(
|
|
242
|
+
0,
|
|
243
|
+
Math.floor(
|
|
244
|
+
this.session.settings.get("bash.autoBackground.thresholdMs") ?? DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
218
247
|
this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
|
|
219
248
|
this.description = prompt.render(bashDescription, {
|
|
220
249
|
asyncEnabled: this.#asyncEnabled,
|
|
250
|
+
autoBackgroundEnabled: this.#autoBackgroundEnabled,
|
|
251
|
+
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
|
221
252
|
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
222
253
|
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
223
254
|
hasGrep: this.session.settings.get("grep.enabled"),
|
|
@@ -253,6 +284,165 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
253
284
|
return outputText;
|
|
254
285
|
}
|
|
255
286
|
|
|
287
|
+
#buildCompletedResult(
|
|
288
|
+
result: BashResult | BashInteractiveResult,
|
|
289
|
+
timeoutSec: number,
|
|
290
|
+
headLines?: number,
|
|
291
|
+
tailLines?: number,
|
|
292
|
+
): AgentToolResult<BashToolDetails> {
|
|
293
|
+
const outputText = this.#formatResultOutput(result, headLines, tailLines);
|
|
294
|
+
const details: BashToolDetails = { timeoutSeconds: timeoutSec };
|
|
295
|
+
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
296
|
+
this.#buildResultText(result, timeoutSec, outputText);
|
|
297
|
+
return resultBuilder.done();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#buildBackgroundStartResult(
|
|
301
|
+
jobId: string,
|
|
302
|
+
label: string,
|
|
303
|
+
previewText: string,
|
|
304
|
+
timeoutSec: number,
|
|
305
|
+
): AgentToolResult<BashToolDetails> {
|
|
306
|
+
const details: BashToolDetails = {
|
|
307
|
+
timeoutSeconds: timeoutSec,
|
|
308
|
+
async: { state: "running", jobId, type: "bash" },
|
|
309
|
+
};
|
|
310
|
+
const lines: string[] = [];
|
|
311
|
+
const trimmedPreview = previewText.trimEnd();
|
|
312
|
+
if (trimmedPreview.length > 0) {
|
|
313
|
+
lines.push(trimmedPreview, "");
|
|
314
|
+
}
|
|
315
|
+
lines.push(`Background job ${jobId} started: ${label}`);
|
|
316
|
+
lines.push("Result will be delivered automatically when complete.");
|
|
317
|
+
lines.push(`Use \`poll\`, \`read jobs://${jobId}\`, or \`cancel_job\` if needed.`);
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
320
|
+
details,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#extractTextResult(result: AgentToolResult<BashToolDetails>): string {
|
|
325
|
+
return result.content.find(block => block.type === "text")?.text ?? "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#startManagedBashJob(options: {
|
|
329
|
+
command: string;
|
|
330
|
+
commandCwd: string;
|
|
331
|
+
timeoutMs: number;
|
|
332
|
+
timeoutSec: number;
|
|
333
|
+
headLines?: number;
|
|
334
|
+
tailLines?: number;
|
|
335
|
+
resolvedEnv?: Record<string, string>;
|
|
336
|
+
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
337
|
+
startBackgrounded: boolean;
|
|
338
|
+
}): ManagedBashJobHandle {
|
|
339
|
+
const manager = this.session.asyncJobManager;
|
|
340
|
+
if (!manager) {
|
|
341
|
+
throw new ToolError("Background job manager unavailable for this session.");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const label = options.command.length > 120 ? `${options.command.slice(0, 117)}...` : options.command;
|
|
345
|
+
let latestText = "";
|
|
346
|
+
let backgrounded = options.startBackgrounded;
|
|
347
|
+
const completion = Promise.withResolvers<ManagedBashJobCompletion>();
|
|
348
|
+
|
|
349
|
+
const jobId = manager.register(
|
|
350
|
+
"bash",
|
|
351
|
+
label,
|
|
352
|
+
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
353
|
+
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
354
|
+
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
355
|
+
try {
|
|
356
|
+
const result = await executeBash(options.command, {
|
|
357
|
+
cwd: options.commandCwd,
|
|
358
|
+
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
359
|
+
timeout: options.timeoutMs,
|
|
360
|
+
signal: runSignal,
|
|
361
|
+
env: options.resolvedEnv,
|
|
362
|
+
artifactPath,
|
|
363
|
+
artifactId,
|
|
364
|
+
onChunk: chunk => {
|
|
365
|
+
tailBuffer.append(chunk);
|
|
366
|
+
latestText = tailBuffer.text();
|
|
367
|
+
void reportProgress(latestText, { async: { state: "running", jobId, type: "bash" } });
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
const finalResult = this.#buildCompletedResult(
|
|
371
|
+
result,
|
|
372
|
+
options.timeoutSec,
|
|
373
|
+
options.headLines,
|
|
374
|
+
options.tailLines,
|
|
375
|
+
);
|
|
376
|
+
const finalText = this.#extractTextResult(finalResult);
|
|
377
|
+
latestText = finalText;
|
|
378
|
+
completion.resolve({ kind: "completed", result: finalResult });
|
|
379
|
+
await reportProgress(finalText, { async: { state: "completed", jobId, type: "bash" } });
|
|
380
|
+
return finalText;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
383
|
+
latestText = message;
|
|
384
|
+
completion.resolve({ kind: "failed", error });
|
|
385
|
+
await reportProgress(message, { async: { state: "failed", jobId, type: "bash" } });
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
onProgress: async (text, details) => {
|
|
391
|
+
latestText = text;
|
|
392
|
+
await options.onUpdate?.({
|
|
393
|
+
content: [{ type: "text", text }],
|
|
394
|
+
details: backgrounded ? ((details ?? {}) as BashToolDetails) : {},
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
jobId,
|
|
402
|
+
label,
|
|
403
|
+
completion: completion.promise,
|
|
404
|
+
getLatestText: () => latestText,
|
|
405
|
+
setBackgrounded: (nextBackgrounded: boolean) => {
|
|
406
|
+
backgrounded = nextBackgrounded;
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async #waitForManagedBashJob(
|
|
412
|
+
job: ManagedBashJobHandle,
|
|
413
|
+
thresholdMs: number,
|
|
414
|
+
signal?: AbortSignal,
|
|
415
|
+
): Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" }> {
|
|
416
|
+
if (signal?.aborted) {
|
|
417
|
+
return { kind: "aborted" };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const waiters: Array<Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" }>> = [
|
|
421
|
+
job.completion,
|
|
422
|
+
Bun.sleep(thresholdMs).then(() => ({ kind: "running" as const })),
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
if (!signal) {
|
|
426
|
+
return await Promise.race(waiters);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
|
|
430
|
+
const onAbort = () => resolveAborted({ kind: "aborted" });
|
|
431
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
432
|
+
waiters.push(abortedPromise);
|
|
433
|
+
try {
|
|
434
|
+
return await Promise.race(waiters);
|
|
435
|
+
} finally {
|
|
436
|
+
signal.removeEventListener("abort", onAbort);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#resolveAutoBackgroundWaitMs(timeoutMs: number): number {
|
|
441
|
+
if (this.#autoBackgroundThresholdMs <= 0) return 0;
|
|
442
|
+
const timeoutBufferMs = 1_000;
|
|
443
|
+
return Math.max(0, Math.min(this.#autoBackgroundThresholdMs, timeoutMs - timeoutBufferMs));
|
|
444
|
+
}
|
|
445
|
+
|
|
256
446
|
async execute(
|
|
257
447
|
_toolCallId: string,
|
|
258
448
|
{
|
|
@@ -345,52 +535,56 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
345
535
|
const timeoutMs = timeoutSec * 1000;
|
|
346
536
|
|
|
347
537
|
if (asyncRequested) {
|
|
348
|
-
|
|
349
|
-
if (!manager) {
|
|
538
|
+
if (!this.session.asyncJobManager) {
|
|
350
539
|
throw new ToolError("Async job manager unavailable for this session.");
|
|
351
540
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
541
|
+
const job = this.#startManagedBashJob({
|
|
542
|
+
command,
|
|
543
|
+
commandCwd,
|
|
544
|
+
timeoutMs,
|
|
545
|
+
timeoutSec,
|
|
546
|
+
headLines,
|
|
547
|
+
tailLines,
|
|
548
|
+
resolvedEnv,
|
|
549
|
+
onUpdate,
|
|
550
|
+
startBackgrounded: true,
|
|
551
|
+
});
|
|
552
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
|
|
556
|
+
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
557
|
+
const startBackgrounded = autoBackgroundWaitMs === 0;
|
|
558
|
+
const job = this.#startManagedBashJob({
|
|
559
|
+
command,
|
|
560
|
+
commandCwd,
|
|
561
|
+
timeoutMs,
|
|
562
|
+
timeoutSec,
|
|
563
|
+
headLines,
|
|
564
|
+
tailLines,
|
|
565
|
+
resolvedEnv,
|
|
566
|
+
onUpdate,
|
|
567
|
+
startBackgrounded,
|
|
568
|
+
});
|
|
569
|
+
if (startBackgrounded) {
|
|
570
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec);
|
|
571
|
+
}
|
|
572
|
+
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
573
|
+
if (waitResult.kind === "completed") {
|
|
574
|
+
this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
|
|
575
|
+
return waitResult.result;
|
|
576
|
+
}
|
|
577
|
+
if (waitResult.kind === "failed") {
|
|
578
|
+
this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
|
|
579
|
+
throw waitResult.error;
|
|
580
|
+
}
|
|
581
|
+
if (waitResult.kind === "aborted") {
|
|
582
|
+
this.session.asyncJobManager.cancel(job.jobId);
|
|
583
|
+
this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
|
|
584
|
+
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
585
|
+
}
|
|
586
|
+
job.setBackgrounded(true);
|
|
587
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec);
|
|
394
588
|
}
|
|
395
589
|
|
|
396
590
|
// Track output for streaming updates (tail only)
|
|
@@ -437,18 +631,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
437
631
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
438
632
|
throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
|
|
439
633
|
}
|
|
440
|
-
|
|
441
|
-
const outputText = this.#formatResultOutput(result, headLines, tailLines);
|
|
442
|
-
const details: BashToolDetails = {};
|
|
443
|
-
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
444
|
-
if (result.exitCode === undefined) {
|
|
445
|
-
throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
|
|
446
|
-
}
|
|
447
|
-
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
448
|
-
throw new ToolError(`${outputText}\n\nCommand exited with code ${result.exitCode}`);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return resultBuilder.done();
|
|
634
|
+
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines);
|
|
452
635
|
}
|
|
453
636
|
}
|
|
454
637
|
|
|
@@ -524,7 +707,7 @@ export const bashToolRenderer = {
|
|
|
524
707
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
525
708
|
|
|
526
709
|
// Build truncation warning
|
|
527
|
-
const timeoutSeconds = renderContext?.timeout;
|
|
710
|
+
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
528
711
|
const timeoutLine =
|
|
529
712
|
typeof timeoutSeconds === "number"
|
|
530
713
|
? uiTheme.fg(
|
package/src/tools/cancel-job.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { isBackgroundJobSupportEnabled } from "../async";
|
|
4
5
|
import cancelJobDescription from "../prompts/tools/cancel-job.md" with { type: "text" };
|
|
5
6
|
import type { ToolSession } from "./index";
|
|
6
7
|
|
|
@@ -27,7 +28,7 @@ export class CancelJobTool implements AgentTool<typeof cancelJobSchema, CancelJo
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
static createIf(session: ToolSession): CancelJobTool | null {
|
|
30
|
-
if (!session.settings
|
|
31
|
+
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
31
32
|
return new CancelJobTool(session);
|
|
32
33
|
}
|
|
33
34
|
|
package/src/tools/find.ts
CHANGED
|
@@ -244,17 +244,20 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
244
244
|
maxResults: effectiveLimit,
|
|
245
245
|
sortByMtime: true,
|
|
246
246
|
gitignore: useGitignore,
|
|
247
|
+
signal: combinedSignal,
|
|
247
248
|
},
|
|
248
249
|
onMatch,
|
|
249
|
-
this.session.searchDb,
|
|
250
250
|
),
|
|
251
251
|
);
|
|
252
252
|
|
|
253
253
|
try {
|
|
254
254
|
let result = await doGlob(true);
|
|
255
|
-
if (result.matches.length === 0) {
|
|
255
|
+
if (result.matches.length === 0 && !timeoutSignal.aborted) {
|
|
256
256
|
result = await doGlob(false);
|
|
257
257
|
}
|
|
258
|
+
// Sort by mtime descending (most recent first) in JS instead of native.
|
|
259
|
+
// This allows native glob to early-terminate at maxResults.
|
|
260
|
+
result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
258
261
|
matches = result.matches;
|
|
259
262
|
} catch (error) {
|
|
260
263
|
if (error instanceof Error && error.name === "AbortError") {
|
package/src/tools/grep.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
9
|
import { computeLineHash } from "../edit/line-hash";
|
|
10
|
-
import {
|
|
10
|
+
import { type ChunkedGrepMatch, describeChunkedGrepMatch } from "../edit/modes/chunk";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
13
13
|
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
|
@@ -162,7 +162,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
162
162
|
const stat = await Bun.file(searchPath).stat();
|
|
163
163
|
isDirectory = stat.isDirectory();
|
|
164
164
|
} catch {
|
|
165
|
-
|
|
165
|
+
const hint = scopePath.includes(",") ? ` (comma-separated paths must each exist relative to cwd)` : "";
|
|
166
|
+
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
@@ -191,7 +192,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
191
192
|
mode: effectiveOutputMode,
|
|
192
193
|
},
|
|
193
194
|
undefined,
|
|
194
|
-
this.session.searchDb,
|
|
195
195
|
);
|
|
196
196
|
} catch (err) {
|
|
197
197
|
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
|
@@ -274,13 +274,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
274
274
|
matchesByFile.get(relativePath)!.push(match);
|
|
275
275
|
}
|
|
276
276
|
if (chunkMode) {
|
|
277
|
-
const
|
|
277
|
+
const annotatedMatches = await Promise.all(
|
|
278
278
|
selectedMatches.map(match => {
|
|
279
279
|
const relativePath = match.path.startsWith("/") ? match.path.slice(1) : match.path;
|
|
280
280
|
const absoluteFilePath = isDirectory ? path.join(searchPath, relativePath) : searchPath;
|
|
281
|
-
|
|
282
|
-
fileMatchCounts.set(displayPath, (fileMatchCounts.get(displayPath) ?? 0) + 1);
|
|
283
|
-
return formatChunkedGrepLine({
|
|
281
|
+
return describeChunkedGrepMatch({
|
|
284
282
|
filePath: absoluteFilePath,
|
|
285
283
|
lineNumber: match.lineNumber,
|
|
286
284
|
line: match.line,
|
|
@@ -289,7 +287,78 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
289
287
|
});
|
|
290
288
|
}),
|
|
291
289
|
);
|
|
292
|
-
const
|
|
290
|
+
const chunkMatchesByFile = new Map<string, ChunkedGrepMatch[]>();
|
|
291
|
+
for (const match of annotatedMatches) {
|
|
292
|
+
recordFile(match.displayPath);
|
|
293
|
+
if (!chunkMatchesByFile.has(match.displayPath)) {
|
|
294
|
+
chunkMatchesByFile.set(match.displayPath, []);
|
|
295
|
+
}
|
|
296
|
+
chunkMatchesByFile.get(match.displayPath)!.push(match);
|
|
297
|
+
}
|
|
298
|
+
const renderChunkedMatchesForFile = (relativePath: string) => {
|
|
299
|
+
const fileMatches = chunkMatchesByFile.get(relativePath) ?? [];
|
|
300
|
+
if (fileMatches.length === 0) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const lineWidth = fileMatches[0]?.fileLineCount.toString().length ?? 1;
|
|
304
|
+
const matchesByChunk = new Map<string, ChunkedGrepMatch[]>();
|
|
305
|
+
for (const match of fileMatches) {
|
|
306
|
+
const chunkKey = match.chunkPath ?? "";
|
|
307
|
+
if (!matchesByChunk.has(chunkKey)) {
|
|
308
|
+
matchesByChunk.set(chunkKey, []);
|
|
309
|
+
}
|
|
310
|
+
matchesByChunk.get(chunkKey)!.push(match);
|
|
311
|
+
}
|
|
312
|
+
for (const [chunkPath, chunkMatches] of matchesByChunk) {
|
|
313
|
+
if (chunkPath) {
|
|
314
|
+
const chunkChecksum = chunkMatches[0]?.chunkChecksum;
|
|
315
|
+
const dashes = "-".repeat(chunkPath.split(".").length - 1);
|
|
316
|
+
const anchor = chunkChecksum
|
|
317
|
+
? `${dashes}@${chunkPath}#${chunkChecksum}`
|
|
318
|
+
: `${dashes}@${chunkPath}`;
|
|
319
|
+
outputLines.push(anchor);
|
|
320
|
+
}
|
|
321
|
+
for (const match of chunkMatches) {
|
|
322
|
+
outputLines.push(` ${match.lineNumber.toString().padStart(lineWidth, " ")} |${match.line}`);
|
|
323
|
+
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
if (isDirectory) {
|
|
328
|
+
const filesByDirectory = new Map<string, string[]>();
|
|
329
|
+
for (const relativePath of fileList) {
|
|
330
|
+
const directory = path.dirname(relativePath).replace(/\\/g, "/");
|
|
331
|
+
if (!filesByDirectory.has(directory)) {
|
|
332
|
+
filesByDirectory.set(directory, []);
|
|
333
|
+
}
|
|
334
|
+
filesByDirectory.get(directory)!.push(relativePath);
|
|
335
|
+
}
|
|
336
|
+
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
337
|
+
if (directory === ".") {
|
|
338
|
+
for (const relativePath of directoryFiles) {
|
|
339
|
+
if (outputLines.length > 0) {
|
|
340
|
+
outputLines.push("");
|
|
341
|
+
}
|
|
342
|
+
outputLines.push(`# ${path.basename(relativePath)}`);
|
|
343
|
+
renderChunkedMatchesForFile(relativePath);
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (outputLines.length > 0) {
|
|
348
|
+
outputLines.push("");
|
|
349
|
+
}
|
|
350
|
+
outputLines.push(`# ${directory}`);
|
|
351
|
+
for (const relativePath of directoryFiles) {
|
|
352
|
+
outputLines.push(`## └─ ${path.basename(relativePath)}`);
|
|
353
|
+
renderChunkedMatchesForFile(relativePath);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
for (const relativePath of fileList) {
|
|
358
|
+
renderChunkedMatchesForFile(relativePath);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const rawOutput = outputLines.join("\n");
|
|
293
362
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
294
363
|
const truncated = Boolean(matchLimitReached || result.limitReached || truncation.truncated);
|
|
295
364
|
const details: GrepToolDetails = {
|