@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.0
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 +84 -1
- package/package.json +7 -7
- package/src/autoresearch/prompt.md +1 -1
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +4 -4
- package/src/cursor.ts +3 -8
- package/src/discovery/helpers.ts +3 -3
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +86 -57
- package/src/edit/line-hash.ts +735 -19
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +658 -0
- package/src/edit/modes/chunk.ts +14 -24
- package/src/edit/modes/hashline.ts +188 -136
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +14 -10
- package/src/edit/streaming.ts +50 -16
- package/src/exec/bash-executor.ts +2 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/components/assistant-message.ts +4 -0
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +1 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/print-mode.ts +8 -0
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +15 -19
- package/src/prompts/tools/ast-grep.md +18 -24
- package/src/prompts/tools/atom.md +96 -0
- package/src/prompts/tools/chunk-edit.md +37 -161
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +4 -5
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +2 -3
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +14 -9
- package/src/session/agent-session.ts +25 -2
- package/src/task/executor.ts +43 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +8 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +509 -697
- package/src/tools/grep.ts +115 -130
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +9 -8
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -21
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -12
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/task/executor.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
|
|
|
18
18
|
import type { Skill } from "../extensibility/skills";
|
|
19
19
|
import { callTool } from "../mcp/client";
|
|
20
20
|
import type { MCPManager } from "../mcp/manager";
|
|
21
|
-
import submitReminderTemplate from "../prompts/system/subagent-submit-reminder.md" with { type: "text" };
|
|
22
21
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
22
|
+
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
23
23
|
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
24
24
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
25
25
|
import type { AuthStorage } from "../session/auth-storage";
|
|
@@ -223,7 +223,7 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
223
223
|
return { data: candidate };
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
export interface
|
|
226
|
+
export interface YieldItem {
|
|
227
227
|
data?: unknown;
|
|
228
228
|
status?: "success" | "aborted";
|
|
229
229
|
error?: string;
|
|
@@ -235,7 +235,7 @@ interface FinalizeSubprocessOutputArgs {
|
|
|
235
235
|
stderr: string;
|
|
236
236
|
doneAborted: boolean;
|
|
237
237
|
signalAborted: boolean;
|
|
238
|
-
|
|
238
|
+
yieldItems?: YieldItem[];
|
|
239
239
|
reportFindings?: ReviewFinding[];
|
|
240
240
|
outputSchema: unknown;
|
|
241
241
|
}
|
|
@@ -244,44 +244,42 @@ interface FinalizeSubprocessOutputResult {
|
|
|
244
244
|
rawOutput: string;
|
|
245
245
|
exitCode: number;
|
|
246
246
|
stderr: string;
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
abortedViaYield: boolean;
|
|
248
|
+
hasYield: boolean;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
export const
|
|
252
|
-
export const
|
|
253
|
-
"SYSTEM WARNING: Subagent exited without calling
|
|
251
|
+
export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
|
|
252
|
+
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
253
|
+
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
|
|
254
254
|
|
|
255
255
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
256
256
|
let { rawOutput, exitCode, stderr } = args;
|
|
257
|
-
const {
|
|
258
|
-
let
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
257
|
+
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
258
|
+
let abortedViaYield = false;
|
|
259
|
+
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
|
|
260
|
+
|
|
261
|
+
if (hasYield) {
|
|
262
|
+
const lastYield = yieldItems[yieldItems.length - 1];
|
|
263
|
+
if (lastYield?.status === "aborted") {
|
|
264
|
+
abortedViaYield = true;
|
|
265
265
|
exitCode = 0;
|
|
266
|
-
stderr =
|
|
266
|
+
stderr = lastYield.error || "Subagent aborted task";
|
|
267
267
|
try {
|
|
268
|
-
rawOutput = JSON.stringify({ aborted: true, error:
|
|
268
|
+
rawOutput = JSON.stringify({ aborted: true, error: lastYield.error }, null, 2);
|
|
269
269
|
} catch {
|
|
270
|
-
rawOutput = `{"aborted":true,"error":"${
|
|
270
|
+
rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
|
|
271
271
|
}
|
|
272
272
|
} else {
|
|
273
|
-
const submitData =
|
|
273
|
+
const submitData = lastYield?.data;
|
|
274
274
|
if (submitData === null || submitData === undefined) {
|
|
275
|
-
rawOutput = rawOutput
|
|
276
|
-
? `${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\n${rawOutput}`
|
|
277
|
-
: SUBAGENT_WARNING_NULL_SUBMIT_RESULT;
|
|
275
|
+
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
278
276
|
} else {
|
|
279
277
|
const completeData = normalizeCompleteData(submitData, reportFindings);
|
|
280
278
|
try {
|
|
281
279
|
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
282
280
|
} catch (err) {
|
|
283
281
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
284
|
-
rawOutput = `{"error":"Failed to serialize
|
|
282
|
+
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
285
283
|
}
|
|
286
284
|
exitCode = 0;
|
|
287
285
|
stderr = "";
|
|
@@ -307,17 +305,15 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
307
305
|
stderr = "";
|
|
308
306
|
} else if (exitCode === 0) {
|
|
309
307
|
const hasRawOutput = rawOutput.trim().length > 0;
|
|
310
|
-
rawOutput = rawOutput
|
|
311
|
-
? `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\n${rawOutput}`
|
|
312
|
-
: SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
|
|
308
|
+
rawOutput = rawOutput ? `${SUBAGENT_WARNING_MISSING_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_MISSING_YIELD;
|
|
313
309
|
if (hasOutputSchema || !hasRawOutput) {
|
|
314
310
|
exitCode = 1;
|
|
315
|
-
stderr =
|
|
311
|
+
stderr = SUBAGENT_WARNING_MISSING_YIELD;
|
|
316
312
|
}
|
|
317
313
|
}
|
|
318
314
|
}
|
|
319
315
|
|
|
320
|
-
return { rawOutput, exitCode, stderr,
|
|
316
|
+
return { rawOutput, exitCode, stderr, abortedViaYield, hasYield };
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
/**
|
|
@@ -564,7 +560,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
564
560
|
const abortSignal = abortController.signal;
|
|
565
561
|
let activeSession: AgentSession | null = null;
|
|
566
562
|
let unsubscribe: (() => void) | null = null;
|
|
567
|
-
let
|
|
563
|
+
let yieldCalled = false;
|
|
568
564
|
|
|
569
565
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
570
566
|
const accumulatedUsage = {
|
|
@@ -789,8 +785,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
789
785
|
existing.push(data);
|
|
790
786
|
}
|
|
791
787
|
progress.extractedToolData[event.toolName] = existing;
|
|
792
|
-
if (event.toolName === "
|
|
793
|
-
|
|
788
|
+
if (event.toolName === "yield") {
|
|
789
|
+
yieldCalled = true;
|
|
794
790
|
}
|
|
795
791
|
}
|
|
796
792
|
}
|
|
@@ -955,7 +951,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
955
951
|
thinkingLevel: effectiveThinkingLevel,
|
|
956
952
|
toolNames,
|
|
957
953
|
outputSchema,
|
|
958
|
-
|
|
954
|
+
requireYieldTool: true,
|
|
959
955
|
contextFiles: options.contextFiles,
|
|
960
956
|
skills: options.skills,
|
|
961
957
|
promptTemplates: options.promptTemplates,
|
|
@@ -1070,7 +1066,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1070
1066
|
await extensionRunner.emit({ type: "session_start" });
|
|
1071
1067
|
}
|
|
1072
1068
|
|
|
1073
|
-
const
|
|
1069
|
+
const MAX_YIELD_RETRIES = 3;
|
|
1074
1070
|
unsubscribe = session.subscribe(event => {
|
|
1075
1071
|
if (isAgentEvent(event)) {
|
|
1076
1072
|
try {
|
|
@@ -1087,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1087
1083
|
await session.prompt(task, { attribution: "agent" });
|
|
1088
1084
|
await session.waitForIdle();
|
|
1089
1085
|
|
|
1090
|
-
const reminderToolChoice = buildNamedToolChoice("
|
|
1086
|
+
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1091
1087
|
|
|
1092
1088
|
let retryCount = 0;
|
|
1093
|
-
while (!
|
|
1089
|
+
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1094
1090
|
try {
|
|
1095
1091
|
retryCount++;
|
|
1096
1092
|
const reminder = prompt.render(submitReminderTemplate, {
|
|
1097
1093
|
retryCount,
|
|
1098
|
-
maxRetries:
|
|
1094
|
+
maxRetries: MAX_YIELD_RETRIES,
|
|
1099
1095
|
});
|
|
1100
1096
|
|
|
1101
1097
|
await session.prompt(reminder, {
|
|
@@ -1111,7 +1107,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1111
1107
|
}
|
|
1112
1108
|
|
|
1113
1109
|
await session.waitForIdle();
|
|
1114
|
-
if (!
|
|
1110
|
+
if (!yieldCalled && !abortSignal.aborted) {
|
|
1115
1111
|
exitCode = 0;
|
|
1116
1112
|
}
|
|
1117
1113
|
|
|
@@ -1186,7 +1182,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1186
1182
|
|
|
1187
1183
|
// Use final output if available, otherwise accumulated output
|
|
1188
1184
|
let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
|
|
1189
|
-
const
|
|
1185
|
+
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
|
|
1190
1186
|
const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
|
|
1191
1187
|
const finalized = finalizeSubprocessOutput({
|
|
1192
1188
|
rawOutput,
|
|
@@ -1194,17 +1190,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1194
1190
|
stderr,
|
|
1195
1191
|
doneAborted: Boolean(done.aborted),
|
|
1196
1192
|
signalAborted: Boolean(signal?.aborted),
|
|
1197
|
-
|
|
1193
|
+
yieldItems,
|
|
1198
1194
|
reportFindings,
|
|
1199
1195
|
outputSchema,
|
|
1200
1196
|
});
|
|
1201
1197
|
rawOutput = finalized.rawOutput;
|
|
1202
1198
|
exitCode = finalized.exitCode;
|
|
1203
1199
|
stderr = finalized.stderr;
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
const { abortedViaSubmitResult, hasSubmitResult } = finalized;
|
|
1200
|
+
const lastYield = yieldItems?.[yieldItems.length - 1];
|
|
1201
|
+
const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
|
|
1202
|
+
const { abortedViaYield, hasYield } = finalized;
|
|
1208
1203
|
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
|
|
1209
1204
|
maxBytes: MAX_OUTPUT_BYTES,
|
|
1210
1205
|
maxLines: MAX_OUTPUT_LINES,
|
|
@@ -1228,16 +1223,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1228
1223
|
}
|
|
1229
1224
|
|
|
1230
1225
|
// Update final progress
|
|
1231
|
-
const wasAborted =
|
|
1226
|
+
const wasAborted = abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
|
|
1232
1227
|
const finalAbortReason = wasAborted
|
|
1233
|
-
?
|
|
1234
|
-
?
|
|
1228
|
+
? abortedViaYield
|
|
1229
|
+
? yieldAbortReason
|
|
1235
1230
|
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
|
|
1236
1231
|
: undefined;
|
|
1237
1232
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1238
1233
|
scheduleProgress(true);
|
|
1239
1234
|
|
|
1240
|
-
// Emit lifecycle end event after finalization so
|
|
1235
|
+
// Emit lifecycle end event after finalization so yield status is reflected
|
|
1241
1236
|
if (options.eventBus) {
|
|
1242
1237
|
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1243
1238
|
id,
|
package/src/task/render.ts
CHANGED
|
@@ -101,12 +101,12 @@ function formatTaskId(id: string): string {
|
|
|
101
101
|
return `${indices} ${labels}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
104
|
+
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
|
|
105
105
|
|
|
106
|
-
function
|
|
106
|
+
function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
|
|
107
107
|
const lines = output.split("\n");
|
|
108
108
|
const firstLine = lines[0]?.trim() ?? "";
|
|
109
|
-
if (!firstLine.startsWith(
|
|
109
|
+
if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
|
|
110
110
|
return { rest: output };
|
|
111
111
|
}
|
|
112
112
|
const rest = lines
|
|
@@ -572,9 +572,9 @@ function renderAgentProgress(
|
|
|
572
572
|
|
|
573
573
|
// Render extracted tool data inline (e.g., review findings)
|
|
574
574
|
if (progress.extractedToolData) {
|
|
575
|
-
// For completed tasks, check for review verdict from
|
|
575
|
+
// For completed tasks, check for review verdict from yield tool
|
|
576
576
|
if (progress.status === "completed") {
|
|
577
|
-
const completeData = progress.extractedToolData.
|
|
577
|
+
const completeData = progress.extractedToolData.yield as Array<{ data: unknown }> | undefined;
|
|
578
578
|
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
579
579
|
const reviewData = completeData
|
|
580
580
|
?.map(c => c.data as SubmitReviewDetails)
|
|
@@ -731,9 +731,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
731
731
|
const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
|
|
732
732
|
const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
|
|
733
733
|
|
|
734
|
-
const { warning: missingCompleteWarning, rest: outputWithoutWarning } =
|
|
735
|
-
result.output,
|
|
736
|
-
);
|
|
734
|
+
const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
|
|
737
735
|
const aborted = result.aborted ?? false;
|
|
738
736
|
const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
|
|
739
737
|
const success = !aborted && result.exitCode === 0 && !result.error;
|
|
@@ -783,11 +781,11 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
783
781
|
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
|
|
784
782
|
);
|
|
785
783
|
}
|
|
786
|
-
// Check for review result (
|
|
787
|
-
const completeData = result.extractedToolData?.
|
|
784
|
+
// Check for review result (yield with review schema + report_finding)
|
|
785
|
+
const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
|
|
788
786
|
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
789
787
|
|
|
790
|
-
// Extract review verdict from
|
|
788
|
+
// Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
|
|
791
789
|
const reviewData = completeData
|
|
792
790
|
?.map(c => c.data as SubmitReviewDetails)
|
|
793
791
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
@@ -804,7 +802,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
804
802
|
const hasCompleteData = completeData && completeData.length > 0;
|
|
805
803
|
const message = hasCompleteData
|
|
806
804
|
? "Review verdict missing expected fields"
|
|
807
|
-
: "Review incomplete (
|
|
805
|
+
: "Review incomplete (yield not called)";
|
|
808
806
|
lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
|
|
809
807
|
lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
|
|
810
808
|
lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
|
|
@@ -817,7 +815,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
817
815
|
if (result.extractedToolData) {
|
|
818
816
|
for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
|
|
819
817
|
// Skip review tools - handled above
|
|
820
|
-
if (toolName === "
|
|
818
|
+
if (toolName === "yield" || toolName === "report_finding") continue;
|
|
821
819
|
|
|
822
820
|
const handler = subprocessToolRegistry.getHandler(toolName);
|
|
823
821
|
if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
|
package/src/tools/ask.ts
CHANGED
|
@@ -32,19 +32,19 @@ import { ToolAbortError } from "./tool-errors";
|
|
|
32
32
|
// =============================================================================
|
|
33
33
|
|
|
34
34
|
const OptionItem = Type.Object({
|
|
35
|
-
label: Type.String({ description: "
|
|
35
|
+
label: Type.String({ description: "display label" }),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
const QuestionItem = Type.Object({
|
|
39
|
-
id: Type.String({ description: "
|
|
40
|
-
question: Type.String({ description: "
|
|
41
|
-
options: Type.Array(OptionItem, { description: "
|
|
42
|
-
multi: Type.Optional(Type.Boolean({ description: "
|
|
43
|
-
recommended: Type.Optional(Type.Number({ description: "
|
|
39
|
+
id: Type.String({ description: "question id", examples: ["auth", "cache"] }),
|
|
40
|
+
question: Type.String({ description: "question text" }),
|
|
41
|
+
options: Type.Array(OptionItem, { description: "available options" }),
|
|
42
|
+
multi: Type.Optional(Type.Boolean({ description: "allow multiple selections" })),
|
|
43
|
+
recommended: Type.Optional(Type.Number({ description: "recommended option index" })),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
const askSchema = Type.Object({
|
|
47
|
-
questions: Type.Array(QuestionItem, { description: "
|
|
47
|
+
questions: Type.Array(QuestionItem, { description: "questions to ask", minItems: 1 }),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
export type AskToolInput = Static<typeof askSchema>;
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import { computeLineHash } from "../edit/line-hash";
|
|
8
|
+
import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
@@ -15,7 +15,6 @@ import type { ToolSession } from ".";
|
|
|
15
15
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import {
|
|
18
|
-
combineSearchGlobs,
|
|
19
18
|
hasGlobPathChars,
|
|
20
19
|
normalizePathLikeInput,
|
|
21
20
|
parseSearchPath,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
} from "./path-utils";
|
|
25
24
|
import {
|
|
26
25
|
dedupeParseErrors,
|
|
26
|
+
formatCodeFrameLine,
|
|
27
27
|
formatCount,
|
|
28
28
|
formatEmptyMessage,
|
|
29
29
|
formatErrorMessage,
|
|
@@ -36,20 +36,19 @@ import { ToolError } from "./tool-errors";
|
|
|
36
36
|
import { toolResult } from "./tool-result";
|
|
37
37
|
|
|
38
38
|
const astEditOpSchema = Type.Object({
|
|
39
|
-
pat: Type.String({ description: "
|
|
40
|
-
out: Type.String({ description: "
|
|
39
|
+
pat: Type.String({ description: "ast pattern", examples: ["oldFn($$$ARGS)"] }),
|
|
40
|
+
out: Type.String({ description: "replacement template", examples: ["newFn($$$ARGS)"] }),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
const astEditSchema = Type.Object({
|
|
44
44
|
ops: Type.Array(astEditOpSchema, {
|
|
45
45
|
minItems: 1,
|
|
46
|
-
description: "
|
|
46
|
+
description: "rewrite ops",
|
|
47
|
+
}),
|
|
48
|
+
path: Type.String({
|
|
49
|
+
description: "file, directory, glob, or comma-separated paths to rewrite",
|
|
50
|
+
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
|
47
51
|
}),
|
|
48
|
-
lang: Type.Optional(Type.String({ description: "Language override" })),
|
|
49
|
-
path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
|
|
50
|
-
glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
|
|
51
|
-
sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
|
|
52
|
-
limit: Type.Optional(Type.Number({ description: "Max total replacements" })),
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
export interface AstEditToolDetails {
|
|
@@ -63,6 +62,9 @@ export interface AstEditToolDetails {
|
|
|
63
62
|
files?: string[];
|
|
64
63
|
fileReplacements?: Array<{ path: string; count: number }>;
|
|
65
64
|
meta?: OutputMeta;
|
|
65
|
+
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
66
|
+
* a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
|
|
67
|
+
displayContent?: string;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
|
@@ -101,10 +103,6 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
101
103
|
seenPatterns.add(pat);
|
|
102
104
|
}
|
|
103
105
|
const normalizedRewrites = Object.fromEntries(ops);
|
|
104
|
-
const maxReplacements = params.limit !== undefined ? Math.floor(params.limit) : undefined;
|
|
105
|
-
if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
|
|
106
|
-
throw new ToolError("limit must be a positive number");
|
|
107
|
-
}
|
|
108
106
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
109
107
|
|
|
110
108
|
const formatScopePath = (targetPath: string): string => {
|
|
@@ -113,8 +111,11 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
113
111
|
};
|
|
114
112
|
let searchPath: string | undefined;
|
|
115
113
|
let scopePath: string | undefined;
|
|
116
|
-
let globFilter
|
|
117
|
-
const rawPath =
|
|
114
|
+
let globFilter: string | undefined;
|
|
115
|
+
const rawPath = normalizePathLikeInput(params.path);
|
|
116
|
+
if (rawPath.length === 0) {
|
|
117
|
+
throw new ToolError("`path` must be a non-empty path or glob");
|
|
118
|
+
}
|
|
118
119
|
if (rawPath) {
|
|
119
120
|
const internalRouter = this.session.internalRouter;
|
|
120
121
|
if (internalRouter?.canHandle(rawPath)) {
|
|
@@ -136,7 +137,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
136
137
|
} else {
|
|
137
138
|
const parsedPath = parseSearchPath(rawPath);
|
|
138
139
|
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
139
|
-
globFilter =
|
|
140
|
+
globFilter = parsedPath.glob;
|
|
140
141
|
scopePath = formatScopePath(searchPath);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -153,12 +154,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
153
154
|
|
|
154
155
|
const result = await astEdit({
|
|
155
156
|
rewrites: normalizedRewrites,
|
|
156
|
-
lang: params.lang?.trim(),
|
|
157
157
|
path: resolvedSearchPath,
|
|
158
158
|
glob: globFilter,
|
|
159
|
-
selector: params.sel?.trim(),
|
|
160
159
|
dryRun: true,
|
|
161
|
-
maxReplacements,
|
|
162
160
|
maxFiles,
|
|
163
161
|
failOnParseError: false,
|
|
164
162
|
signal,
|
|
@@ -205,24 +203,29 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
205
203
|
|
|
206
204
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
207
205
|
const outputLines: string[] = [];
|
|
206
|
+
const displayLines: string[] = [];
|
|
208
207
|
const renderChangesForFile = (relativePath: string) => {
|
|
209
208
|
const fileChanges = changesByFile.get(relativePath) ?? [];
|
|
210
|
-
const
|
|
211
|
-
|
|
209
|
+
const lineNumberWidth = fileChanges.reduce(
|
|
210
|
+
(width, change) => Math.max(width, String(change.startLine).length),
|
|
211
|
+
0,
|
|
212
|
+
);
|
|
212
213
|
for (const change of fileChanges) {
|
|
213
214
|
const beforeFirstLine = change.before.split("\n", 1)[0] ?? "";
|
|
214
215
|
const afterFirstLine = change.after.split("\n", 1)[0] ?? "";
|
|
215
216
|
const beforeLine = beforeFirstLine.slice(0, 120);
|
|
216
217
|
const afterLine = afterFirstLine.slice(0, 120);
|
|
217
218
|
const beforeRef = useHashLines
|
|
218
|
-
? `${change.startLine}
|
|
219
|
-
: `${change.startLine
|
|
219
|
+
? `${change.startLine}${computeLineHash(change.startLine, beforeFirstLine)}`
|
|
220
|
+
: `${change.startLine}:${change.startColumn}`;
|
|
220
221
|
const afterRef = useHashLines
|
|
221
|
-
? `${change.startLine}
|
|
222
|
-
: `${change.startLine
|
|
223
|
-
const lineSeparator = useHashLines ?
|
|
222
|
+
? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
|
|
223
|
+
: `${change.startLine}:${change.startColumn}`;
|
|
224
|
+
const lineSeparator = useHashLines ? HASHLINE_CONTENT_SEPARATOR : " ";
|
|
224
225
|
outputLines.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
|
|
225
226
|
outputLines.push(`+${afterRef}${lineSeparator}${afterLine}`);
|
|
227
|
+
displayLines.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
|
|
228
|
+
displayLines.push(formatCodeFrameLine("+", change.startLine, afterLine, lineNumberWidth));
|
|
226
229
|
}
|
|
227
230
|
};
|
|
228
231
|
|
|
@@ -240,20 +243,28 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
240
243
|
for (const relativePath of directoryFiles) {
|
|
241
244
|
if (outputLines.length > 0) {
|
|
242
245
|
outputLines.push("");
|
|
246
|
+
displayLines.push("");
|
|
243
247
|
}
|
|
244
248
|
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
245
|
-
|
|
249
|
+
const header = `# ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
250
|
+
outputLines.push(header);
|
|
251
|
+
displayLines.push(header);
|
|
246
252
|
renderChangesForFile(relativePath);
|
|
247
253
|
}
|
|
248
254
|
continue;
|
|
249
255
|
}
|
|
250
256
|
if (outputLines.length > 0) {
|
|
251
257
|
outputLines.push("");
|
|
258
|
+
displayLines.push("");
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
const dirHeader = `# ${directory}`;
|
|
261
|
+
outputLines.push(dirHeader);
|
|
262
|
+
displayLines.push(dirHeader);
|
|
254
263
|
for (const relativePath of directoryFiles) {
|
|
255
264
|
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
256
|
-
|
|
265
|
+
const fileHeader = `## └─ ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
266
|
+
outputLines.push(fileHeader);
|
|
267
|
+
displayLines.push(fileHeader);
|
|
257
268
|
renderChangesForFile(relativePath);
|
|
258
269
|
}
|
|
259
270
|
}
|
|
@@ -268,7 +279,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
268
279
|
count: fileReplacementCounts.get(filePath) ?? 0,
|
|
269
280
|
}));
|
|
270
281
|
if (result.limitReached) {
|
|
271
|
-
outputLines.push("", "Limit reached; narrow path
|
|
282
|
+
outputLines.push("", "Limit reached; narrow path.");
|
|
272
283
|
}
|
|
273
284
|
if (dedupedParseErrors.length) {
|
|
274
285
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -284,12 +295,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
284
295
|
apply: async (_reason: string) => {
|
|
285
296
|
const applyResult = await astEdit({
|
|
286
297
|
rewrites: normalizedRewrites,
|
|
287
|
-
lang: params.lang?.trim(),
|
|
288
298
|
path: resolvedSearchPath,
|
|
289
299
|
glob: globFilter,
|
|
290
|
-
selector: params.sel?.trim(),
|
|
291
300
|
dryRun: false,
|
|
292
|
-
maxReplacements,
|
|
293
301
|
maxFiles,
|
|
294
302
|
failOnParseError: false,
|
|
295
303
|
});
|
|
@@ -351,6 +359,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
351
359
|
const details: AstEditToolDetails = {
|
|
352
360
|
...baseDetails,
|
|
353
361
|
fileReplacements,
|
|
362
|
+
displayContent: displayLines.join("\n"),
|
|
354
363
|
};
|
|
355
364
|
return toolResult(details).text(outputLines.join("\n")).done();
|
|
356
365
|
});
|
|
@@ -363,10 +372,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
363
372
|
|
|
364
373
|
interface AstEditRenderArgs {
|
|
365
374
|
ops?: Array<{ pat?: string; out?: string }>;
|
|
366
|
-
lang?: string;
|
|
367
375
|
path?: string;
|
|
368
|
-
sel?: string;
|
|
369
|
-
limit?: number;
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -375,9 +381,7 @@ export const astEditToolRenderer = {
|
|
|
375
381
|
inline: true,
|
|
376
382
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
377
383
|
const meta: string[] = [];
|
|
378
|
-
if (args.lang) meta.push(`lang:${args.lang}`);
|
|
379
384
|
if (args.path) meta.push(`in ${args.path}`);
|
|
380
|
-
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
381
385
|
const rewriteCount = args.ops?.length ?? 0;
|
|
382
386
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
383
387
|
|
|
@@ -432,7 +436,7 @@ export const astEditToolRenderer = {
|
|
|
432
436
|
const rewriteCount = args?.ops?.length ?? 0;
|
|
433
437
|
const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
|
|
434
438
|
|
|
435
|
-
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
439
|
+
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
436
440
|
const rawLines = textContent.split("\n");
|
|
437
441
|
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
438
442
|
const allGroups: string[][] = [];
|
|
@@ -467,7 +471,7 @@ export const astEditToolRenderer = {
|
|
|
467
471
|
|
|
468
472
|
const extraLines: string[] = [];
|
|
469
473
|
if (limitReached) {
|
|
470
|
-
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path
|
|
474
|
+
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
|
|
471
475
|
}
|
|
472
476
|
if (details?.parseErrors?.length) {
|
|
473
477
|
const total = details.parseErrors.length;
|