@oh-my-pi/pi-coding-agent 11.2.3 → 11.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +119 -4
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/hooks/status-line.ts +1 -1
  5. package/examples/sdk/11-sessions.ts +1 -1
  6. package/package.json +8 -8
  7. package/src/cli/args.ts +9 -6
  8. package/src/cli/update-cli.ts +2 -2
  9. package/src/commands/index/index.ts +2 -5
  10. package/src/commit/agentic/agent.ts +1 -1
  11. package/src/commit/changelog/index.ts +2 -2
  12. package/src/config/keybindings.ts +16 -1
  13. package/src/config/model-registry.ts +25 -20
  14. package/src/config/model-resolver.ts +8 -8
  15. package/src/config/resolve-config-value.ts +92 -0
  16. package/src/config/settings-schema.ts +9 -0
  17. package/src/config.ts +14 -1
  18. package/src/export/html/template.css +7 -0
  19. package/src/export/html/template.generated.ts +1 -1
  20. package/src/export/html/template.js +33 -16
  21. package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
  22. package/src/extensibility/extensions/index.ts +18 -0
  23. package/src/extensibility/extensions/loader.ts +15 -0
  24. package/src/extensibility/extensions/runner.ts +78 -1
  25. package/src/extensibility/extensions/types.ts +131 -5
  26. package/src/extensibility/extensions/wrapper.ts +1 -1
  27. package/src/extensibility/plugins/git-url.ts +270 -0
  28. package/src/extensibility/plugins/index.ts +2 -0
  29. package/src/extensibility/slash-commands.ts +45 -0
  30. package/src/index.ts +7 -0
  31. package/src/lsp/render.ts +50 -43
  32. package/src/lsp/utils.ts +2 -2
  33. package/src/main.ts +11 -10
  34. package/src/mcp/transports/stdio.ts +3 -5
  35. package/src/modes/components/custom-message.ts +0 -8
  36. package/src/modes/components/diff.ts +41 -13
  37. package/src/modes/components/footer.ts +4 -4
  38. package/src/modes/components/model-selector.ts +4 -0
  39. package/src/modes/components/todo-display.ts +13 -3
  40. package/src/modes/components/tool-execution.ts +30 -16
  41. package/src/modes/components/tree-selector.ts +50 -19
  42. package/src/modes/controllers/event-controller.ts +1 -0
  43. package/src/modes/controllers/extension-ui-controller.ts +34 -2
  44. package/src/modes/controllers/input-controller.ts +47 -33
  45. package/src/modes/controllers/selector-controller.ts +10 -15
  46. package/src/modes/interactive-mode.ts +50 -38
  47. package/src/modes/print-mode.ts +6 -0
  48. package/src/modes/rpc/rpc-client.ts +4 -4
  49. package/src/modes/rpc/rpc-mode.ts +17 -2
  50. package/src/modes/rpc/rpc-types.ts +2 -2
  51. package/src/modes/types.ts +1 -0
  52. package/src/modes/utils/ui-helpers.ts +3 -1
  53. package/src/patch/applicator.ts +106 -4
  54. package/src/patch/fuzzy.ts +1 -1
  55. package/src/patch/shared.ts +77 -63
  56. package/src/prompts/system/plan-mode-active.md +6 -6
  57. package/src/prompts/system/system-prompt.md +2 -1
  58. package/src/prompts/tools/ask.md +2 -2
  59. package/src/prompts/tools/gemini-image.md +2 -2
  60. package/src/prompts/tools/lsp.md +2 -2
  61. package/src/prompts/tools/patch.md +1 -1
  62. package/src/prompts/tools/python.md +3 -3
  63. package/src/prompts/tools/task.md +7 -1
  64. package/src/prompts/tools/todo-write.md +2 -2
  65. package/src/prompts/tools/web-search.md +2 -2
  66. package/src/prompts/tools/write.md +2 -5
  67. package/src/sdk.ts +15 -11
  68. package/src/session/agent-session.ts +92 -34
  69. package/src/session/auth-storage.ts +2 -1
  70. package/src/session/blob-store.ts +105 -0
  71. package/src/session/session-manager.ts +107 -44
  72. package/src/task/executor.ts +19 -9
  73. package/src/task/render.ts +80 -58
  74. package/src/tools/ask.ts +28 -5
  75. package/src/tools/bash.ts +47 -39
  76. package/src/tools/browser.ts +248 -26
  77. package/src/tools/calculator.ts +42 -23
  78. package/src/tools/fetch.ts +33 -16
  79. package/src/tools/find.ts +57 -22
  80. package/src/tools/grep.ts +54 -25
  81. package/src/tools/index.ts +5 -5
  82. package/src/tools/notebook.ts +19 -6
  83. package/src/tools/path-utils.ts +26 -1
  84. package/src/tools/python.ts +20 -14
  85. package/src/tools/read.ts +21 -8
  86. package/src/tools/render-utils.ts +5 -45
  87. package/src/tools/ssh.ts +59 -53
  88. package/src/tools/submit-result.ts +2 -2
  89. package/src/tools/todo-write.ts +32 -14
  90. package/src/tools/truncate.ts +1 -1
  91. package/src/tools/write.ts +42 -26
  92. package/src/tui/code-cell.ts +1 -1
  93. package/src/tui/output-block.ts +61 -3
  94. package/src/tui/tree-list.ts +4 -4
  95. package/src/tui/utils.ts +71 -1
  96. package/src/utils/frontmatter.ts +1 -1
  97. package/src/utils/title-generator.ts +1 -1
  98. package/src/utils/tools-manager.ts +18 -2
  99. package/src/web/scrapers/osv.ts +4 -1
  100. package/src/web/scrapers/youtube.ts +1 -1
  101. package/src/web/search/index.ts +1 -1
  102. package/src/web/search/render.ts +96 -90
@@ -286,6 +286,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
286
286
  return Promise.resolve({ success: false, error: "Theme switching not supported in RPC mode" });
287
287
  }
288
288
 
289
+ getToolsExpanded() {
290
+ // Tool expansion not supported in RPC mode - no TUI
291
+ return false;
292
+ }
293
+
294
+ setToolsExpanded(_expanded: boolean) {
295
+ // Tool expansion not supported in RPC mode - no TUI
296
+ }
297
+
289
298
  setEditorComponent(): void {
290
299
  // Custom editor components not supported in RPC mode
291
300
  }
@@ -316,6 +325,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
316
325
  getActiveTools: () => session.getActiveToolNames(),
317
326
  getAllTools: () => session.getAllToolNames(),
318
327
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
328
+ getCommands: () => [],
319
329
  setModel: async model => {
320
330
  const key = await session.modelRegistry.getApiKey(model);
321
331
  if (!key) return false;
@@ -335,6 +345,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
335
345
  shutdownState.requested = true;
336
346
  },
337
347
  getContextUsage: () => session.getContextUsage(),
348
+ getSystemPrompt: () => session.systemPrompt,
338
349
  compact: async instructionsOrOptions => {
339
350
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
340
351
  const options =
@@ -364,6 +375,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
364
375
  const result = await session.navigateTree(targetId, { summarize: options?.summarize });
365
376
  return { cancelled: result.cancelled };
366
377
  },
378
+ switchSession: async sessionPath => {
379
+ const success = await session.switchSession(sessionPath);
380
+ return { cancelled: !success };
381
+ },
367
382
  compact: async instructionsOrOptions => {
368
383
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
369
384
  const options =
@@ -412,12 +427,12 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
412
427
  }
413
428
 
414
429
  case "steer": {
415
- await session.steer(command.message);
430
+ await session.steer(command.message, command.images);
416
431
  return success(id, "steer");
417
432
  }
418
433
 
419
434
  case "follow_up": {
420
- await session.followUp(command.message);
435
+ await session.followUp(command.message, command.images);
421
436
  return success(id, "follow_up");
422
437
  }
423
438
 
@@ -17,8 +17,8 @@ import type { CompactionResult } from "../../session/compaction";
17
17
  export type RpcCommand =
18
18
  // Prompting
19
19
  | { id?: string; type: "prompt"; message: string; images?: ImageContent[]; streamingBehavior?: "steer" | "followUp" }
20
- | { id?: string; type: "steer"; message: string }
21
- | { id?: string; type: "follow_up"; message: string }
20
+ | { id?: string; type: "steer"; message: string; images?: ImageContent[] }
21
+ | { id?: string; type: "follow_up"; message: string; images?: ImageContent[] }
22
22
  | { id?: string; type: "abort" }
23
23
  | { id?: string; type: "new_session"; parentSession?: string }
24
24
 
@@ -176,6 +176,7 @@ export interface InteractiveModeContext {
176
176
  cycleThinkingLevel(): void;
177
177
  cycleRoleModel(options?: { temporary?: boolean }): Promise<void>;
178
178
  toggleToolOutputExpansion(): void;
179
+ setToolsExpanded(expanded: boolean): void;
179
180
  toggleThinkingBlockVisibility(): void;
180
181
  openExternalEditor(): void;
181
182
  registerExtensionShortcuts(): void;
@@ -203,7 +203,9 @@ export class UiHelpers {
203
203
 
204
204
  // Render tool call components
205
205
  for (const content of message.content) {
206
- if (content.type !== "toolCall") continue;
206
+ if (content.type !== "toolCall") {
207
+ continue;
208
+ }
207
209
 
208
210
  if (content.name === "read") {
209
211
  if (!readGroup) {
@@ -126,6 +126,8 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
126
126
 
127
127
  let patternTabOnly = true;
128
128
  let actualSpaceOnly = true;
129
+ let patternSpaceOnly = true;
130
+ let actualTabOnly = true;
129
131
  let patternMixed = false;
130
132
  let actualMixed = false;
131
133
 
@@ -133,6 +135,7 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
133
135
  if (line.trim().length === 0) continue;
134
136
  const ws = getLeadingWhitespace(line);
135
137
  if (ws.includes(" ")) patternTabOnly = false;
138
+ if (ws.includes("\t")) patternSpaceOnly = false;
136
139
  if (ws.includes(" ") && ws.includes("\t")) patternMixed = true;
137
140
  }
138
141
 
@@ -140,6 +143,7 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
140
143
  if (line.trim().length === 0) continue;
141
144
  const ws = getLeadingWhitespace(line);
142
145
  if (ws.includes("\t")) actualSpaceOnly = false;
146
+ if (ws.includes(" ")) actualTabOnly = false;
143
147
  if (ws.includes(" ") && ws.includes("\t")) actualMixed = true;
144
148
  }
145
149
 
@@ -173,6 +177,88 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
173
177
  }
174
178
  }
175
179
 
180
+ // Reverse: pattern uses spaces, actual uses tabs — infer spaces = tabs * width + offset
181
+ // Collect (tabs, spaces) pairs from matched lines to solve for the model's tab rendering.
182
+ // With one data point: spaces = tabs * width (offset=0).
183
+ // With two+: solve ax + b via pairs with distinct tab counts.
184
+ if (!patternMixed && !actualMixed && patternSpaceOnly && actualTabOnly) {
185
+ const samples = new Map<number, number>(); // tabs -> spaces
186
+ const lineCount = Math.min(patternLines.length, actualLines.length);
187
+ let consistent = true;
188
+ for (let i = 0; i < lineCount; i++) {
189
+ const patternLine = patternLines[i];
190
+ const actualLine = actualLines[i];
191
+ if (patternLine.trim().length === 0 || actualLine.trim().length === 0) continue;
192
+ const spaces = countLeadingWhitespace(patternLine);
193
+ const tabs = countLeadingWhitespace(actualLine);
194
+ if (tabs === 0) continue;
195
+ const existing = samples.get(tabs);
196
+ if (existing !== undefined && existing !== spaces) {
197
+ consistent = false;
198
+ break;
199
+ }
200
+ samples.set(tabs, spaces);
201
+ }
202
+
203
+ if (consistent && samples.size > 0) {
204
+ let tabWidth: number | undefined;
205
+ let offset = 0;
206
+
207
+ if (samples.size === 1) {
208
+ // One level: assume offset=0, width = spaces / tabs
209
+ const [[tabs, spaces]] = samples;
210
+ if (spaces % tabs === 0) {
211
+ tabWidth = spaces / tabs;
212
+ }
213
+ } else {
214
+ // Two+ levels: solve via any two distinct pairs
215
+ // spaces = tabs * width + offset => width = (s2 - s1) / (t2 - t1)
216
+ const entries = [...samples.entries()];
217
+ const [t1, s1] = entries[0];
218
+ const [t2, s2] = entries[1];
219
+ if (t1 !== t2) {
220
+ const w = (s2 - s1) / (t2 - t1);
221
+ if (w > 0 && Number.isInteger(w)) {
222
+ const b = s1 - t1 * w;
223
+ // Validate all samples against this model
224
+ let valid = true;
225
+ for (const [t, s] of samples) {
226
+ if (t * w + b !== s) {
227
+ valid = false;
228
+ break;
229
+ }
230
+ }
231
+ if (valid) {
232
+ tabWidth = w;
233
+ offset = b;
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ if (tabWidth !== undefined && tabWidth > 0) {
240
+ const converted = newLines.map(line => {
241
+ if (line.trim().length === 0) return line;
242
+ const ws = countLeadingWhitespace(line);
243
+ if (ws === 0) return line;
244
+ // Reverse: tabs = (spaces - offset) / width
245
+ const adjusted = ws - offset;
246
+ if (adjusted >= 0 && adjusted % tabWidth! === 0) {
247
+ return "\t".repeat(adjusted / tabWidth!) + line.slice(ws);
248
+ }
249
+ // Partial tab — keep remainder as spaces
250
+ const tabCount = Math.floor(adjusted / tabWidth!);
251
+ const remainder = adjusted - tabCount * tabWidth!;
252
+ if (tabCount >= 0) {
253
+ return "\t".repeat(tabCount) + " ".repeat(remainder) + line.slice(ws);
254
+ }
255
+ return line;
256
+ });
257
+ return converted;
258
+ }
259
+ }
260
+ }
261
+
176
262
  // Build a map from trimmed content to actual lines (by content, not position)
177
263
  // This handles fuzzy matches where pattern and actual may not be positionally aligned
178
264
  const contentToActualLines = new Map<string, string[]>();
@@ -434,8 +520,7 @@ function formatSequenceMatchPreview(lines: string[], startIdx: number): string {
434
520
  return previewLines
435
521
  .map((line, i) => {
436
522
  const num = start + i + 1;
437
- const truncated =
438
- line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 3)}...` : line;
523
+ const truncated = line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 1)}…` : line;
439
524
  return ` ${num} | ${truncated}`;
440
525
  })
441
526
  .join("\n");
@@ -1103,7 +1188,7 @@ function computeReplacements(
1103
1188
  return lines
1104
1189
  .map((line, i) => {
1105
1190
  const num = start + i + 1;
1106
- const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 3)}...` : line;
1191
+ const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 1)}…` : line;
1107
1192
  return ` ${num} | ${truncated}`;
1108
1193
  })
1109
1194
  .join("\n");
@@ -1119,8 +1204,25 @@ function computeReplacements(
1119
1204
 
1120
1205
  // Adjust indentation if needed (handles fuzzy matches where indentation differs)
1121
1206
  const actualMatchedLines = originalLines.slice(found, found + pattern.length);
1122
- const adjustedNewLines = adjustLinesIndentation(pattern, actualMatchedLines, newSlice);
1123
1207
 
1208
+ // Skip pure-context hunks (no +/- lines — oldLines === newLines).
1209
+ // They serve only to advance lineIndex for subsequent hunks.
1210
+ let isNoOp = pattern.length === newSlice.length;
1211
+ if (isNoOp) {
1212
+ for (let i = 0; i < pattern.length; i++) {
1213
+ if (pattern[i] !== newSlice[i]) {
1214
+ isNoOp = false;
1215
+ break;
1216
+ }
1217
+ }
1218
+ }
1219
+
1220
+ if (isNoOp) {
1221
+ lineIndex = found + pattern.length;
1222
+ continue;
1223
+ }
1224
+
1225
+ const adjustedNewLines = adjustLinesIndentation(pattern, actualMatchedLines, newSlice);
1124
1226
  replacements.push({ startIndex: found, oldLen: pattern.length, newLines: adjustedNewLines });
1125
1227
  lineIndex = found + pattern.length;
1126
1228
  }
@@ -241,7 +241,7 @@ export function findMatch(
241
241
  const preview = previewLines
242
242
  .map((line, idx) => {
243
243
  const num = start + idx + 1;
244
- return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 3)}...` : line}`;
244
+ return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 1)}…` : line}`;
245
245
  })
246
246
  .join("\n");
247
247
  occurrencePreviews.push(preview);
@@ -13,12 +13,14 @@ import {
13
13
  formatExpandHint,
14
14
  formatStatusIcon,
15
15
  getDiffStats,
16
+ PREVIEW_LIMITS,
17
+ replaceTabs,
16
18
  shortenPath,
17
19
  ToolUIKit,
18
20
  truncateDiffByHunk,
19
21
  } from "../tools/render-utils";
20
22
  import type { RenderCallOptions } from "../tools/renderers";
21
- import { renderStatusLine } from "../tui";
23
+ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
22
24
  import type { DiffError, DiffResult, Operation } from "./types";
23
25
 
24
26
  // ═══════════════════════════════════════════════════════════════════════════
@@ -85,8 +87,6 @@ export interface EditRenderContext {
85
87
  renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
86
88
  }
87
89
 
88
- const EDIT_DIFF_PREVIEW_HUNKS = 2;
89
- const EDIT_DIFF_PREVIEW_LINES = 24;
90
90
  const EDIT_STREAMING_PREVIEW_LINES = 12;
91
91
 
92
92
  function countLines(text: string): number {
@@ -140,7 +140,7 @@ function renderDiffSection(
140
140
  hiddenLines,
141
141
  } = expanded
142
142
  ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
143
- : truncateDiffByHunk(diff, EDIT_DIFF_PREVIEW_HUNKS, EDIT_DIFF_PREVIEW_LINES);
143
+ : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
144
144
 
145
145
  text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
146
146
  if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
@@ -182,7 +182,7 @@ export const editToolRenderer = {
182
182
  const maxLines = 6;
183
183
  text += "\n\n";
184
184
  for (const line of previewLines.slice(0, maxLines)) {
185
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
185
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
186
186
  }
187
187
  if (previewLines.length > maxLines) {
188
188
  text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
@@ -192,7 +192,7 @@ export const editToolRenderer = {
192
192
  const maxLines = 6;
193
193
  text += "\n\n";
194
194
  for (const line of previewLines.slice(0, maxLines)) {
195
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
195
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
196
196
  }
197
197
  if (previewLines.length > maxLines) {
198
198
  text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
@@ -209,75 +209,89 @@ export const editToolRenderer = {
209
209
  args?: EditRenderArgs,
210
210
  ): Component {
211
211
  const ui = new ToolUIKit(uiTheme);
212
- const { expanded, renderContext } = options;
213
212
  const rawPath = args?.file_path || args?.path || "";
214
213
  const filePath = shortenPath(rawPath);
215
214
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
216
215
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
217
- const editDiffPreview = renderContext?.editDiffPreview;
218
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
219
216
 
220
- // Get op and rename from args or details
221
217
  const op = args?.op || result.details?.op;
222
218
  const rename = args?.rename || result.details?.rename;
219
+ const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
223
220
 
224
- // Build path display with line number if available
225
- let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
226
- const firstChangedLine =
227
- (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
228
- (result.details && !result.isError ? result.details.firstChangedLine : undefined);
229
- if (firstChangedLine) {
230
- pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
231
- }
221
+ // Pre-compute metadata line (static across renders)
222
+ const metadataLine =
223
+ op !== "delete"
224
+ ? `\n${formatMetadataLine(countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? ""), editLanguage, uiTheme)}`
225
+ : "";
232
226
 
233
- // Add arrow for rename operations
234
- if (rename) {
235
- pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
236
- }
227
+ // Pre-compute error text (static)
228
+ const errorText = result.isError ? (result.content?.find(c => c.type === "text")?.text ?? "") : "";
237
229
 
238
- // Show operation type for patch mode
239
- const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
240
- const header = renderStatusLine(
241
- {
242
- icon: result.isError ? "error" : "success",
243
- title: opTitle,
244
- description: `${editIcon} ${pathDisplay}`,
245
- },
246
- uiTheme,
247
- );
248
- let text = header;
249
-
250
- // Skip metadata line for delete operations
251
- if (op !== "delete") {
252
- const editLineCount = countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? "");
253
- text += `\n${formatMetadataLine(editLineCount, editLanguage, uiTheme)}`;
254
- }
230
+ let cached: RenderCache | undefined;
255
231
 
256
- if (result.isError) {
257
- // Show error from result
258
- const errorText = result.content?.find(c => c.type === "text")?.text ?? "";
259
- if (errorText) {
260
- text += `\n\n${uiTheme.fg("error", errorText)}`;
261
- }
262
- } else if (result.details?.diff) {
263
- // Prefer actual diff after execution
264
- text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
265
- } else if (editDiffPreview) {
266
- // Use cached diff preview when no actual diff is available
267
- if ("error" in editDiffPreview) {
268
- text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
269
- } else if (editDiffPreview.diff) {
270
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
271
- }
272
- }
232
+ return {
233
+ render(width) {
234
+ const { expanded, renderContext } = options;
235
+ const editDiffPreview = renderContext?.editDiffPreview;
236
+ const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
237
+ const key = new Hasher().bool(expanded).u32(width).digest();
238
+ if (cached?.key === key) return cached.lines;
273
239
 
274
- // Show LSP diagnostics if available
275
- if (result.details?.diagnostics) {
276
- text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
277
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
278
- );
279
- }
240
+ // Build path display with line number
241
+ let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
242
+ const firstChangedLine =
243
+ (editDiffPreview && "firstChangedLine" in editDiffPreview
244
+ ? editDiffPreview.firstChangedLine
245
+ : undefined) || (result.details && !result.isError ? result.details.firstChangedLine : undefined);
246
+ if (firstChangedLine) {
247
+ pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
248
+ }
280
249
 
281
- return new Text(text, 0, 0);
250
+ // Add arrow for rename operations
251
+ if (rename) {
252
+ pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
253
+ }
254
+
255
+ const header = renderStatusLine(
256
+ {
257
+ icon: result.isError ? "error" : "success",
258
+ title: opTitle,
259
+ description: `${editIcon} ${pathDisplay}`,
260
+ },
261
+ uiTheme,
262
+ );
263
+ let text = header;
264
+ text += metadataLine;
265
+
266
+ if (result.isError) {
267
+ if (errorText) {
268
+ text += `\n\n${uiTheme.fg("error", errorText)}`;
269
+ }
270
+ } else if (result.details?.diff) {
271
+ text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
272
+ } else if (editDiffPreview) {
273
+ if ("error" in editDiffPreview) {
274
+ text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
275
+ } else if (editDiffPreview.diff) {
276
+ text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
277
+ }
278
+ }
279
+
280
+ // Show LSP diagnostics if available
281
+ if (result.details?.diagnostics) {
282
+ text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
283
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
284
+ );
285
+ }
286
+
287
+ const lines =
288
+ width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n");
289
+ cached = { key, lines };
290
+ return lines;
291
+ },
292
+ invalidate() {
293
+ cached = undefined;
294
+ },
295
+ };
282
296
  },
283
297
  };
@@ -19,9 +19,9 @@ Create plan at `{{planFilePath}}`.
19
19
 
20
20
  Use `{{editToolName}}` incremental updates; `{{writeToolName}}` only create/full replace.
21
21
 
22
- <important>
22
+ <caution>
23
23
  Plan execution runs in fresh context (session cleared). Make plan file self-contained: include requirements, decisions, key findings, remaining todos needed to continue without prior session history.
24
- </important>
24
+ </caution>
25
25
 
26
26
  {{#if reentry}}
27
27
  ## Re-entry
@@ -56,7 +56,7 @@ Use `{{editToolName}}` update plan file as you learn; don't wait until end.
56
56
  - Smaller task → fewer or no questions
57
57
  </procedure>
58
58
 
59
- <important>
59
+ <caution>
60
60
  ### Plan Structure
61
61
 
62
62
  Use clear markdown headers; include:
@@ -65,7 +65,7 @@ Use clear markdown headers; include:
65
65
  - Verification: how to test end-to-end
66
66
 
67
67
  Concise enough to scan. Detailed enough to execute.
68
- </important>
68
+ </caution>
69
69
 
70
70
  {{else}}
71
71
  ## Planning Workflow
@@ -87,9 +87,9 @@ Update `{{planFilePath}}` (`{{editToolName}}` changes, `{{writeToolName}}` only
87
87
  - Verification section
88
88
  </procedure>
89
89
 
90
- <important>
90
+ <caution>
91
91
  Ask questions throughout. Don't make large assumptions about user intent.
92
- </important>
92
+ </caution>
93
93
  {{/if}}
94
94
 
95
95
  <directives>
@@ -4,7 +4,7 @@ XML tags prompt: system-level instructions, not suggestions.
4
4
  Tag hierarchy (enforcement):
5
5
  - `<critical>` — Inviolable; noncompliance = system failure.
6
6
  - `<prohibited>` — Forbidden; actions cause harm.
7
- - `<important>` — High priority; deviate only with justification.
7
+ - `<caution>` — High priority; important to follow.
8
8
  - `<instruction>` — Operating rules; follow precisely.
9
9
  - `<conditions>` — When rules apply; check before acting.
10
10
  - `<avoid>` — Anti-patterns; prefer alternatives.
@@ -221,6 +221,7 @@ Main branch: {{git.mainBranch}}
221
221
  {{#if skills.length}}
222
222
  <skills>
223
223
  Scan descriptions vs domain. Skill covers output? Read `skill://<name>` first.
224
+ When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.
224
225
 
225
226
  {{#list skills join="\n"}}
226
227
  <skill name="{{name}}">
@@ -16,9 +16,9 @@ Ask user when you need clarification or input during task execution.
16
16
  Returns selected option(s) as text. For multi-part questions, returns map of question IDs to selected values.
17
17
  </output>
18
18
 
19
- <important>
19
+ <caution>
20
20
  - Provide 2-5 concise, distinct options
21
- </important>
21
+ </caution>
22
22
 
23
23
  <critical>
24
24
  **Default to action. Do NOT ask unless you are genuinely blocked and user preference is required to avoid a wrong outcome.**
@@ -14,10 +14,10 @@ When using multiple `input_images`, describe each image's role in `subject` or `
14
14
  Returns generated image saved to disk. Response includes file path where image was written.
15
15
  </output>
16
16
 
17
- <important>
17
+ <caution>
18
18
  - For photoreal: add "ultra-detailed, realistic, natural skin texture" to style
19
19
  - For posters/cards: use 9:16 aspect ratio with negative space for text placement
20
20
  - For iteration: use `changes` for targeted adjustments rather than regenerating from scratch
21
21
  - For text: add "sharp, legible, correctly spelled" for important text; keep text short
22
22
  - For diagrams: include "scientifically accurate" in style and provide facts explicitly
23
- </important>
23
+ </caution>
@@ -22,7 +22,7 @@ Interact with Language Server Protocol servers for code intelligence.
22
22
  - `reload`: Confirmation of server restart
23
23
  </output>
24
24
 
25
- <important>
25
+ <caution>
26
26
  - Requires running LSP server for target language
27
27
  - Some operations require file to be saved to disk
28
- </important>
28
+ </caution>
@@ -48,7 +48,7 @@ Returns success/failure; on failure, error message indicates:
48
48
  - Never use anchors as comments (no line numbers, location labels, placeholders like `@@ @@`)
49
49
  - Do not place new lines outside intended block
50
50
  - If edit fails or breaks structure, re-read file and produce new patch from current content—do not retry same diff
51
- - If indentation/alignment wrong after editing, run formatter (`go fmt`, `cargo fmt`, `ruff format`, `biome`, etc.)never make repeated edit attempts to fix whitespace
51
+ - **NEVER** use edit to fix indentation or reformat coderun the project's formatter instead
52
52
  </critical>
53
53
 
54
54
  <example name="create">
@@ -41,13 +41,13 @@ User sees output like Jupyter notebook; rich displays render fully:
41
41
  - `display(HTML(...))` → rendered HTML
42
42
  - `display(Markdown(...))` → formatted markdown
43
43
  - `plt.show()` → inline figures
44
- **You will see object repr** (e.g., `<IPython.core.display.JSON object>`). Trust `display()`; do not assume user sees only repr.
44
+ **You will see object repr** (e.g., `<IPython.core.display.JSON object>`). Trust `display()`; do not assume user sees only repr.
45
45
  </output>
46
46
 
47
- <important>
47
+ <caution>
48
48
  - Per-call mode uses fresh kernel each call
49
49
  - Use `reset: true` to clear state when session mode active
50
- </important>
50
+ </caution>
51
51
 
52
52
  <critical>
53
53
  - Use `run()` for shell commands; never raw `subprocess`
@@ -70,6 +70,10 @@ Run in isolated git worktree; returns patches. Use when tasks edit overlapping f
70
70
  ### `schema` (optional — recommended for structured output)
71
71
 
72
72
  JTD schema defining expected response structure. Use typed properties. If you care about parsing result, define here — **never describe output format in `context` or `assignment`**.
73
+
74
+ <caution>
75
+ **Schema vs agent mismatch causes null output.** Agents with `output="structured"` (e.g., `explore`) have a built-in schema. If you also pass `schema`, yours takes precedence — but if you describe output format in `context`/`assignment` instead, the agent's built-in schema wins. The agent gets confused trying to fit your requested format into its schema shape and submits `null`. Either: (1) use `schema` to override the built-in one, (2) use `task` agent which has no built-in schema, or (3) match your instructions to the agent's expected output shape.
76
+ </caution>
73
77
  ---
74
78
 
75
79
  ## Writing an assignment
@@ -115,9 +119,11 @@ Use structure every assignment:
115
119
  - "No WASM."
116
120
 
117
121
  If tempted to write above, expand using templates.
122
+ **Output format in prose instead of `schema`** — agent returns null:
123
+ Structured agents (`explore`, `reviewer`) have built-in output schemas. Describing a different output format in `context`/`assignment` without overriding via `schema` creates a mismatch — the agent can't reconcile your prose instructions with its schema and submits null data. Always use `schema` for output structure, or pick an agent whose built-in schema matches your needs.
118
124
  **Test/lint commands in parallel tasks** — edit wars:
119
125
  Parallel agents share working tree. If two agents run `bun check` or `bun test` concurrently, they see each other's half-finished edits, "fix" phantom errors, loop. **Never tell parallel tasks run project-wide build/test/lint commands.** Each task edits, stops. Caller verifies after all tasks complete.
120
- **If you cant specify scope yet**, create **Discovery task** first: enumerate files, find callsites, list candidates. Then fan out with explicit paths.
126
+ **If you can't specify scope yet**, create **Discovery task** first: enumerate files, find callsites, list candidates. Then fan out with explicit paths.
121
127
 
122
128
  ### Delegate intent, not keystrokes
123
129
 
@@ -36,9 +36,9 @@ Use proactively:
36
36
  Returns confirmation todo list updated.
37
37
  </output>
38
38
 
39
- <important>
39
+ <caution>
40
40
  When in doubt, use this.
41
- </important>
41
+ </caution>
42
42
 
43
43
  <example name="use-dark-mode">
44
44
  User: Add dark mode toggle to settings. Run tests when done.
@@ -14,6 +14,6 @@ Returns search results formatted as blocks with:
14
14
  - Provider-dependent structure based on selected backend
15
15
  </output>
16
16
 
17
- <important>
17
+ <caution>
18
18
  Searches are performed automatically within a single API call—no pagination or follow-up requests needed.
19
- </important>
19
+ </caution>
@@ -14,8 +14,5 @@ Confirmation of file creation/write with path. When LSP available, content may b
14
14
  <critical>
15
15
  - Prefer Edit tool for modifying existing files (more precise, preserves formatting)
16
16
  - Create documentation files (*.md, README) only when explicitly requested
17
- </critical>
18
-
19
- <important>
20
- - Include emojis only when explicitly requested
21
- </important>
17
+ - No emojis unless requested
18
+ </critical>