@oh-my-pi/pi-coding-agent 14.7.2 → 14.7.4

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 (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +7 -7
  3. package/src/cli/read-cli.ts +1 -2
  4. package/src/commands/read.ts +2 -7
  5. package/src/config/settings-schema.ts +0 -5
  6. package/src/edit/modes/hashline.ts +40 -19
  7. package/src/edit/modes/patch.ts +7 -5
  8. package/src/edit/modes/replace.ts +6 -2
  9. package/src/edit/notebook.ts +222 -0
  10. package/src/edit/read-file.ts +7 -0
  11. package/src/edit/renderer.ts +4 -3
  12. package/src/edit/streaming.ts +49 -7
  13. package/src/modes/components/diff.ts +54 -7
  14. package/src/modes/components/tool-execution.ts +3 -29
  15. package/src/prompts/agents/designer.md +1 -2
  16. package/src/prompts/agents/explore.md +2 -5
  17. package/src/prompts/agents/init.md +1 -4
  18. package/src/prompts/agents/librarian.md +1 -3
  19. package/src/prompts/agents/plan.md +7 -8
  20. package/src/prompts/agents/reviewer.md +1 -2
  21. package/src/prompts/ci-green-request.md +10 -10
  22. package/src/prompts/commands/orchestrate.md +48 -0
  23. package/src/prompts/memories/consolidation.md +10 -10
  24. package/src/prompts/memories/read-path.md +6 -6
  25. package/src/prompts/system/agent-creation-architect.md +54 -44
  26. package/src/prompts/system/custom-system-prompt.md +3 -5
  27. package/src/prompts/system/eager-todo.md +4 -4
  28. package/src/prompts/system/handoff-document.md +7 -4
  29. package/src/prompts/system/plan-mode-active.md +7 -3
  30. package/src/prompts/system/plan-mode-approved.md +5 -5
  31. package/src/prompts/system/summarization-system.md +2 -2
  32. package/src/prompts/system/system-prompt.md +53 -65
  33. package/src/prompts/system/title-system.md +2 -2
  34. package/src/prompts/system/web-search.md +16 -19
  35. package/src/prompts/tools/bash.md +8 -8
  36. package/src/prompts/tools/browser.md +4 -4
  37. package/src/prompts/tools/debug.md +3 -1
  38. package/src/prompts/tools/eval.md +13 -9
  39. package/src/prompts/tools/hashline.md +4 -2
  40. package/src/prompts/tools/image-gen.md +1 -1
  41. package/src/prompts/tools/read.md +1 -2
  42. package/src/prompts/tools/reflect.md +3 -3
  43. package/src/prompts/tools/render-mermaid.md +2 -2
  44. package/src/prompts/tools/resolve.md +2 -2
  45. package/src/prompts/tools/retain.md +3 -2
  46. package/src/prompts/tools/rewind.md +2 -2
  47. package/src/prompts/tools/search-tool-bm25.md +3 -4
  48. package/src/prompts/tools/task.md +1 -1
  49. package/src/prompts/tools/todo-write.md +2 -2
  50. package/src/task/commands.ts +5 -1
  51. package/src/tools/fetch.ts +6 -7
  52. package/src/tools/index.ts +0 -4
  53. package/src/tools/read.ts +18 -7
  54. package/src/tools/renderers.ts +0 -2
  55. package/src/tools/write.ts +41 -26
  56. package/src/tools/notebook.ts +0 -286
@@ -1,6 +1,6 @@
1
- Ends an active checkpoint and rewinds context back to that checkpoint, replacing intermediate exploration with your report.
1
+ End an active checkpoint. Rewind context to it, replacing intermediate exploration with your report.
2
2
 
3
- Use this immediately after investigative work started with `checkpoint`.
3
+ Call immediately after `checkpoint`-started investigative work.
4
4
 
5
5
  Requirements:
6
6
  - `report` is **REQUIRED** and must be concise, factual, and actionable.
@@ -1,7 +1,6 @@
1
1
  Search hidden tool metadata to discover and activate tools.
2
2
 
3
- Use this tool when you need a capability that is not currently available in your active tool set. It searches all discoverable tools — including MCP tools and built-in tools that are hidden to save tokens.
4
-
3
+ Activate hidden tools (MCP and built-in) when you need a capability not in your active tool set.
5
4
  {{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
6
5
  {{#if discoverableMCPToolCount}}Total discoverable tools available: {{discoverableMCPToolCount}}.{{/if}}
7
6
  Input:
@@ -16,7 +15,7 @@ Behavior:
16
15
  - Newly activated tools become available before the next model call in the same overall turn
17
16
 
18
17
  Notes:
19
- - If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
18
+ Start with `limit` 510 if unsure.
20
19
  - `query` is matched against tool metadata fields:
21
20
  - `name`
22
21
  - `label`
@@ -25,7 +24,7 @@ Notes:
25
24
  - `description` / `summary`
26
25
  - input schema property keys (`schema_keys`)
27
26
 
28
- This is not repository search, file search, or code search. Use it only for tool discovery.
27
+ Not for repository/file/code search. Tool discovery only.
29
28
 
30
29
  Returns JSON with:
31
30
  - `query`
@@ -5,7 +5,7 @@ Launches subagents to parallelize workflows.
5
5
  - Use `job` (with `poll`) to wait. **MUST NOT** poll `read jobs://` in a loop.
6
6
  {{/if}}
7
7
 
8
- Subagents have no access to your conversation history. Every fact, file path, and decision they need **MUST** be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
8
+ Subagents have no conversation history. Every fact, file path, and decision they need **MUST** be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
9
9
 
10
10
  <parameters>
11
11
  - `agent`: agent type for all tasks
@@ -6,12 +6,12 @@ Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, an
6
6
 
7
7
  |`op`|Required fields|Effect|
8
8
  |---|---|---|
9
- |`init`|`list`|Initialize the full list|
9
+ |`init`|`list: [{phase, items: string[]}]`|Initialize the full list (replaces any existing list)|
10
10
  |`start`|`task`|Mark in progress|
11
11
  |`done`|`task` or `phase`|Mark completed|
12
12
  |`drop`|`task` or `phase`|Mark abandoned|
13
13
  |`rm`|`task` or `phase`|Remove|
14
- |`append`|`phase`, `items: string[]`|Append tasks; lazily creates phase|
14
+ |`append`|`phase`, `items: string[]`|Append tasks to `phase`; lazily creates phase|
15
15
  |`note`|`task`, `text`|Append a note to a task. Reminders for future-you only.|
16
16
 
17
17
  ## Anatomy
@@ -9,8 +9,12 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
9
9
  import { loadCapability } from "../discovery";
10
10
  // Embed command markdown files at build time
11
11
  import initMd from "../prompts/agents/init.md" with { type: "text" };
12
+ import orchestrateMd from "../prompts/commands/orchestrate.md" with { type: "text" };
12
13
 
13
- const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
14
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
15
+ { name: "init.md", content: prompt.render(initMd) },
16
+ { name: "orchestrate.md", content: prompt.render(orchestrateMd) },
17
+ ];
14
18
 
15
19
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
16
20
 
@@ -1220,13 +1220,13 @@ function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: bool
1220
1220
 
1221
1221
  async function buildReadUrlCacheEntry(
1222
1222
  session: ToolSession,
1223
- params: { path: string; timeout?: number; raw?: boolean },
1223
+ params: { path: string; raw?: boolean },
1224
1224
  signal?: AbortSignal,
1225
1225
  options?: { ensureArtifact?: boolean },
1226
1226
  ): Promise<ReadUrlCacheEntry> {
1227
- const { path: url, timeout: rawTimeout = 20, raw = false } = params;
1227
+ const { path: url, raw = false } = params;
1228
1228
 
1229
- const effectiveTimeout = clampTimeout("fetch", rawTimeout);
1229
+ const effectiveTimeout = clampTimeout("fetch", 30);
1230
1230
 
1231
1231
  if (signal?.aborted) {
1232
1232
  throw new ToolAbortError();
@@ -1254,7 +1254,7 @@ async function buildReadUrlCacheEntry(
1254
1254
 
1255
1255
  export async function loadReadUrlCacheEntry(
1256
1256
  session: ToolSession,
1257
- params: { path: string; timeout?: number; raw?: boolean },
1257
+ params: { path: string; raw?: boolean },
1258
1258
  signal?: AbortSignal,
1259
1259
  options?: { ensureArtifact?: boolean; preferCached?: boolean },
1260
1260
  ): Promise<ReadUrlCacheEntry> {
@@ -1291,7 +1291,7 @@ function buildUrlReadOutput(result: FetchRenderResult, content: string): string
1291
1291
 
1292
1292
  export async function executeReadUrl(
1293
1293
  session: ToolSession,
1294
- params: { path: string; timeout?: number; raw?: boolean },
1294
+ params: { path: string; raw?: boolean },
1295
1295
  signal?: AbortSignal,
1296
1296
  ): Promise<AgentToolResult<ReadUrlToolDetails>> {
1297
1297
  let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
@@ -1345,7 +1345,7 @@ function countNonEmptyLines(text: string): number {
1345
1345
 
1346
1346
  /** Render URL read call (URL preview) */
1347
1347
  export function renderReadUrlCall(
1348
- args: { path?: string; url?: string; timeout?: number; raw?: boolean },
1348
+ args: { path?: string; url?: string; raw?: boolean },
1349
1349
  _options: RenderResultOptions,
1350
1350
  uiTheme: Theme = theme,
1351
1351
  ): Component {
@@ -1355,7 +1355,6 @@ export function renderReadUrlCall(
1355
1355
  const description = `${domain}${path ? ` ${path}` : ""}`.trim();
1356
1356
  const meta: string[] = [];
1357
1357
  if (args.raw) meta.push("raw");
1358
- if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
1359
1358
  const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
1360
1359
  return new Text(text, 0, 0);
1361
1360
  }
@@ -37,7 +37,6 @@ import { HindsightRetainTool } from "./hindsight-retain";
37
37
  import { InspectImageTool } from "./inspect-image";
38
38
  import { IrcTool } from "./irc";
39
39
  import { JobTool } from "./job";
40
- import { NotebookTool } from "./notebook";
41
40
  import { wrapToolWithMetaNotice } from "./output-meta";
42
41
  import { ReadTool } from "./read";
43
42
  import { RecipeTool } from "./recipe";
@@ -80,7 +79,6 @@ export * from "./image-gen";
80
79
  export * from "./inspect-image";
81
80
  export * from "./irc";
82
81
  export * from "./job";
83
- export * from "./notebook";
84
82
  export * from "./read";
85
83
  export * from "./recipe";
86
84
  export * from "./render-mermaid";
@@ -271,7 +269,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
271
269
  find: s => new FindTool(s),
272
270
  search: s => new SearchTool(s),
273
271
  lsp: LspTool.createIf,
274
- notebook: s => new NotebookTool(s),
275
272
  inspect_image: s => new InspectImageTool(s),
276
273
  browser: s => new BrowserTool(s),
277
274
  checkpoint: CheckpointTool.createIf,
@@ -430,7 +427,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
430
427
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
431
428
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
432
429
  if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
433
- if (name === "notebook") return session.settings.get("notebook.enabled");
434
430
  if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
435
431
  if (name === "web_search") return session.settings.get("web_search.enabled");
436
432
  // search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
package/src/tools/read.ts CHANGED
@@ -9,6 +9,7 @@ import { Text } from "@oh-my-pi/pi-tui";
9
9
  import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import { type Static, Type } from "@sinclair/typebox";
11
11
  import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
12
+ import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
12
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
14
  import { parseInternalUrl } from "../internal-urls/parse";
14
15
  import type { InternalUrl } from "../internal-urls/types";
@@ -418,7 +419,6 @@ const readSchema = Type.Object({
418
419
  description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
419
420
  examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com:L1-L40"],
420
421
  }),
421
- timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 20 })),
422
422
  });
423
423
 
424
424
  export type ReadToolInput = Static<typeof readSchema>;
@@ -1084,7 +1084,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1084
1084
  _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
1085
1085
  _toolContext?: AgentToolContext,
1086
1086
  ): Promise<AgentToolResult<ReadToolDetails>> {
1087
- let { path: readPath, timeout } = params;
1087
+ let { path: readPath } = params;
1088
1088
  if (readPath.startsWith("file://")) {
1089
1089
  readPath = expandPath(readPath);
1090
1090
  }
@@ -1098,7 +1098,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1098
1098
  if (parsedUrlTarget.offset !== undefined || parsedUrlTarget.limit !== undefined) {
1099
1099
  const cached = await loadReadUrlCacheEntry(
1100
1100
  this.session,
1101
- { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw },
1101
+ { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
1102
1102
  signal,
1103
1103
  {
1104
1104
  ensureArtifact: true,
@@ -1111,7 +1111,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1111
1111
  entityLabel: "URL output",
1112
1112
  });
1113
1113
  }
1114
- return executeReadUrl(this.session, { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw }, signal);
1114
+ return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
1115
1115
  }
1116
1116
 
1117
1117
  // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
@@ -1196,7 +1196,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1196
1196
  const ext = path.extname(absolutePath).toLowerCase();
1197
1197
  const _hasEditTool = this.session.hasEditTool ?? true;
1198
1198
  const _language = getLanguageFromPath(absolutePath);
1199
- const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext) || (ext === ".ipynb" && parsed.kind === "raw");
1199
+ const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
1200
1200
  // Read the file based on type
1201
1201
  let content: Array<TextContent | ImageContent> | undefined;
1202
1202
  let details: ReadToolDetails = {};
@@ -1263,8 +1263,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1263
1263
  throw error;
1264
1264
  }
1265
1265
  }
1266
+ } else if (isNotebookPath(absolutePath) && parsed.kind !== "raw") {
1267
+ const { offset, limit } = selToOffsetLimit(parsed);
1268
+ return this.#buildInMemoryTextResult(
1269
+ await readEditableNotebookText(absolutePath, localReadPath),
1270
+ offset,
1271
+ limit,
1272
+ {
1273
+ details: { resolvedPath: absolutePath },
1274
+ sourcePath: absolutePath,
1275
+ entityLabel: "notebook",
1276
+ },
1277
+ );
1266
1278
  } else if (shouldConvertWithMarkit) {
1267
- // Convert document or notebook via markit.
1279
+ // Convert document via markit.
1268
1280
  const result = await convertFileWithMarkit(absolutePath, signal);
1269
1281
  if (result.ok) {
1270
1282
  // Apply truncation to converted content
@@ -1556,7 +1568,6 @@ interface ReadRenderArgs {
1556
1568
  path?: string;
1557
1569
  file_path?: string;
1558
1570
  sel?: string;
1559
- timeout?: number;
1560
1571
  // Legacy fields from old schema — tolerated for in-flight tool calls during transition
1561
1572
  offset?: number;
1562
1573
  limit?: number;
@@ -22,7 +22,6 @@ import { findToolRenderer } from "./find";
22
22
  import { githubToolRenderer } from "./gh-renderer";
23
23
  import { inspectImageToolRenderer } from "./inspect-image-renderer";
24
24
  import { jobToolRenderer } from "./job";
25
- import { notebookToolRenderer } from "./notebook";
26
25
  import { readToolRenderer } from "./read";
27
26
  import { recipeToolRenderer } from "./recipe/render";
28
27
  import { resolveToolRenderer } from "./resolve";
@@ -60,7 +59,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
60
59
  find: findToolRenderer as ToolRenderer,
61
60
  search: searchToolRenderer as ToolRenderer,
62
61
  lsp: lspToolRenderer as ToolRenderer,
63
- notebook: notebookToolRenderer as ToolRenderer,
64
62
  inspect_image: inspectImageToolRenderer as ToolRenderer,
65
63
  read: readToolRenderer as ToolRenderer,
66
64
  job: jobToolRenderer as ToolRenderer,
@@ -9,7 +9,7 @@ import { type Static, Type } from "@sinclair/typebox";
9
9
  import { stripHashlinePrefixes } from "../edit";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
12
- import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
12
+ import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
13
13
  import writeDescription from "../prompts/tools/write.md" with { type: "text" };
14
14
  import type { ToolSession } from "../sdk";
15
15
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
@@ -168,6 +168,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
168
168
  readonly concurrency = "exclusive";
169
169
  readonly loadMode = "discoverable";
170
170
  readonly summary = "Write content to a file (creates or overwrites)";
171
+ readonly intent = (args: Partial<WriteToolInput>) => (args.path ? `writing ${args.path}` : "writing");
171
172
 
172
173
  readonly #writethrough: WritethroughCallback;
173
174
 
@@ -523,52 +524,67 @@ function countLines(text: string): number {
523
524
  return text.split("\n").length;
524
525
  }
525
526
 
526
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
527
- const icon = uiTheme.getLangIcon(language);
528
- if (lineCount !== null) {
529
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
530
- }
531
- return uiTheme.fg("dim", `${icon}`);
527
+ function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
528
+ if (lineCount <= 0) return "";
529
+ return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
532
530
  }
533
531
 
534
532
  function normalizeDisplayText(text: string): string {
535
533
  return text.replace(/\r/g, "");
536
534
  }
537
535
 
538
- function formatStreamingContent(content: string, uiTheme: Theme): string {
536
+ function formatStreamingContent(content: string, language: string | undefined, uiTheme: Theme): string {
539
537
  if (!content) return "";
540
538
  const lines = normalizeDisplayText(content).split("\n");
541
- const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
542
- const hidden = lines.length - displayLines.length;
539
+ const totalLines = lines.length;
540
+ const startIndex = Math.max(0, totalLines - WRITE_STREAMING_PREVIEW_LINES);
541
+ const visibleLines = lines.slice(startIndex);
542
+ const hidden = startIndex;
543
+ const highlighted = highlightCode(visibleLines.join("\n"), language);
544
+ const lineNumberWidth = String(totalLines).length;
543
545
 
544
546
  let text = "\n\n";
545
547
  if (hidden > 0) {
546
- text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
548
+ text += `${uiTheme.fg("dim", `… (${hidden} earlier line${hidden === 1 ? "" : "s"})`)}\n`;
547
549
  }
548
- for (const line of displayLines) {
549
- text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
550
+ for (let i = 0; i < highlighted.length; i++) {
551
+ const lineNum = startIndex + i + 1;
552
+ const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
553
+ const body = replaceTabs(highlighted[i] ?? "");
554
+ text += ` ${gutter}${body}\n`;
550
555
  }
551
556
  text += uiTheme.fg("dim", `… (streaming)`);
552
557
  return text;
553
558
  }
554
559
 
555
- function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme): string {
560
+ function renderContentPreview(
561
+ content: string,
562
+ expanded: boolean,
563
+ language: string | undefined,
564
+ uiTheme: Theme,
565
+ ): string {
556
566
  if (!content) return "";
557
- const lines = normalizeDisplayText(content).split("\n");
558
- const maxLines = expanded ? lines.length : Math.min(lines.length, WRITE_PREVIEW_LINES);
559
- const displayLines = expanded ? lines : lines.slice(-maxLines);
560
- const hidden = lines.length - displayLines.length;
567
+ const rawLines = normalizeDisplayText(content).split("\n");
568
+ const totalLines = rawLines.length;
569
+ const maxLines = expanded ? totalLines : Math.min(totalLines, WRITE_PREVIEW_LINES);
570
+ const visibleLines = rawLines.slice(0, maxLines);
571
+ const highlighted = highlightCode(visibleLines.join("\n"), language);
572
+ const lineNumberWidth = String(maxLines).length;
573
+ const hidden = totalLines - maxLines;
561
574
 
562
575
  let text = "\n\n";
563
- for (const line of displayLines) {
564
- text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
576
+ for (let i = 0; i < highlighted.length; i++) {
577
+ const lineNum = i + 1;
578
+ const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
579
+ const body = replaceTabs(highlighted[i] ?? "");
580
+ text += ` ${gutter}${body}\n`;
565
581
  }
566
582
  if (!expanded && hidden > 0) {
567
583
  const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
568
584
  const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
569
585
  text += uiTheme.fg("dim", moreLine);
570
586
  }
571
- return text;
587
+ return text.trimEnd();
572
588
  }
573
589
 
574
590
  export const writeToolRenderer = {
@@ -588,7 +604,7 @@ export const writeToolRenderer = {
588
604
  }
589
605
 
590
606
  // Show streaming preview of content (tail)
591
- text += formatStreamingContent(args.content, uiTheme);
607
+ text += formatStreamingContent(args.content, lang, uiTheme);
592
608
 
593
609
  return new Text(text, 0, 0);
594
610
  },
@@ -606,17 +622,17 @@ export const writeToolRenderer = {
606
622
  const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
607
623
  const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
608
624
  const lineCount = countLines(fileContent);
625
+ const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
609
626
 
610
627
  // Build header with status icon
611
628
  const header = renderStatusLine(
612
629
  {
613
630
  icon: "success",
614
631
  title: "Write",
615
- description: `${langIcon} ${pathDisplay}`,
632
+ description: `${langIcon} ${pathDisplay}${lineSuffix}`,
616
633
  },
617
634
  uiTheme,
618
635
  );
619
- const metadataLine = formatMetadataLine(lineCount, lang ?? "text", uiTheme);
620
636
  const diagnostics = result.details?.diagnostics;
621
637
 
622
638
  let cached: RenderCache | undefined;
@@ -628,8 +644,7 @@ export const writeToolRenderer = {
628
644
  if (cached?.key === key) return cached.lines;
629
645
 
630
646
  let text = header;
631
- text += `\n${metadataLine}`;
632
- text += renderContentPreview(fileContent, expanded, uiTheme);
647
+ text += renderContentPreview(fileContent, expanded, lang, uiTheme);
633
648
 
634
649
  if (diagnostics) {
635
650
  const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>