@oh-my-pi/pi-coding-agent 11.6.1 → 11.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.7.0] - 2026-02-07
6
+ ### Changed
7
+
8
+ - Enhanced error messages for failed Python cells to include full combined output context instead of just the error message
9
+ - Updated error cell output styling to use error color theme instead of standard tool output color for better visual distinction
10
+
11
+ ### Fixed
12
+
13
+ - Improved error handling in Python cell execution to preserve and display combined output from previous cells when an error occurs
14
+ - Fixed tab character rendering in Python tool output display to properly format whitespace in cell output and status events
15
+
5
16
  ## [11.6.1] - 2026-02-07
6
17
  ### Fixed
7
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.6.1",
3
+ "version": "11.7.0",
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.6.1",
94
- "@oh-my-pi/pi-agent-core": "11.6.1",
95
- "@oh-my-pi/pi-ai": "11.6.1",
96
- "@oh-my-pi/pi-natives": "11.6.1",
97
- "@oh-my-pi/pi-tui": "11.6.1",
98
- "@oh-my-pi/pi-utils": "11.6.1",
93
+ "@oh-my-pi/omp-stats": "11.7.0",
94
+ "@oh-my-pi/pi-agent-core": "11.7.0",
95
+ "@oh-my-pi/pi-ai": "11.7.0",
96
+ "@oh-my-pi/pi-natives": "11.7.0",
97
+ "@oh-my-pi/pi-tui": "11.7.0",
98
+ "@oh-my-pi/pi-utils": "11.7.0",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -454,6 +454,7 @@ export class InteractiveMode implements InteractiveModeContext {
454
454
  private async loadTodoList(): Promise<void> {
455
455
  const sessionFile = this.sessionManager.getSessionFile() ?? null;
456
456
  if (!sessionFile) {
457
+ this.todoItems = [];
457
458
  this.renderTodoList();
458
459
  return;
459
460
  }
@@ -463,9 +464,12 @@ export class InteractiveMode implements InteractiveModeContext {
463
464
  const data = (await Bun.file(todoPath).json()) as { todos?: TodoItem[] };
464
465
  if (data?.todos && Array.isArray(data.todos)) {
465
466
  this.todoItems = data.todos;
467
+ } else {
468
+ this.todoItems = [];
466
469
  }
467
470
  } catch (error) {
468
471
  if (isEnoent(error)) {
472
+ this.todoItems = [];
469
473
  this.renderTodoList();
470
474
  return;
471
475
  }
@@ -184,242 +184,149 @@ export type SymbolKey =
184
184
  type SymbolMap = Record<SymbolKey, string>;
185
185
 
186
186
  const UNICODE_SYMBOLS: SymbolMap = {
187
- // Status Indicators
188
- // pick: ✓ | alt: ✅ ☑ ✚
189
- "status.success": "",
190
- // pick: ✗ | alt: ✘ ✖ ❌ ⨯
191
- "status.error": "✗",
192
- // pick: ⚠ | alt: ‼ ⁉ ▲ △
187
+ // Status
188
+ "status.success": "",
189
+ "status.error": "",
193
190
  "status.warning": "⚠",
194
- // pick: ℹ | alt: 🛈 ⅈ
195
- "status.info": "",
196
- // pick: ◔ | alt: ● ◐ ◑ ◒ ◓ ⏳ …
197
- "status.pending": "◔",
198
- // pick: ○ | alt: ◌ ◯ ⃠
199
- "status.disabled": "○",
200
- // pick: ● | alt: ◉ ◎ ⬤
191
+ "status.info": "",
192
+ "status.pending": "",
193
+ "status.disabled": "⦸",
201
194
  "status.enabled": "●",
202
- // pick: ↻ | alt: ↺ ⟲ ◐ ▶
203
- "status.running": "",
204
- // pick: ◐ | alt: ◑ ◒ ◓ ◔
205
- "status.shadowed": "◐",
206
- // pick: ⊗ | alt: ⊘ ⛔ ⏹ ⨂
207
- "status.aborted": "⊗",
195
+ "status.running": "",
196
+ "status.shadowed": "",
197
+ "status.aborted": "⏹",
208
198
  // Navigation
209
- // pick: ❯ | alt: › ▸ ▹
210
199
  "nav.cursor": "❯",
211
- // pick: ➜ | alt: → ➔ ⇒
212
- "nav.selected": "➜",
213
- // pick: ▸ | alt: ▶ ▹ ⯈
200
+ "nav.selected": "",
214
201
  "nav.expand": "▸",
215
- // pick: ▾ | alt: ▼ ▽ ⯆
216
202
  "nav.collapse": "▾",
217
- // pick: ← | alt: ↩ ↫ ⇦
218
- "nav.back": "←",
219
- // Tree Connectors
220
- // pick: ├─ | alt: ├╴ ├╌ ├┄ ╠═
203
+ "nav.back": "⟵",
204
+ // Tree
221
205
  "tree.branch": "├─",
222
- // pick: └─ | alt: └╴ └╌ └┄ ╚═
223
206
  "tree.last": "└─",
224
- // pick: │ | alt: ┃ ║ ▏ ▕
225
207
  "tree.vertical": "│",
226
- // pick: ─ | alt: ━ ═ ╌ ┄
227
208
  "tree.horizontal": "─",
228
- // pick: └ | alt: ⎿ ╰ ↳
229
- "tree.hook": "\u2514",
230
- // Box Drawing - Rounded
231
- // pick: ╭ | alt: ┌ ┏ ╔
209
+ "tree.hook": "",
210
+ // Box (rounded)
232
211
  "boxRound.topLeft": "╭",
233
- // pick: ╮ | alt: ┐ ┓ ╗
234
212
  "boxRound.topRight": "╮",
235
- // pick: ╰ | alt: └ ┗ ╚
236
213
  "boxRound.bottomLeft": "╰",
237
- // pick: ╯ | alt: ┘ ┛ ╝
238
214
  "boxRound.bottomRight": "╯",
239
- // pick: ─ | alt: ━ ═ ╌
240
215
  "boxRound.horizontal": "─",
241
- // pick: │ | alt: ┃ ║ ▏
242
216
  "boxRound.vertical": "│",
243
- // Box Drawing - Sharp
244
- // pick: ┌ | alt: ┏ ╭ ╔
217
+ // Box (sharp)
245
218
  "boxSharp.topLeft": "┌",
246
- // pick: ┐ | alt: ┓ ╮ ╗
247
219
  "boxSharp.topRight": "┐",
248
- // pick: └ | alt: ┗ ╰ ╚
249
220
  "boxSharp.bottomLeft": "└",
250
- // pick: ┘ | alt: ┛ ╯ ╝
251
221
  "boxSharp.bottomRight": "┘",
252
- // pick: ─ | alt: ━ ═ ╌
253
222
  "boxSharp.horizontal": "─",
254
- // pick: │ | alt: ┃ ║ ▏
255
223
  "boxSharp.vertical": "│",
256
- // pick: ┼ | alt: ╋ ╬ ┿
257
224
  "boxSharp.cross": "┼",
258
- // pick: ┬ | alt: ╦ ┯ ┳
259
225
  "boxSharp.teeDown": "┬",
260
- // pick: ┴ | alt: ╩ ┷ ┻
261
226
  "boxSharp.teeUp": "┴",
262
- // pick: ├ | alt: ╠ ┝ ┣
263
227
  "boxSharp.teeRight": "├",
264
- // pick: ┤ | alt: ╣ ┥ ┫
265
228
  "boxSharp.teeLeft": "┤",
266
- // Separators
267
- // pick: │ | alt: ┃ ║ ▏
268
- "sep.powerline": "",
269
- // pick: │ | alt: ┆ ┊
270
- "sep.powerlineThin": "",
271
- // pick: > | alt: › ▸ »
272
- "sep.powerlineLeft": ">",
273
- // pick: < | alt: ‹ ◂ «
274
- "sep.powerlineRight": "<",
275
- // pick: > | alt: › ▸
229
+ // Separators (powerline-ish, but pure Unicode)
230
+ "sep.powerline": "▕",
231
+ "sep.powerlineThin": "",
232
+ "sep.powerlineLeft": "▶",
233
+ "sep.powerlineRight": "",
276
234
  "sep.powerlineThinLeft": ">",
277
- // pick: < | alt: ‹ ◂
278
235
  "sep.powerlineThinRight": "<",
279
- // pick: █ | alt: ▓ ▒ ░ ▉
280
- "sep.block": "█",
281
- // pick: space | alt: ␠ ·
236
+ "sep.block": "",
282
237
  "sep.space": " ",
283
- // pick: > | alt: › » ▸
284
238
  "sep.asciiLeft": ">",
285
- // pick: < | alt: ‹ « ◂
286
239
  "sep.asciiRight": "<",
287
- // pick: · | alt: • ⋅
288
240
  "sep.dot": " · ",
289
- // pick: / | alt: / ∕ ⁄
290
241
  "sep.slash": " / ",
291
- // pick: | | alt: ┃ ║
292
- "sep.pipe": " | ",
242
+ "sep.pipe": "",
293
243
  // Icons
294
- // pick: ◈ | alt: ◆
295
- "icon.model": "",
296
- // pick: 📋 | alt: 🗒 📝
297
- "icon.plan": "📋",
298
- // pick: 📁 | alt: 📂 🗂 🗃
244
+ "icon.model": "",
245
+ "icon.plan": "🗺",
299
246
  "icon.folder": "📁",
300
- // pick: 📄 | alt: 📃 📝
301
247
  "icon.file": "📄",
302
- // pick: ⎇ | alt: 🔀 ⑂
303
248
  "icon.git": "⎇",
304
- // pick: ⎇ | alt: 🌿
305
- "icon.branch": "",
306
- // pick: ⊛ | alt: ◎ ◍ ⊙
307
- "icon.tokens": "⊛",
308
- // pick: ◫ | alt: ◧ ▣ ▦
249
+ "icon.branch": "",
250
+ "icon.tokens": "🪙",
309
251
  "icon.context": "◫",
310
- // pick: $ | alt: 💲 💰
311
- "icon.cost": "$",
312
- // pick: ◷ | alt: ⏱ ⏲ ⌛
313
- "icon.time": "◷",
314
- // pick: π | alt: ∏ ∑
252
+ "icon.cost": "💲",
253
+ "icon.time": "",
315
254
  "icon.pi": "π",
316
- // pick: AG | alt: 👥 👤
317
- "icon.agents": "AG",
318
- // pick: cache | alt: 💾 🗄
319
- "icon.cache": "cache",
320
- // pick: in: | alt: ⤵ ↲
321
- "icon.input": "in:",
322
- // pick: out: | alt: ⤴ ↱
323
- "icon.output": "out:",
324
- // pick: host | alt: 🖥 💻
325
- "icon.host": "host",
326
- // pick: id | alt: 🧭 🧩
327
- "icon.session": "id",
328
- // pick: 📦 | alt: 🧰
255
+ "icon.agents": "👥",
256
+ "icon.cache": "💾",
257
+ "icon.input": "⤵",
258
+ "icon.output": "",
259
+ "icon.host": "🖥",
260
+ "icon.session": "🆔",
329
261
  "icon.package": "📦",
330
- // pick: ⚠ | alt: ❗
331
262
  "icon.warning": "⚠",
332
- // pick: ↩ | alt: ↺ ⟲
333
- "icon.rewind": "",
334
- // pick: ⚡ | alt: ✨
335
- "icon.auto": "",
336
- // pick: ✧ | alt: ⚙ SK 🧠
337
- "icon.extensionSkill": "",
338
- // pick: ⚒ | alt: ⛭ TL 🛠
339
- "icon.extensionTool": "",
340
- // pick: / | alt: ⌘ ⌥
341
- "icon.extensionSlashCommand": "/",
342
- // pick: ◈ | alt: ⧫ MCP 🔌
343
- "icon.extensionMcp": "◈",
344
- // pick: § | alt: ⚖ RL 📏
345
- "icon.extensionRule": "§",
346
- // pick: | alt: ⚓ HK 🪝
347
- "icon.extensionHook": "",
348
- // pick: PR | alt: 💬 ✎
349
- "icon.extensionPrompt": "PR",
350
- // pick: CF | alt: 📄 📎
351
- "icon.extensionContextFile": "CF",
352
- // pick: IN | alt: 📘 ℹ
353
- "icon.extensionInstruction": "IN",
354
- // Thinking Levels
355
- // pick: [min] | alt: · ◔ min
356
- "thinking.minimal": "[min]",
357
- // pick: [low] | alt: ◑ low ▪ low
358
- "thinking.low": "[low]",
359
- // pick: [med] | alt: ◒ med ▪ med
360
- "thinking.medium": "[med]",
361
- // pick: [high] | alt: ◕ high ▪ high
362
- "thinking.high": "[high]",
363
- // pick: [xhi] | alt: ◉ xhi ▪ xhi
364
- "thinking.xhigh": "[xhi]",
263
+ "icon.rewind": "↶",
264
+ "icon.auto": "",
265
+ "icon.extensionSkill": "",
266
+ "icon.extensionTool": "🛠",
267
+ "icon.extensionSlashCommand": "⌘",
268
+ "icon.extensionMcp": "🔌",
269
+ "icon.extensionRule": "⚖",
270
+ "icon.extensionHook": "🪝",
271
+ "icon.extensionPrompt": "✎",
272
+ "icon.extensionContextFile": "📎",
273
+ "icon.extensionInstruction": "📘",
274
+ // Thinking levels
275
+ "thinking.minimal": "◔ min",
276
+ "thinking.low": "◑ low",
277
+ "thinking.medium": "◒ med",
278
+ "thinking.high": "◕ high",
279
+ "thinking.xhigh": "◉ xhi",
365
280
  // Checkboxes
366
- // pick: ☑ | alt: ✓ ✔ ✅
367
281
  "checkbox.checked": "☑",
368
- // pick: ☐ | alt: □ ▢
369
282
  "checkbox.unchecked": "☐",
370
- // pick: • | alt: · ▪ ◦
283
+ // Formatting
371
284
  "format.bullet": "•",
372
- // pick: – | alt: ― -
373
- "format.dash": "",
374
- // pick: ⟨ | alt: [ ⟦
375
- "format.bracketLeft": "⟨",
376
- // pick: ⟩ | alt: ] ⟧
377
- "format.bracketRight": "⟩",
378
- // Markdown-specific
379
- // pick: │ | alt: ┃ ║
380
- "md.quoteBorder": "│",
381
- // pick: ─ | alt: ━ ═
285
+ "format.dash": "",
286
+ "format.bracketLeft": "",
287
+ "format.bracketRight": "⟧",
288
+ // Markdown
289
+ "md.quoteBorder": "▏",
382
290
  "md.hrChar": "─",
383
- // pick: • | alt: · ▪ ◦
384
291
  "md.bullet": "•",
385
- // Language icons (unicode uses code symbol prefix)
386
- "lang.default": "",
387
- "lang.typescript": "❖ ts",
388
- "lang.javascript": "❖ js",
389
- "lang.python": "❖ py",
390
- "lang.rust": "❖ rs",
391
- "lang.go": "❖ go",
392
- "lang.java": "❖ java",
393
- "lang.c": "❖ c",
394
- "lang.cpp": "❖ c++",
395
- "lang.csharp": "❖ c#",
396
- "lang.ruby": "❖ rb",
397
- "lang.php": "❖ php",
398
- "lang.swift": "❖ swift",
399
- "lang.kotlin": "❖ kt",
400
- "lang.shell": "❖ sh",
401
- "lang.html": "❖ html",
402
- "lang.css": "❖ css",
403
- "lang.json": "❖ json",
404
- "lang.yaml": "❖ yaml",
405
- "lang.markdown": "❖ md",
406
- "lang.sql": "❖ sql",
407
- "lang.docker": "❖ docker",
408
- "lang.lua": "❖ lua",
409
- "lang.text": "❖ txt",
410
- "lang.env": "❖ env",
411
- "lang.toml": "❖ toml",
412
- "lang.xml": "❖ xml",
413
- "lang.ini": "❖ ini",
414
- "lang.conf": "❖ conf",
415
- "lang.log": "❖ log",
416
- "lang.csv": "❖ csv",
417
- "lang.tsv": "❖ tsv",
418
- "lang.image": "❖ img",
419
- "lang.pdf": "❖ pdf",
420
- "lang.archive": "❖ zip",
421
- "lang.binary": "❖ bin",
422
- // Settings tab icons
292
+ // Language/file icons (emoji-centric, no Nerd Font required)
293
+ "lang.default": "",
294
+ "lang.typescript": "🟦",
295
+ "lang.javascript": "🟨",
296
+ "lang.python": "🐍",
297
+ "lang.rust": "🦀",
298
+ "lang.go": "🐹",
299
+ "lang.java": "",
300
+ "lang.c": "",
301
+ "lang.cpp": "",
302
+ "lang.csharp": "",
303
+ "lang.ruby": "💎",
304
+ "lang.php": "🐘",
305
+ "lang.swift": "🕊",
306
+ "lang.kotlin": "🅺",
307
+ "lang.shell": "💻",
308
+ "lang.html": "🌐",
309
+ "lang.css": "🎨",
310
+ "lang.json": "🧾",
311
+ "lang.yaml": "📋",
312
+ "lang.markdown": "📝",
313
+ "lang.sql": "🗄",
314
+ "lang.docker": "🐳",
315
+ "lang.lua": "🌙",
316
+ "lang.text": "🗒",
317
+ "lang.env": "🔧",
318
+ "lang.toml": "🧾",
319
+ "lang.xml": "⟨⟩",
320
+ "lang.ini": "",
321
+ "lang.conf": "",
322
+ "lang.log": "📜",
323
+ "lang.csv": "📑",
324
+ "lang.tsv": "📑",
325
+ "lang.image": "🖼",
326
+ "lang.pdf": "📕",
327
+ "lang.archive": "🗜",
328
+ "lang.binary": "",
329
+ // Settings tabs
423
330
  "tab.display": "🎨",
424
331
  "tab.agent": "🤖",
425
332
  "tab.input": "⌨",
@@ -846,7 +753,7 @@ export type SpinnerType = "status" | "activity";
846
753
 
847
754
  const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
848
755
  unicode: {
849
- status: ["·", "", "", ""],
756
+ status: ["", "", "", "", "⡿", "⣟", "⣯", "⣷"],
850
757
  activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
851
758
  },
852
759
  nerd: {
@@ -179,6 +179,7 @@ Continue non-destructively; someone's work may live there.
179
179
  - Ask for parameters only when required; otherwise choose safe defaults, state them.
180
180
  - Non-trivial logic: define test first when feasible.
181
181
  - Algorithmic work: start naive correct version before optimizing.
182
+ - **Formatting is a batch operation, not per-edit cleanup.** Never fix indentation or style issues one edit at a time. Make all semantic changes first, then run the project's formatter once at the end. One command beats twenty whitespace edits.
182
183
 
183
184
  ## Integration
184
185
  - AGENTS.md defines local law; nearest wins, deeper overrides higher.
@@ -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
- - **NEVER** use edit to fix indentation or reformat coderun the project's formatter instead
51
+ - **NEVER** use edit to fix indentation, whitespace, or reformat code. Formatting is a single command run once at the end (`bun fmt`, `cargo fmt`, `prettier --write`, etc.)—not N individual edits. If you see inconsistent indentation after an edit, leave it; the formatter will fix all of it in one pass.
52
52
  </critical>
53
53
 
54
54
  <example name="create">
@@ -17,7 +17,7 @@ import type { ToolSession } from ".";
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 { shortenPath, ToolUIKit, truncateToWidth } from "./render-utils";
20
+ import { replaceTabs, shortenPath, ToolUIKit, truncateToWidth } from "./render-utils";
21
21
  import { ToolAbortError, ToolError } from "./tool-errors";
22
22
  import { toolResult } from "./tool-result";
23
23
  import { DEFAULT_MAX_BYTES } from "./truncate";
@@ -341,18 +341,93 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
341
341
  cellResult.status = "error";
342
342
  pushUpdate();
343
343
  const errorMsg = result.output || "Command aborted";
344
- throw new ToolError(cells.length > 1 ? `Cell ${i + 1} aborted: ${errorMsg}` : errorMsg);
344
+ const combinedOutput = cellOutputs.join("\n\n");
345
+ const outputText =
346
+ cells.length > 1
347
+ ? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
348
+ : combinedOutput || errorMsg;
349
+
350
+ const rawSummary = (await finalizeOutput()) ?? {
351
+ output: "",
352
+ truncated: false,
353
+ totalLines: 0,
354
+ totalBytes: 0,
355
+ outputLines: 0,
356
+ outputBytes: 0,
357
+ };
358
+ const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
359
+ const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
360
+ const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
361
+ const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
362
+ const summaryForMeta: OutputSummary = {
363
+ output: combinedOutput,
364
+ truncated: rawSummary.truncated,
365
+ totalLines: outputLines + missingLines,
366
+ totalBytes: outputBytes + missingBytes,
367
+ outputLines,
368
+ outputBytes,
369
+ artifactId: rawSummary.artifactId,
370
+ };
371
+
372
+ const details: PythonToolDetails = {
373
+ cells: cellResults,
374
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
375
+ images: images.length > 0 ? images : undefined,
376
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
377
+ isError: true,
378
+ };
379
+
380
+ return toolResult(details)
381
+ .text(outputText)
382
+ .truncationFromSummary(summaryForMeta, { direction: "tail" })
383
+ .done();
345
384
  }
346
385
 
347
386
  if (result.exitCode !== 0 && result.exitCode !== undefined) {
348
387
  cellResult.status = "error";
349
388
  pushUpdate();
350
389
  const combinedOutput = cellOutputs.join("\n\n");
351
- throw new ToolError(
390
+ const outputText =
352
391
  cells.length > 1
353
392
  ? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
354
- : `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`,
355
- );
393
+ : combinedOutput
394
+ ? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
395
+ : `Command exited with code ${result.exitCode}`;
396
+
397
+ const rawSummary = (await finalizeOutput()) ?? {
398
+ output: "",
399
+ truncated: false,
400
+ totalLines: 0,
401
+ totalBytes: 0,
402
+ outputLines: 0,
403
+ outputBytes: 0,
404
+ };
405
+ const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
406
+ const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
407
+ const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
408
+ const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
409
+ const summaryForMeta: OutputSummary = {
410
+ output: combinedOutput,
411
+ truncated: rawSummary.truncated,
412
+ totalLines: outputLines + missingLines,
413
+ totalBytes: outputBytes + missingBytes,
414
+ outputLines,
415
+ outputBytes,
416
+ artifactId: rawSummary.artifactId,
417
+ };
418
+
419
+ const details: PythonToolDetails = {
420
+ cells: cellResults,
421
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
422
+ images: images.length > 0 ? images : undefined,
423
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
424
+ isError: true,
425
+ };
426
+
427
+ return toolResult(details)
428
+ .text(outputText)
429
+ .truncationFromSummary(summaryForMeta, { direction: "tail" })
430
+ .done();
356
431
  }
357
432
 
358
433
  cellResult.status = "complete";
@@ -627,7 +702,7 @@ function formatStatusEventExpanded(event: PythonStatusEvent, theme: Theme): stri
627
702
  const addPreview = (preview: string, maxLines = 3) => {
628
703
  const previewLines = String(preview).split("\n").slice(0, maxLines);
629
704
  for (const line of previewLines) {
630
- lines.push(` ${theme.fg("toolOutput", truncateToWidth(line, 80))}`);
705
+ lines.push(` ${theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}`);
631
706
  }
632
707
  const totalLines = String(preview).split("\n").length;
633
708
  if (totalLines > maxLines) {
@@ -744,7 +819,10 @@ function formatCellOutputLines(
744
819
  const rawLines = cell.output ? cell.output.split("\n") : [];
745
820
  const displayLines = expanded ? rawLines : rawLines.slice(-previewLines);
746
821
  const hiddenCount = rawLines.length - displayLines.length;
747
- const outputLines = displayLines.map(line => theme.fg("toolOutput", line));
822
+ const outputLines = displayLines.map(line => {
823
+ const cleaned = replaceTabs(line);
824
+ return cell.status === "error" ? theme.fg("error", cleaned) : theme.fg("toolOutput", cleaned);
825
+ });
748
826
 
749
827
  if (outputLines.length === 0) {
750
828
  return { lines: [], hiddenCount: 0 };