@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +743 -24
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +893 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +193 -146
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/defaults.json +142 -652
  28. package/src/lsp/index.ts +1 -1
  29. package/src/mcp/render.ts +1 -8
  30. package/src/modes/components/assistant-message.ts +4 -0
  31. package/src/modes/components/diff.ts +23 -14
  32. package/src/modes/components/footer.ts +21 -16
  33. package/src/modes/components/session-selector.ts +3 -3
  34. package/src/modes/components/settings-defs.ts +6 -1
  35. package/src/modes/components/todo-reminder.ts +1 -8
  36. package/src/modes/components/tool-execution.ts +1 -4
  37. package/src/modes/controllers/selector-controller.ts +1 -1
  38. package/src/modes/print-mode.ts +8 -0
  39. package/src/prompts/agents/librarian.md +1 -1
  40. package/src/prompts/agents/reviewer.md +4 -4
  41. package/src/prompts/ci-green-request.md +1 -1
  42. package/src/prompts/review-request.md +1 -1
  43. package/src/prompts/system/subagent-system-prompt.md +3 -3
  44. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  45. package/src/prompts/system/system-prompt.md +3 -0
  46. package/src/prompts/tools/ask.md +3 -2
  47. package/src/prompts/tools/ast-edit.md +16 -20
  48. package/src/prompts/tools/ast-grep.md +19 -24
  49. package/src/prompts/tools/atom.md +87 -0
  50. package/src/prompts/tools/chunk-edit.md +37 -161
  51. package/src/prompts/tools/debug.md +4 -5
  52. package/src/prompts/tools/exit-plan-mode.md +4 -5
  53. package/src/prompts/tools/find.md +4 -8
  54. package/src/prompts/tools/github.md +18 -0
  55. package/src/prompts/tools/grep.md +4 -5
  56. package/src/prompts/tools/hashline.md +22 -89
  57. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  58. package/src/prompts/tools/inspect-image.md +6 -6
  59. package/src/prompts/tools/lsp.md +1 -1
  60. package/src/prompts/tools/patch.md +12 -19
  61. package/src/prompts/tools/python.md +3 -2
  62. package/src/prompts/tools/read-chunk.md +2 -3
  63. package/src/prompts/tools/read.md +2 -2
  64. package/src/prompts/tools/ssh.md +8 -17
  65. package/src/prompts/tools/todo-write.md +54 -41
  66. package/src/sdk.ts +14 -9
  67. package/src/session/agent-session.ts +25 -2
  68. package/src/session/session-manager.ts +4 -1
  69. package/src/task/executor.ts +43 -48
  70. package/src/task/render.ts +11 -13
  71. package/src/tools/ask.ts +7 -7
  72. package/src/tools/ast-edit.ts +45 -41
  73. package/src/tools/ast-grep.ts +77 -85
  74. package/src/tools/bash.ts +8 -9
  75. package/src/tools/browser.ts +32 -30
  76. package/src/tools/calculator.ts +4 -4
  77. package/src/tools/cancel-job.ts +1 -1
  78. package/src/tools/checkpoint.ts +2 -2
  79. package/src/tools/debug.ts +41 -37
  80. package/src/tools/exit-plan-mode.ts +1 -1
  81. package/src/tools/find.ts +4 -4
  82. package/src/tools/gh-renderer.ts +12 -4
  83. package/src/tools/gh.ts +509 -697
  84. package/src/tools/grep.ts +116 -131
  85. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  86. package/src/tools/index.ts +14 -32
  87. package/src/tools/inspect-image.ts +3 -3
  88. package/src/tools/json-tree.ts +114 -114
  89. package/src/tools/match-line-format.ts +8 -7
  90. package/src/tools/notebook.ts +8 -7
  91. package/src/tools/poll-tool.ts +2 -1
  92. package/src/tools/python.ts +9 -23
  93. package/src/tools/read.ts +32 -25
  94. package/src/tools/render-mermaid.ts +1 -1
  95. package/src/tools/render-utils.ts +18 -0
  96. package/src/tools/renderers.ts +2 -2
  97. package/src/tools/report-tool-issue.ts +3 -2
  98. package/src/tools/resolve.ts +1 -1
  99. package/src/tools/review.ts +12 -10
  100. package/src/tools/search-tool-bm25.ts +2 -4
  101. package/src/tools/ssh.ts +4 -4
  102. package/src/tools/todo-write.ts +172 -147
  103. package/src/tools/vim.ts +14 -15
  104. package/src/tools/write.ts +4 -4
  105. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  106. package/src/utils/edit-mode.ts +2 -1
  107. package/src/utils/file-display-mode.ts +10 -5
  108. package/src/utils/git.ts +9 -5
  109. package/src/utils/shell-snapshot.ts +2 -3
  110. package/src/vim/render.ts +4 -4
  111. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  112. package/src/prompts/tools/gh-issue-view.md +0 -11
  113. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  114. package/src/prompts/tools/gh-pr-diff.md +0 -12
  115. package/src/prompts/tools/gh-pr-push.md +0 -12
  116. package/src/prompts/tools/gh-pr-view.md +0 -11
  117. package/src/prompts/tools/gh-repo-view.md +0 -11
  118. package/src/prompts/tools/gh-run-watch.md +0 -12
  119. package/src/prompts/tools/gh-search-issues.md +0 -11
  120. package/src/prompts/tools/gh-search-prs.md +0 -11
@@ -29,17 +29,7 @@ import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
29
29
  import { DebugTool } from "./debug";
30
30
  import { ExitPlanModeTool } from "./exit-plan-mode";
31
31
  import { FindTool } from "./find";
32
- import {
33
- GhIssueViewTool,
34
- GhPrCheckoutTool,
35
- GhPrDiffTool,
36
- GhPrPushTool,
37
- GhPrViewTool,
38
- GhRepoViewTool,
39
- GhRunWatchTool,
40
- GhSearchIssuesTool,
41
- GhSearchPrsTool,
42
- } from "./gh";
32
+ import { GithubTool } from "./gh";
43
33
  import { GrepTool } from "./grep";
44
34
  import { InspectImageTool } from "./inspect-image";
45
35
  import { NotebookTool } from "./notebook";
@@ -53,9 +43,9 @@ import { ResolveTool } from "./resolve";
53
43
  import { reportFindingTool } from "./review";
54
44
  import { SearchToolBm25Tool } from "./search-tool-bm25";
55
45
  import { loadSshTool } from "./ssh";
56
- import { SubmitResultTool } from "./submit-result";
57
46
  import { type TodoPhase, TodoWriteTool } from "./todo-write";
58
47
  import { WriteTool } from "./write";
48
+ import { YieldTool } from "./yield";
59
49
 
60
50
  // Exa MCP tools (22 tools)
61
51
 
@@ -77,9 +67,9 @@ export * from "./checkpoint";
77
67
  export * from "./debug";
78
68
  export * from "./exit-plan-mode";
79
69
  export * from "./find";
80
- export * from "./gemini-image";
81
70
  export * from "./gh";
82
71
  export * from "./grep";
72
+ export * from "./image-gen";
83
73
  export * from "./inspect-image";
84
74
  export * from "./notebook";
85
75
  export * from "./poll-tool";
@@ -91,10 +81,10 @@ export * from "./resolve";
91
81
  export * from "./review";
92
82
  export * from "./search-tool-bm25";
93
83
  export * from "./ssh";
94
- export * from "./submit-result";
95
84
  export * from "./todo-write";
96
85
  export * from "./vim";
97
86
  export * from "./write";
87
+ export * from "./yield";
98
88
 
99
89
  /** Tool type (AgentTool from pi-ai) */
100
90
  export type Tool = AgentTool<any, any, any>;
@@ -131,8 +121,8 @@ export interface ToolSession {
131
121
  eventBus?: EventBus;
132
122
  /** Output schema for structured completion (subagents) */
133
123
  outputSchema?: unknown;
134
- /** Whether to include the submit_result tool by default */
135
- requireSubmitResultTool?: boolean;
124
+ /** Whether to include the yield tool by default */
125
+ requireYieldTool?: boolean;
136
126
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
137
127
  taskDepth?: number;
138
128
  /** Get session file */
@@ -217,15 +207,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
217
207
  calc: s => new CalculatorTool(s),
218
208
  ssh: loadSshTool,
219
209
  edit: s => new EditTool(s),
220
- gh_repo_view: GhRepoViewTool.createIf,
221
- gh_issue_view: GhIssueViewTool.createIf,
222
- gh_pr_view: GhPrViewTool.createIf,
223
- gh_pr_diff: GhPrDiffTool.createIf,
224
- gh_pr_checkout: GhPrCheckoutTool.createIf,
225
- gh_pr_push: GhPrPushTool.createIf,
226
- gh_run_watch: GhRunWatchTool.createIf,
227
- gh_search_issues: GhSearchIssuesTool.createIf,
228
- gh_search_prs: GhSearchPrsTool.createIf,
210
+ github: GithubTool.createIf,
229
211
  find: s => new FindTool(s),
230
212
  grep: s => new GrepTool(s),
231
213
  lsp: LspTool.createIf,
@@ -245,7 +227,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
245
227
  };
246
228
 
247
229
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
248
- submit_result: s => new SubmitResultTool(s),
230
+ yield: s => new YieldTool(s),
249
231
  report_finding: () => reportFindingTool,
250
232
  report_tool_issue: s => createReportToolIssueTool(s),
251
233
  exit_plan_mode: s => new ExitPlanModeTool(s),
@@ -288,7 +270,7 @@ function getPythonModeFromEnv(): PythonToolMode | null {
288
270
  * Create tools from BUILTIN_TOOLS registry.
289
271
  */
290
272
  export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
291
- const includeSubmitResult = session.requireSubmitResultTool === true;
273
+ const includeYield = session.requireYieldTool === true;
292
274
  const enableLsp = session.enableLsp ?? true;
293
275
  const requestedTools =
294
276
  toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
@@ -390,10 +372,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
390
372
  if (name === "bash") return allowBash;
391
373
  if (name === "python") return allowPython;
392
374
  if (name === "debug") return session.settings.get("debug.enabled");
393
- if (name === "todo_write") return !includeSubmitResult && session.settings.get("todo.enabled");
375
+ if (name === "todo_write") return !includeYield && session.settings.get("todo.enabled");
394
376
  if (name === "find") return session.settings.get("find.enabled");
395
377
  if (name === "grep") return session.settings.get("grep.enabled");
396
- if (name.startsWith("gh_")) return session.settings.get("github.enabled");
378
+ if (name === "github") return session.settings.get("github.enabled");
397
379
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
398
380
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
399
381
  if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
@@ -411,8 +393,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
411
393
  }
412
394
  return true;
413
395
  };
414
- if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {
415
- requestedTools.push("submit_result");
396
+ if (includeYield && requestedTools && !requestedTools.includes("yield")) {
397
+ requestedTools.push("yield");
416
398
  }
417
399
 
418
400
  const filteredRequestedTools = requestedTools?.filter(name => name in allTools && isToolAllowed(name));
@@ -421,7 +403,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
421
403
  ? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
422
404
  : [
423
405
  ...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
424
- ...(includeSubmitResult ? ([["submit_result", HIDDEN_TOOLS.submit_result]] as const) : []),
406
+ ...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
425
407
  ...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
426
408
  ];
427
409
 
@@ -16,8 +16,8 @@ import { ToolError } from "./tool-errors";
16
16
 
17
17
  const inspectImageSchema = Type.Object(
18
18
  {
19
- path: Type.String({ description: "Filesystem path to an image" }),
20
- question: Type.String({ description: "Question to ask about the image" }),
19
+ path: Type.String({ description: "image path", examples: ["image.png"] }),
20
+ question: Type.String({ description: "question about image", examples: ["What is in this image?"] }),
21
21
  },
22
22
  { additionalProperties: false },
23
23
  );
@@ -43,7 +43,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
43
43
  readonly label = "InspectImage";
44
44
  readonly description: string;
45
45
  readonly parameters = inspectImageSchema;
46
- readonly strict = true;
46
+ readonly strict = false;
47
47
 
48
48
  constructor(
49
49
  private readonly session: ToolSession,
@@ -13,17 +13,17 @@ export const JSON_TREE_MAX_LINES_EXPANDED = 200;
13
13
  export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
14
14
  export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
15
15
 
16
- /** Keys injected by the harness that should not be displayed to users */
17
- const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]);
18
-
19
- /** Strip harness-internal keys from tool args for display */
20
- export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
21
- const result: Record<string, unknown> = {};
22
- for (const [key, value] of Object.entries(args)) {
23
- if (!HIDDEN_ARG_KEYS.has(key)) result[key] = value;
24
- }
25
- return result;
16
+ const HIDDEN_ARG_KEYS = { [INTENT_FIELD]: 1, __partialJson: 1 };
17
+
18
+ const ARGS_INLINE_PAIR_SEP = ", ";
19
+ const ARGS_INLINE_PAIR_SEP_WIDTH = Bun.stringWidth(ARGS_INLINE_PAIR_SEP);
20
+ const ARGS_INLINE_MORE = "…";
21
+ const ARGS_INLINE_MORE_WIDTH = Bun.stringWidth(ARGS_INLINE_MORE);
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return !!value && typeof value === "object" && !Array.isArray(value);
26
25
  }
26
+
27
27
  /**
28
28
  * Format a scalar value for inline display.
29
29
  */
@@ -49,40 +49,35 @@ export function formatScalar(value: unknown, maxLen: number): string {
49
49
  * Format args inline for collapsed view.
50
50
  */
51
51
  export function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
52
- const entries = Object.entries(args).filter(([k]) => !HIDDEN_ARG_KEYS.has(k));
53
- if (entries.length === 0) return "";
54
-
55
- // Single arg: show key=value
56
- if (entries.length === 1) {
57
- const [key, value] = entries[0];
58
- return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`;
59
- }
60
-
61
- // Multiple args: show key=value, key=value...
62
- const pairs: string[] = [];
63
- let totalLen = 0;
64
-
65
- for (const [key, value] of entries) {
66
- const valueStr = formatScalar(value, 24);
67
- const pairStr = `${key}=${valueStr}`;
68
- const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length;
69
-
70
- if (totalLen + addLen > maxWidth && pairs.length > 0) {
71
- pairs.push("…");
72
- break;
52
+ let result = "";
53
+ let width = 0;
54
+ for (const key in args) {
55
+ if (key in HIDDEN_ARG_KEYS) continue;
56
+ const value = args[key];
57
+ const sep = width > 0 ? ARGS_INLINE_PAIR_SEP : "";
58
+ const sepW = width > 0 ? ARGS_INLINE_PAIR_SEP_WIDTH : 0;
59
+ const current = width + sepW;
60
+ const cap = maxWidth - current - ARGS_INLINE_MORE_WIDTH;
61
+ if (cap <= 0) {
62
+ return `${result}${ARGS_INLINE_MORE}`;
73
63
  }
74
-
75
- pairs.push(pairStr);
76
- totalLen += addLen;
64
+ const valueMaxLen = Math.min(maxWidth - current, 24);
65
+ const valueStr = formatScalar(value, valueMaxLen);
66
+ const piece = `${key}=${valueStr}`;
67
+ const pieceW = Bun.stringWidth(piece);
68
+ if (pieceW > cap) {
69
+ return `${result}${sep}${truncateToWidth(piece, cap)}`;
70
+ }
71
+ result += sep + piece;
72
+ width = current + pieceW;
77
73
  }
78
-
79
- return pairs.join(", ");
74
+ return result;
80
75
  }
81
76
 
82
77
  /**
83
78
  * Build tree prefix for nested rendering.
84
79
  */
85
- function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
80
+ function buildTreePrefix(theme: Theme, ancestors: readonly boolean[]): string {
86
81
  return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
87
82
  }
88
83
 
@@ -119,109 +114,114 @@ export function renderJsonTreeLines(
119
114
  }
120
115
 
121
116
  const connector = isLast ? theme.tree.last : theme.tree.branch;
122
- const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
123
-
124
- // Handle scalars
125
- if (val === null || val === undefined || typeof val !== "object") {
126
- const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
117
+ const prefix = `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", connector)} `;
118
+
119
+ ancestors.push(!isLast);
120
+ try {
121
+ // Handle scalars
122
+ if (val === null || val === undefined || typeof val !== "object") {
123
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
124
+
125
+ // Special handling for multiline strings
126
+ if (typeof val === "string" && val.includes("\n")) {
127
+ const strLines = val.split("\n");
128
+ const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
129
+ const continuePrefix = buildTreePrefix(theme, ancestors);
130
+
131
+ // First line with label
132
+ const firstLine = truncateToWidth(strLines[0], maxScalarLen);
133
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
134
+
135
+ // Subsequent lines indented
136
+ for (let i = 1; i < maxStrLines; i++) {
137
+ if (lines.length >= maxLines) {
138
+ truncated = true;
139
+ break;
140
+ }
141
+ const line = truncateToWidth(strLines[i], maxScalarLen);
142
+ pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
143
+ }
127
144
 
128
- // Special handling for multiline strings
129
- if (typeof val === "string" && val.includes("\n")) {
130
- const strLines = val.split("\n");
131
- const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
132
- const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme);
145
+ // Show truncation and closing quote
146
+ if (strLines.length > maxStrLines) {
147
+ truncated = true;
148
+ pushLine(
149
+ `${continuePrefix} ${theme.fg("dim", ` (${strLines.length - maxStrLines} more lines)"`)}`,
150
+ );
151
+ } else {
152
+ // Add closing quote to last line - need to modify the last pushed line
153
+ const lastIdx = lines.length - 1;
154
+ lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
155
+ }
156
+ return;
157
+ }
133
158
 
134
- // First line with label
135
- const firstLine = truncateToWidth(strLines[0], maxScalarLen);
136
- pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
159
+ const scalar = formatScalar(val, maxScalarLen);
160
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
161
+ return;
162
+ }
137
163
 
138
- // Subsequent lines indented
139
- for (let i = 1; i < maxStrLines; i++) {
164
+ // Handle arrays
165
+ if (Array.isArray(val)) {
166
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
167
+ pushLine(`${prefix}${iconArray} ${header}`);
168
+ if (val.length === 0) {
169
+ pushLine(
170
+ `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
171
+ );
172
+ return;
173
+ }
174
+ if (depth >= maxDepth) {
175
+ pushLine(
176
+ `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
177
+ );
178
+ return;
179
+ }
180
+ for (let i = 0; i < val.length; i++) {
181
+ renderNode(val[i], `[${i}]`, ancestors, i === val.length - 1, depth + 1);
140
182
  if (lines.length >= maxLines) {
141
183
  truncated = true;
142
- break;
184
+ return;
143
185
  }
144
- const line = truncateToWidth(strLines[i], maxScalarLen);
145
- pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
146
- }
147
-
148
- // Show truncation and closing quote
149
- if (strLines.length > maxStrLines) {
150
- truncated = true;
151
- pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`);
152
- } else {
153
- // Add closing quote to last line - need to modify the last pushed line
154
- const lastIdx = lines.length - 1;
155
- lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
156
186
  }
157
187
  return;
158
188
  }
159
189
 
160
- const scalar = formatScalar(val, maxScalarLen);
161
- pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
162
- return;
163
- }
190
+ // Handle objects
191
+ if (!isRecord(val)) return;
164
192
 
165
- // Handle arrays
166
- if (Array.isArray(val)) {
167
- const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
168
- pushLine(`${prefix}${iconArray} ${header}`);
169
- if (val.length === 0) {
170
- pushLine(
171
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
172
- );
193
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
194
+ pushLine(`${prefix}${iconObject} ${header}`);
195
+ if (depth >= maxDepth) {
196
+ pushLine(`${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`);
173
197
  return;
174
198
  }
175
- if (depth >= maxDepth) {
199
+ const keys = Object.keys(val);
200
+ if (keys.length === 0) {
176
201
  pushLine(
177
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "")}`,
202
+ `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
178
203
  );
179
204
  return;
180
205
  }
181
- const nextAncestors = [...ancestors, !isLast];
182
- for (let i = 0; i < val.length; i++) {
183
- renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
206
+ for (let i = 0; i < keys.length; i++) {
207
+ const childKey = keys[i];
208
+ const child = val[childKey];
209
+ renderNode(child, childKey, ancestors, i === keys.length - 1, depth + 1);
184
210
  if (lines.length >= maxLines) {
185
211
  truncated = true;
186
212
  return;
187
213
  }
188
214
  }
189
- return;
190
- }
191
-
192
- // Handle objects
193
- const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
194
- pushLine(`${prefix}${iconObject} ${header}`);
195
- const entries = Object.entries(val as Record<string, unknown>);
196
- if (entries.length === 0) {
197
- pushLine(
198
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
199
- );
200
- return;
201
- }
202
- if (depth >= maxDepth) {
203
- pushLine(
204
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
205
- );
206
- return;
207
- }
208
- const nextAncestors = [...ancestors, !isLast];
209
- for (let i = 0; i < entries.length; i++) {
210
- const [childKey, child] = entries[i];
211
- renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
212
- if (lines.length >= maxLines) {
213
- truncated = true;
214
- return;
215
- }
215
+ } finally {
216
+ ancestors.pop();
216
217
  }
217
218
  };
218
219
 
219
220
  // Render root level
220
- if (value && typeof value === "object" && !Array.isArray(value)) {
221
- const entries = Object.entries(value as Record<string, unknown>);
222
- for (let i = 0; i < entries.length; i++) {
223
- const [childKey, child] = entries[i];
224
- renderNode(child, childKey, [], i === entries.length - 1, 1);
221
+ if (isRecord(value)) {
222
+ for (const key in value) {
223
+ if (key in HIDDEN_ARG_KEYS) continue;
224
+ renderNode(value[key], key, [], true, 1);
225
225
  if (lines.length >= maxLines) {
226
226
  truncated = true;
227
227
  break;
@@ -2,19 +2,20 @@ import { computeLineHash } from "../edit/line-hash";
2
2
 
3
3
  /**
4
4
  * Format a single line of match output for grep/ast-grep style results.
5
- * Uses hashline refs when hashlines are enabled, otherwise pads the number.
5
+ *
6
+ * Match lines use `>` as the anchor/content separator; context lines use `:`.
7
+ * In hashline mode the anchor is `LINE+ID` (no `#`); in plain mode it is
8
+ * just the line number. Line numbers are never padded.
6
9
  */
7
10
  export function formatMatchLine(
8
11
  lineNumber: number,
9
12
  line: string,
10
13
  isMatch: boolean,
11
- options: { useHashLines: boolean; lineWidth: number },
14
+ options: { useHashLines: boolean },
12
15
  ): string {
13
- const separator = isMatch ? ":" : "-";
16
+ const separator = isMatch ? ">" : ":";
14
17
  if (options.useHashLines) {
15
- const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
16
- return `${ref}${separator}${line}`;
18
+ return `${lineNumber}${computeLineHash(lineNumber, line)}${separator}${line}`;
17
19
  }
18
- const padded = lineNumber.toString().padStart(options.lineWidth, " ");
19
- return `${padded}${separator}${line}`;
20
+ return `${lineNumber}${separator}${line}`;
20
21
  }
@@ -13,14 +13,16 @@ import { formatCount, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils"
13
13
 
14
14
  const notebookSchema = Type.Object({
15
15
  action: StringEnum(["edit", "insert", "delete"], {
16
- description: "Action to perform on the notebook cell",
16
+ description: "cell action",
17
+ examples: ["edit", "insert", "delete"],
17
18
  }),
18
- notebook_path: Type.String({ description: "Path to the .ipynb file (relative or absolute)" }),
19
- cell_index: Type.Number({ description: "0-based index of the cell to operate on" }),
20
- content: Type.Optional(Type.String({ description: "New cell content (required for edit/insert)" })),
19
+ notebook_path: Type.String({ description: "notebook path", examples: ["analysis.ipynb"] }),
20
+ cell_index: Type.Number({ description: "cell index", examples: [0, 1] }),
21
+ content: Type.Optional(Type.String({ description: "new cell content" })),
21
22
  cell_type: Type.Optional(
22
23
  StringEnum(["code", "markdown"], {
23
- description: "Cell type for insert (default: code)",
24
+ description: "cell type",
25
+ examples: ["code", "markdown"],
24
26
  }),
25
27
  ),
26
28
  });
@@ -62,8 +64,7 @@ type NotebookParams = Static<typeof notebookSchema>;
62
64
  export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
63
65
  readonly name = "notebook";
64
66
  readonly label = "Notebook";
65
- readonly description =
66
- "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based. Paths must be absolute.";
67
+ readonly description = "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based.";
67
68
  readonly parameters = notebookSchema;
68
69
  readonly strict = true;
69
70
  readonly concurrency = "exclusive";
@@ -8,7 +8,8 @@ import type { ToolSession } from "./index";
8
8
  const pollSchema = Type.Object({
9
9
  jobs: Type.Optional(
10
10
  Type.Array(Type.String(), {
11
- description: "Specific job IDs to wait for. If omitted, waits for any running job.",
11
+ description: "job ids to wait for",
12
+ examples: [["job-1234"]],
12
13
  }),
13
14
  ),
14
15
  });
@@ -1,4 +1,3 @@
1
- import type * as fs from "node:fs";
2
1
  import * as path from "node:path";
3
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
3
  import type { ImageContent } from "@oh-my-pi/pi-ai";
@@ -16,7 +15,6 @@ import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from ".
16
15
  import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
17
16
  import type { ToolSession } from ".";
18
17
  import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
19
- import { resolveToCwd } from "./path-utils";
20
18
  import { formatTitle, replaceTabs, shortenPath, truncateToWidth, wrapBrackets } from "./render-utils";
21
19
  import { ToolAbortError, ToolError } from "./tool-errors";
22
20
  import { toolResult } from "./tool-result";
@@ -47,14 +45,13 @@ function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
47
45
  export const pythonSchema = Type.Object({
48
46
  cells: Type.Array(
49
47
  Type.Object({
50
- code: Type.String({ description: "Python code to execute" }),
51
- title: Type.Optional(Type.String({ description: "Cell label, e.g. 'imports', 'helper'" })),
48
+ code: Type.String({ description: "python code", examples: ["print('hello')", "import json"] }),
49
+ title: Type.String({ description: "cell label", examples: ["imports", "helper"] }),
52
50
  }),
53
- { description: "Cells to execute sequentially in persistent kernel" },
51
+ { description: "cells to execute" },
54
52
  ),
55
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 30 })),
56
- cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
57
- reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })),
53
+ timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 30 })),
54
+ reset: Type.Optional(Type.Boolean({ description: "restart kernel" })),
58
55
  });
59
56
  export type PythonToolParams = Static<typeof pythonSchema>;
60
57
 
@@ -177,7 +174,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
177
174
  }
178
175
  const session = this.session;
179
176
 
180
- const { cells, timeout: rawTimeout = 30, cwd, reset } = params;
177
+ const { cells, timeout: rawTimeout = 30, reset } = params;
181
178
  // Clamp to reasonable range: 1s - 600s (10 min)
182
179
  const timeoutSec = clampTimeout("python", rawTimeout);
183
180
  const timeoutMs = timeoutSec * 1000;
@@ -204,17 +201,6 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
204
201
  }
205
202
  session.assertPythonExecutionAllowed?.();
206
203
 
207
- const commandCwd = cwd ? resolveToCwd(cwd, session.cwd) : session.cwd;
208
- let cwdStat: fs.Stats;
209
- try {
210
- cwdStat = await Bun.file(commandCwd).stat();
211
- } catch {
212
- throw new ToolError(`Working directory does not exist: ${commandCwd}`);
213
- }
214
- if (!cwdStat.isDirectory()) {
215
- throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
216
- }
217
-
218
204
  const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES * 2);
219
205
  const jsonOutputs: unknown[] = [];
220
206
  const images: ImageContent[] = [];
@@ -273,11 +259,11 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
273
259
  pushUpdate();
274
260
  },
275
261
  });
276
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
262
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
277
263
 
278
264
  if (getPreludeDocs().length === 0) {
279
265
  const warmup = await warmPythonEnvironment(
280
- commandCwd,
266
+ session.cwd,
281
267
  sessionId,
282
268
  session.settings.get("python.sharedGateway"),
283
269
  sessionFile ?? undefined,
@@ -292,7 +278,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
292
278
  }
293
279
 
294
280
  const baseExecutorOptions = {
295
- cwd: commandCwd,
281
+ cwd: session.cwd,
296
282
  deadlineMs,
297
283
  signal: combinedSignal,
298
284
  sessionId,