@oh-my-pi/pi-coding-agent 11.3.0 → 11.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.4.1] - 2026-02-06
6
+ ### Fixed
7
+
8
+ - Fixed tab character display in error messages and bash tool output by properly replacing tabs with spaces
9
+
10
+ ## [11.4.0] - 2026-02-06
11
+
12
+ ### Added
13
+
14
+ - Visualize leading whitespace (indentation) in diff output with dim glyphs—tabs display as `→` and spaces as `·` for improved readability
15
+
16
+ ### Fixed
17
+
18
+ - Fixed patch applicator to correctly handle context-only hunks (pure context lines between @@ markers) without altering indentation in tab-indented files
19
+ - Fixed indentation conversion logic to infer tab width from space-to-tab patterns using linear regression (ax+b model) when pattern uses spaces and actual file uses tabs
20
+ - Fixed tab character rendering in tool output previews and code cell displays, ensuring tabs are properly converted to spaces for consistent terminal display
21
+ - Fixed `newSession()` to properly await session manager operations, ensuring new session is fully initialized before returning
22
+ - Fixed session formatting to use XML structure for tools and tool invocations instead of YAML, improving compatibility with structured output parsing
23
+
5
24
  ## [11.3.0] - 2026-02-06
6
25
 
7
26
  ### Added
@@ -109,6 +128,7 @@
109
128
  - Fixed CLI invocation with flags only (e.g. `pi --model=codex`) to route to the default command instead of erroring
110
129
 
111
130
  ## [11.2.0] - 2026-02-05
131
+
112
132
  ### Added
113
133
 
114
134
  - Added `omp commit` command to generate commit messages and update changelogs with `--push`, `--dry-run`, `--no-changelog`, and model override flags
@@ -1080,7 +1100,7 @@
1080
1100
  - Updated all tools to use structured metadata instead of inline notices for truncation, limits, and diagnostics
1081
1101
  - Replaced manual error formatting with ToolError.render() and standardized error handling
1082
1102
  - Enhanced bash and python executors to save full output as artifacts when truncated
1083
- - Improved abort signal handling across all tools with consistent ToolAbortError
1103
+ - Improved abort signal handling across <caution>ith consistent ToolAbortError
1084
1104
  - Renamed task parameter from `vars` to `args` throughout task tool interface and updated template rendering to support built-in `{{id}}` and `{{description}}` placeholders
1085
1105
  - Simplified todo-write tool by removing active_form parameter, using single content field for task descriptions
1086
1106
  - Updated system prompt structure with `<important>` and `<avoid>` tags, clearer critical sections, and standardized whitespace handling
@@ -1808,8 +1828,8 @@
1808
1828
 
1809
1829
  - Fixed editor border rendering glitch after canceling slash command autocomplete
1810
1830
  - Fixed login/logout credential path message to reference agent.db
1811
- - Removed legacy auth.json file—credentials are stored exclusively in agent.db
1812
- - Removed legacy auth.json file—credentials are stored exclusively in agent.db
1831
+ - Removed legacy auth.json file—credentials are stored exclusively in agent.db
1832
+ - Removed legacy auth.json file—credentials are stored exclusively in agent.db
1813
1833
 
1814
1834
  ## [4.2.0] - 2026-01-10
1815
1835
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.3.0",
3
+ "version": "11.4.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -90,12 +90,12 @@
90
90
  "@mozilla/readability": "0.6.0",
91
91
  "@oclif/core": "^4.8.0",
92
92
  "@oclif/plugin-autocomplete": "^3.2.40",
93
- "@oh-my-pi/omp-stats": "11.3.0",
94
- "@oh-my-pi/pi-agent-core": "11.3.0",
95
- "@oh-my-pi/pi-ai": "11.3.0",
96
- "@oh-my-pi/pi-natives": "11.3.0",
97
- "@oh-my-pi/pi-tui": "11.3.0",
98
- "@oh-my-pi/pi-utils": "11.3.0",
93
+ "@oh-my-pi/omp-stats": "11.4.1",
94
+ "@oh-my-pi/pi-agent-core": "11.4.1",
95
+ "@oh-my-pi/pi-ai": "11.4.1",
96
+ "@oh-my-pi/pi-natives": "11.4.1",
97
+ "@oh-my-pi/pi-tui": "11.4.1",
98
+ "@oh-my-pi/pi-utils": "11.4.1",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -2,6 +2,38 @@ import * as Diff from "diff";
2
2
  import { theme } from "../../modes/theme/theme";
3
3
  import { replaceTabs } from "../../tools/render-utils";
4
4
 
5
+ /** SGR dim on / normal intensity — additive, preserves fg/bg colors. */
6
+ const DIM = "\x1b[2m";
7
+ const DIM_OFF = "\x1b[22m";
8
+
9
+ /**
10
+ * Visualize leading whitespace (indentation) with dim glyphs.
11
+ * Tabs become ` → ` and spaces become `·`. Only affects whitespace
12
+ * before the first non-whitespace character; remaining tabs in code
13
+ * content are replaced with spaces (like replaceTabs).
14
+ */
15
+ function visualizeIndent(text: string): string {
16
+ const match = text.match(/^([ \t]+)/);
17
+ if (!match) return replaceTabs(text);
18
+ const indent = match[1];
19
+ const rest = text.slice(indent.length);
20
+ // Normalize: collapse 3-space groups (tab-width) into tab arrows,
21
+ // then handle remaining tabs and lone spaces.
22
+ const normalized = indent.replaceAll("\t", " ");
23
+ let visible = "";
24
+ let pos = 0;
25
+ while (pos < normalized.length) {
26
+ if (pos + 3 <= normalized.length && normalized.slice(pos, pos + 3) === " ") {
27
+ visible += `${DIM} → ${DIM_OFF}`;
28
+ pos += 3;
29
+ } else {
30
+ visible += `${DIM}·${DIM_OFF}`;
31
+ pos++;
32
+ }
33
+ }
34
+ return `${visible}${replaceTabs(rest)}`;
35
+ }
36
+
5
37
  /**
6
38
  * Parse diff line to extract prefix, line number, and content.
7
39
  * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
@@ -122,24 +154,26 @@ export function renderDiff(diffText: string, _options: RenderDiffOptions = {}):
122
154
  replaceTabs(added.content),
123
155
  );
124
156
 
125
- result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, removedLine)));
126
- result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, addedLine)));
157
+ result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removedLine))));
158
+ result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine))));
127
159
  } else {
128
160
  // Show all removed lines first, then all added lines
129
161
  for (const removed of removedLines) {
130
- result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, replaceTabs(removed.content))));
162
+ result.push(
163
+ theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removed.content))),
164
+ );
131
165
  }
132
166
  for (const added of addedLines) {
133
- result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, replaceTabs(added.content))));
167
+ result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(added.content))));
134
168
  }
135
169
  }
136
170
  } else if (parsed.prefix === "+") {
137
171
  // Standalone added line
138
- result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, replaceTabs(parsed.content))));
172
+ result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, visualizeIndent(parsed.content))));
139
173
  i++;
140
174
  } else {
141
175
  // Context line
142
- result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, replaceTabs(parsed.content))));
176
+ result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, visualizeIndent(parsed.content))));
143
177
  i++;
144
178
  }
145
179
  }
@@ -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[]>();
@@ -1118,8 +1204,25 @@ function computeReplacements(
1118
1204
 
1119
1205
  // Adjust indentation if needed (handles fuzzy matches where indentation differs)
1120
1206
  const actualMatchedLines = originalLines.slice(found, found + pattern.length);
1121
- const adjustedNewLines = adjustLinesIndentation(pattern, actualMatchedLines, newSlice);
1122
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);
1123
1226
  replacements.push({ startIndex: found, oldLen: pattern.length, newLines: adjustedNewLines });
1124
1227
  lineIndex = found + pattern.length;
1125
1228
  }
@@ -14,6 +14,7 @@ import {
14
14
  formatStatusIcon,
15
15
  getDiffStats,
16
16
  PREVIEW_LIMITS,
17
+ replaceTabs,
17
18
  shortenPath,
18
19
  ToolUIKit,
19
20
  truncateDiffByHunk,
@@ -181,7 +182,7 @@ export const editToolRenderer = {
181
182
  const maxLines = 6;
182
183
  text += "\n\n";
183
184
  for (const line of previewLines.slice(0, maxLines)) {
184
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
185
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
185
186
  }
186
187
  if (previewLines.length > maxLines) {
187
188
  text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
@@ -191,7 +192,7 @@ export const editToolRenderer = {
191
192
  const maxLines = 6;
192
193
  text += "\n\n";
193
194
  for (const line of previewLines.slice(0, maxLines)) {
194
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
195
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
195
196
  }
196
197
  if (previewLines.length > maxLines) {
197
198
  text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
@@ -264,13 +265,13 @@ export const editToolRenderer = {
264
265
 
265
266
  if (result.isError) {
266
267
  if (errorText) {
267
- text += `\n\n${uiTheme.fg("error", errorText)}`;
268
+ text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
268
269
  }
269
270
  } else if (result.details?.diff) {
270
271
  text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
271
272
  } else if (editDiffPreview) {
272
273
  if ("error" in editDiffPreview) {
273
- text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
274
+ text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
274
275
  } else if (editDiffPreview.diff) {
275
276
  text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
276
277
  }
@@ -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.
@@ -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`
@@ -123,7 +123,7 @@ If tempted to write above, expand using templates.
123
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.
124
124
  **Test/lint commands in parallel tasks** — edit wars:
125
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.
126
- **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.
127
127
 
128
128
  ### Delegate intent, not keystrokes
129
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>
@@ -29,7 +29,6 @@ import type {
29
29
  } from "@oh-my-pi/pi-ai";
30
30
  import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
31
31
  import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
32
- import { YAML } from "bun";
33
32
  import type { Rule } from "../capability/rule";
34
33
  import { getAgentDbPath } from "../config";
35
34
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
@@ -1740,7 +1739,7 @@ export class AgentSession {
1740
1739
  await this.abort();
1741
1740
  this.agent.reset();
1742
1741
  await this.sessionManager.flush();
1743
- this.sessionManager.newSession(options);
1742
+ await this.sessionManager.newSession(options);
1744
1743
  this.agent.sessionId = this.sessionManager.getSessionId();
1745
1744
  this._steeringMessages = [];
1746
1745
  this._followUpMessages = [];
@@ -2443,7 +2442,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
2443
2442
 
2444
2443
  // Start a new session
2445
2444
  await this.sessionManager.flush();
2446
- this.sessionManager.newSession();
2445
+ await this.sessionManager.newSession();
2447
2446
  this.agent.reset();
2448
2447
  this.agent.sessionId = this.sessionManager.getSessionId();
2449
2448
  this._steeringMessages = [];
@@ -3453,7 +3452,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3453
3452
  await this.sessionManager.flush();
3454
3453
 
3455
3454
  if (!selectedEntry.parentId) {
3456
- this.sessionManager.newSession({ parentSession: previousSessionFile });
3455
+ await this.sessionManager.newSession({ parentSession: previousSessionFile });
3457
3456
  } else {
3458
3457
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
3459
3458
  }
@@ -3875,6 +3874,16 @@ Be thorough - include exact file paths, function names, error messages, and tech
3875
3874
  formatSessionAsText(): string {
3876
3875
  const lines: string[] = [];
3877
3876
 
3877
+ /** Serialize an object as XML parameter elements, one per key. */
3878
+ function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
3879
+ const parts: string[] = [];
3880
+ for (const [key, value] of Object.entries(args)) {
3881
+ const text = typeof value === "string" ? value : JSON.stringify(value);
3882
+ parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
3883
+ }
3884
+ return parts.join("\n");
3885
+ }
3886
+
3878
3887
  // Include system prompt at the beginning
3879
3888
  const systemPrompt = this.agent.state.systemPrompt;
3880
3889
  if (systemPrompt) {
@@ -3914,12 +3923,11 @@ Be thorough - include exact file paths, function names, error messages, and tech
3914
3923
  if (tools.length > 0) {
3915
3924
  lines.push("## Available Tools\n");
3916
3925
  for (const tool of tools) {
3917
- lines.push(`### ${tool.name}\n`);
3926
+ lines.push(`<tool name="${tool.name}">`);
3918
3927
  lines.push(tool.description);
3919
- lines.push("\n```yaml");
3920
3928
  const parametersClean = stripTypeBoxFields(tool.parameters);
3921
- lines.push(YAML.stringify(parametersClean, null, 2));
3922
- lines.push("```\n");
3929
+ lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
3930
+ lines.push("<" + "/tool>\n");
3923
3931
  }
3924
3932
  lines.push("\n");
3925
3933
  }
@@ -3951,10 +3959,11 @@ Be thorough - include exact file paths, function names, error messages, and tech
3951
3959
  lines.push(c.thinking);
3952
3960
  lines.push("</thinking>\n");
3953
3961
  } else if (c.type === "toolCall") {
3954
- lines.push(`### Tool: ${c.name}`);
3955
- lines.push("```yaml");
3956
- lines.push(YAML.stringify(c.arguments, null, 2));
3957
- lines.push("```\n");
3962
+ lines.push(`<invoke name="${c.name}">`);
3963
+ if (c.arguments && typeof c.arguments === "object") {
3964
+ lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
3965
+ }
3966
+ lines.push("<" + "/invoke>\n");
3958
3967
  }
3959
3968
  }
3960
3969
  lines.push("");
package/src/tools/bash.ts CHANGED
@@ -17,7 +17,7 @@ import { applyHeadTail, normalizeBashCommand } from "./bash-normalize";
17
17
  import type { OutputMeta } from "./output-meta";
18
18
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
19
19
  import { resolveToCwd } from "./path-utils";
20
- import { formatBytes, wrapBrackets } from "./render-utils";
20
+ import { formatBytes, replaceTabs, wrapBrackets } from "./render-utils";
21
21
  import { ToolError } from "./tool-errors";
22
22
  import { toolResult } from "./tool-result";
23
23
  import { DEFAULT_MAX_BYTES } from "./truncate";
@@ -271,11 +271,13 @@ export const bashToolRenderer = {
271
271
  const hasOutput = displayOutput.trim().length > 0;
272
272
  if (hasOutput) {
273
273
  if (expanded) {
274
- outputLines.push(...displayOutput.split("\n").map(line => uiTheme.fg("toolOutput", line)));
274
+ outputLines.push(
275
+ ...displayOutput.split("\n").map(line => uiTheme.fg("toolOutput", replaceTabs(line))),
276
+ );
275
277
  } else {
276
278
  const styledOutput = displayOutput
277
279
  .split("\n")
278
- .map(line => uiTheme.fg("toolOutput", line))
280
+ .map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
279
281
  .join("\n");
280
282
  const textContent = styledOutput;
281
283
  const result = truncateToVisualLines(textContent, previewLines, width);
@@ -23,6 +23,7 @@ import {
23
23
  formatExpandHint,
24
24
  formatMoreItems,
25
25
  formatStatusIcon,
26
+ replaceTabs,
26
27
  shortenPath,
27
28
  ToolUIKit,
28
29
  } from "./render-utils";
@@ -160,7 +161,7 @@ function formatStreamingContent(content: string, uiTheme: Theme, ui: ToolUIKit):
160
161
  text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
161
162
  }
162
163
  for (const line of displayLines) {
163
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
164
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
164
165
  }
165
166
  text += uiTheme.fg("dim", `… (streaming)`);
166
167
  return text;
@@ -175,7 +176,7 @@ function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme
175
176
 
176
177
  let text = "\n\n";
177
178
  for (const line of displayLines) {
178
- text += `${uiTheme.fg("toolOutput", ui.truncate(line, 80))}\n`;
179
+ text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
179
180
  }
180
181
  if (!expanded && hidden > 0) {
181
182
  const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
@@ -90,7 +90,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
90
90
  const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, outputMaxLines);
91
91
  const displayLines = rawLines
92
92
  .slice(0, maxLines)
93
- .map(line => (line.includes("\x1b[") ? line : theme.fg("toolOutput", line)));
93
+ .map(line => (line.includes("\x1b[") ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))));
94
94
  outputLines.push(...displayLines);
95
95
  const remaining = rawLines.length - maxLines;
96
96
  if (remaining > 0) {