@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6

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 (165) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/types/capability/rule-buckets.d.ts +1 -1
  3. package/dist/types/capability/rule.d.ts +6 -1
  4. package/dist/types/cli/update-cli.d.ts +11 -1
  5. package/dist/types/config/model-registry.d.ts +18 -1
  6. package/dist/types/discovery/at-imports.d.ts +15 -0
  7. package/dist/types/edit/diff.d.ts +3 -2
  8. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
  9. package/dist/types/eval/backend.d.ts +7 -0
  10. package/dist/types/eval/js/context-manager.d.ts +1 -0
  11. package/dist/types/eval/js/executor.d.ts +2 -0
  12. package/dist/types/eval/js/index.d.ts +1 -1
  13. package/dist/types/eval/js/shared/helpers.d.ts +6 -0
  14. package/dist/types/eval/js/shared/runtime.d.ts +5 -0
  15. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  16. package/dist/types/eval/py/executor.d.ts +7 -0
  17. package/dist/types/eval/py/index.d.ts +1 -1
  18. package/dist/types/exa/index.d.ts +1 -19
  19. package/dist/types/exa/mcp-client.d.ts +10 -3
  20. package/dist/types/exa/types.d.ts +0 -83
  21. package/dist/types/export/ttsr.d.ts +14 -0
  22. package/dist/types/extensibility/extensions/types.d.ts +8 -1
  23. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  24. package/dist/types/internal-urls/local-protocol.d.ts +10 -0
  25. package/dist/types/mcp/oauth-flow.d.ts +2 -2
  26. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  27. package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
  28. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +31 -2
  30. package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
  31. package/dist/types/modes/image-references.d.ts +8 -3
  32. package/dist/types/modes/interactive-mode.d.ts +9 -1
  33. package/dist/types/modes/theme/theme.d.ts +2 -1
  34. package/dist/types/modes/types.d.ts +3 -1
  35. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  36. package/dist/types/session/agent-session.d.ts +0 -2
  37. package/dist/types/task/render.d.ts +1 -0
  38. package/dist/types/tools/ask.d.ts +1 -0
  39. package/dist/types/tools/browser/tab-worker.d.ts +15 -0
  40. package/dist/types/tools/index.d.ts +17 -2
  41. package/dist/types/tools/render-utils.d.ts +1 -1
  42. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  43. package/dist/types/utils/block-context.d.ts +35 -0
  44. package/dist/types/utils/git.d.ts +6 -0
  45. package/dist/types/utils/image-loading.d.ts +12 -0
  46. package/package.json +29 -9
  47. package/src/capability/rule-buckets.ts +4 -2
  48. package/src/capability/rule.ts +10 -1
  49. package/src/cli/auth-broker-cli.ts +6 -7
  50. package/src/cli/auth-gateway-cli.ts +4 -3
  51. package/src/cli/list-models.ts +5 -0
  52. package/src/cli/update-cli.ts +138 -16
  53. package/src/commit/agentic/tools/split-commit.ts +8 -1
  54. package/src/config/model-provider-priority.ts +1 -0
  55. package/src/config/model-registry.ts +81 -2
  56. package/src/debug/index.ts +4 -8
  57. package/src/discovery/at-imports.ts +273 -0
  58. package/src/discovery/builtin-rules/index.ts +4 -0
  59. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  60. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  61. package/src/discovery/helpers.ts +2 -1
  62. package/src/edit/diff.ts +114 -4
  63. package/src/edit/hashline/diff.ts +1 -1
  64. package/src/edit/hashline/execute.ts +1 -1
  65. package/src/edit/modes/patch.ts +6 -2
  66. package/src/edit/modes/replace.ts +1 -1
  67. package/src/edit/renderer.ts +12 -2
  68. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  69. package/src/eval/backend.ts +15 -0
  70. package/src/eval/js/context-manager.ts +4 -2
  71. package/src/eval/js/executor.ts +3 -0
  72. package/src/eval/js/index.ts +7 -1
  73. package/src/eval/js/shared/helpers.ts +53 -6
  74. package/src/eval/js/shared/runtime.ts +8 -0
  75. package/src/eval/js/worker-core.ts +1 -0
  76. package/src/eval/js/worker-protocol.ts +6 -0
  77. package/src/eval/py/executor.ts +12 -0
  78. package/src/eval/py/index.ts +7 -1
  79. package/src/eval/py/prelude.py +43 -4
  80. package/src/eval/py/runner.py +1 -0
  81. package/src/exa/index.ts +1 -26
  82. package/src/exa/mcp-client.ts +10 -10
  83. package/src/exa/types.ts +0 -97
  84. package/src/export/ttsr.ts +122 -1
  85. package/src/extensibility/extensions/types.ts +8 -1
  86. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  87. package/src/extensibility/plugins/doctor.ts +1 -1
  88. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
  89. package/src/goals/tools/goal-tool.ts +1 -1
  90. package/src/internal-urls/docs-index.generated.ts +7 -6
  91. package/src/internal-urls/local-protocol.ts +13 -0
  92. package/src/lsp/render.ts +8 -6
  93. package/src/mcp/oauth-flow.ts +3 -3
  94. package/src/mcp/render.ts +7 -1
  95. package/src/modes/components/agent-dashboard.ts +6 -4
  96. package/src/modes/components/custom-editor.ts +12 -6
  97. package/src/modes/components/login-dialog.ts +1 -1
  98. package/src/modes/components/oauth-selector.ts +4 -4
  99. package/src/modes/components/read-tool-group.ts +10 -3
  100. package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
  101. package/src/modes/components/status-line/index.ts +1 -0
  102. package/src/modes/components/status-line/types.ts +23 -8
  103. package/src/modes/components/tool-execution.ts +1 -1
  104. package/src/modes/components/transcript-container.ts +17 -10
  105. package/src/modes/components/user-message.ts +6 -3
  106. package/src/modes/components/welcome.ts +1 -1
  107. package/src/modes/controllers/event-controller.ts +8 -0
  108. package/src/modes/controllers/extension-ui-controller.ts +143 -127
  109. package/src/modes/controllers/input-controller.ts +60 -11
  110. package/src/modes/controllers/mcp-command-controller.ts +52 -17
  111. package/src/modes/controllers/selector-controller.ts +4 -11
  112. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  113. package/src/modes/image-references.ts +13 -7
  114. package/src/modes/interactive-mode.ts +35 -3
  115. package/src/modes/rpc/rpc-mode.ts +1 -1
  116. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
  117. package/src/modes/theme/theme.ts +95 -1
  118. package/src/modes/types.ts +3 -1
  119. package/src/modes/utils/ui-helpers.ts +14 -5
  120. package/src/prompts/tools/bash.md +1 -1
  121. package/src/prompts/tools/eval.md +4 -4
  122. package/src/sdk.ts +31 -14
  123. package/src/session/agent-session.ts +290 -196
  124. package/src/session/session-manager.ts +1 -1
  125. package/src/slash-commands/builtin-registry.ts +9 -1
  126. package/src/system-prompt.ts +15 -9
  127. package/src/task/index.ts +9 -1
  128. package/src/task/render.ts +36 -14
  129. package/src/tools/ask.ts +14 -5
  130. package/src/tools/bash-interactive.ts +1 -1
  131. package/src/tools/bash.ts +14 -2
  132. package/src/tools/browser/render.ts +5 -2
  133. package/src/tools/browser/tab-worker.ts +211 -91
  134. package/src/tools/debug.ts +5 -2
  135. package/src/tools/eval-render.ts +6 -3
  136. package/src/tools/eval.ts +1 -1
  137. package/src/tools/gh-renderer.ts +29 -15
  138. package/src/tools/index.ts +32 -4
  139. package/src/tools/inspect-image-renderer.ts +12 -5
  140. package/src/tools/job.ts +9 -6
  141. package/src/tools/memory-render.ts +19 -5
  142. package/src/tools/read.ts +165 -18
  143. package/src/tools/render-utils.ts +3 -1
  144. package/src/tools/resolve.ts +1 -1
  145. package/src/tools/review.ts +1 -1
  146. package/src/tools/ssh.ts +4 -1
  147. package/src/tools/todo.ts +8 -1
  148. package/src/tools/tool-timeouts.ts +1 -1
  149. package/src/tools/write.ts +1 -1
  150. package/src/tui/code-cell.ts +1 -1
  151. package/src/utils/block-context.ts +312 -0
  152. package/src/utils/git.ts +41 -0
  153. package/src/utils/image-loading.ts +31 -1
  154. package/src/web/search/providers/codex.ts +1 -1
  155. package/src/web/search/render.ts +14 -6
  156. package/dist/types/exa/factory.d.ts +0 -13
  157. package/dist/types/exa/render.d.ts +0 -19
  158. package/dist/types/exa/researcher.d.ts +0 -9
  159. package/dist/types/exa/search.d.ts +0 -9
  160. package/dist/types/exa/websets.d.ts +0 -9
  161. package/src/exa/factory.ts +0 -60
  162. package/src/exa/render.ts +0 -244
  163. package/src/exa/researcher.ts +0 -36
  164. package/src/exa/search.ts +0 -47
  165. package/src/exa/websets.ts +0 -248
package/src/tools/read.ts CHANGED
@@ -9,7 +9,12 @@ import type { Component } from "@oh-my-pi/pi-tui";
9
9
  import { Text } from "@oh-my-pi/pi-tui";
10
10
  import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
11
11
  import * as z from "zod/v4";
12
- import { canonicalSnapshotKey, getFileSnapshotStore, recordFileSnapshot } from "../edit/file-snapshot-store";
12
+ import {
13
+ canonicalSnapshotKey,
14
+ getFileSnapshotStore,
15
+ recordFileSnapshot,
16
+ SNAPSHOT_MAX_BYTES,
17
+ } from "../edit/file-snapshot-store";
13
18
  import { normalizeToLF } from "../edit/normalize";
14
19
  import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
15
20
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -30,6 +35,7 @@ import {
30
35
  } from "../session/streaming-output";
31
36
  import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
32
37
  import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
38
+ import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context";
33
39
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
34
40
  import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
35
41
  import { convertFileWithMarkit } from "../utils/markit";
@@ -108,6 +114,15 @@ const PROSE_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]);
108
114
  // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
109
115
  const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
110
116
 
117
+ async function readBracketContextFullLines(absolutePath: string, fileSize: number): Promise<string[] | undefined> {
118
+ if (fileSize > SNAPSHOT_MAX_BYTES) return undefined;
119
+ try {
120
+ return normalizeToLF(await Bun.file(absolutePath).text()).split("\n");
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+
111
126
  function isRemoteMountPath(absolutePath: string): boolean {
112
127
  return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
113
128
  }
@@ -174,6 +189,21 @@ function formatTextWithMode(
174
189
  return text;
175
190
  }
176
191
 
192
+ const BRACKET_CONTEXT_ELLIPSIS = "…";
193
+
194
+ function formatLineEntryWithMode(entry: LineEntry, shouldAddHashLines: boolean, shouldAddLineNumbers: boolean): string {
195
+ if (entry.kind === "ellipsis") return BRACKET_CONTEXT_ELLIPSIS;
196
+ return formatSingleLine(entry.lineNumber, entry.text, shouldAddHashLines, shouldAddLineNumbers);
197
+ }
198
+
199
+ function formatLineEntriesWithMode(
200
+ entries: readonly LineEntry[],
201
+ shouldAddHashLines: boolean,
202
+ shouldAddLineNumbers: boolean,
203
+ ): string {
204
+ return entries.map(entry => formatLineEntryWithMode(entry, shouldAddHashLines, shouldAddLineNumbers)).join("\n");
205
+ }
206
+
177
207
  const BRACE_PAIRS: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
178
208
  const BRACE_TAIL_TRAILING_RE = /^[;,)\]}]*$/;
179
209
 
@@ -915,6 +945,21 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
915
945
  emittedHashlineHeader = true;
916
946
  return prependHashlineHeader(formatted, hashContext);
917
947
  };
948
+ const formatLineEntries = (entries: readonly LineEntry[], startNum: number): string => {
949
+ const firstLine = entries.find(entry => entry.kind === "line");
950
+ details.displayContent = {
951
+ text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
952
+ startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startNum,
953
+ };
954
+ const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
955
+ if (!hashContext || emittedHashlineHeader) return formatted;
956
+ emittedHashlineHeader = true;
957
+ return prependHashlineHeader(formatted, hashContext);
958
+ };
959
+ const buildLineEntries = (endLineDisplay: number): LineEntry[] =>
960
+ buildLineEntriesWithBlockContext(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }], {
961
+ path: options.sourcePath,
962
+ });
918
963
 
919
964
  let outputText: string;
920
965
  let truncationInfo:
@@ -946,7 +991,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
946
991
  options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
947
992
  };
948
993
  } else if (truncation.truncated) {
949
- outputText = formatText(truncation.content, startLineDisplay);
994
+ const outputLines = truncation.outputLines ?? countTextLines(truncation.content);
995
+ const endLineDisplay = startLineDisplay + Math.max(0, outputLines - 1);
996
+ outputText =
997
+ options.raw === true
998
+ ? formatText(truncation.content, startLineDisplay)
999
+ : formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay);
950
1000
  details.truncation = truncation;
951
1001
  truncationInfo = {
952
1002
  result: truncation,
@@ -956,10 +1006,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
956
1006
  const remaining = allLines.length - (startLine + userLimitedLines);
957
1007
  const nextOffset = startLine + userLimitedLines + 1;
958
1008
 
959
- outputText = formatText(selectedContent, startLineDisplay);
1009
+ outputText =
1010
+ options.raw === true
1011
+ ? formatText(selectedContent, startLineDisplay)
1012
+ : formatLineEntries(buildLineEntries(endLine), startLineDisplay);
960
1013
  outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
961
1014
  } else {
962
- outputText = formatText(truncation.content, startLineDisplay);
1015
+ outputText =
1016
+ options.raw === true
1017
+ ? formatText(truncation.content, startLineDisplay)
1018
+ : formatLineEntries(buildLineEntries(endLine), startLineDisplay);
963
1019
  }
964
1020
 
965
1021
  resultBuilder.text(outputText);
@@ -1011,21 +1067,37 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1011
1067
  if (options.sourceUrl) resultBuilder.sourceUrl(options.sourceUrl);
1012
1068
  if (options.sourceInternal) resultBuilder.sourceInternal(options.sourceInternal);
1013
1069
 
1014
- const parts: string[] = [];
1015
1070
  const outOfBounds: LineRange[] = [];
1071
+ const visibleSpans: Array<{ startLine: number; endLine: number }> = [];
1072
+ const rawParts: string[] = [];
1016
1073
  for (const range of ranges) {
1017
1074
  if (range.startLine > totalLines) {
1018
1075
  outOfBounds.push(range);
1019
1076
  continue;
1020
1077
  }
1021
1078
  const effectiveEnd = Math.min(range.endLine ?? totalLines, totalLines);
1022
- const sliced = allLines.slice(range.startLine - 1, effectiveEnd).join("\n");
1023
- const formatted = formatTextWithMode(sliced, range.startLine, shouldAddHashLines, shouldAddLineNumbers);
1024
- parts.push(hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted);
1025
- if (hashContext) emittedHashlineHeader = true;
1079
+ visibleSpans.push({ startLine: range.startLine, endLine: effectiveEnd });
1080
+ if (options.raw === true) {
1081
+ rawParts.push(allLines.slice(range.startLine - 1, effectiveEnd).join("\n"));
1082
+ }
1026
1083
  }
1027
1084
 
1028
- const outputText = parts.length > 0 ? parts.join("\n\n…\n\n") : "";
1085
+ let outputText = "";
1086
+ if (options.raw === true) {
1087
+ outputText = rawParts.length > 0 ? rawParts.join("\n\n…\n\n") : "";
1088
+ } else if (visibleSpans.length > 0) {
1089
+ const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath });
1090
+ const firstLine = entries.find(entry => entry.kind === "line");
1091
+ if (firstLine?.kind === "line") {
1092
+ details.displayContent = {
1093
+ text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
1094
+ startLine: firstLine.lineNumber,
1095
+ };
1096
+ }
1097
+ const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
1098
+ outputText = hashContext && !emittedHashlineHeader ? prependHashlineHeader(formatted, hashContext) : formatted;
1099
+ if (hashContext) emittedHashlineHeader = true;
1100
+ }
1029
1101
  const notices: string[] = [];
1030
1102
  for (const range of outOfBounds) {
1031
1103
  const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
@@ -1046,6 +1118,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1046
1118
  async #readLocalFileMultiRange(
1047
1119
  absolutePath: string,
1048
1120
  ranges: readonly LineRange[],
1121
+ fileSize: number,
1049
1122
  parsed: ParsedSelector,
1050
1123
  displayMode: { hashLines: boolean; lineNumbers: boolean },
1051
1124
  suffixResolution: { from: string; to: string } | undefined,
@@ -1053,6 +1126,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1053
1126
  ): Promise<{
1054
1127
  outputText: string;
1055
1128
  columnTruncated: number;
1129
+ displayContent?: { text: string; startLine: number };
1056
1130
  bridgeResult?: AgentToolResult<ReadToolDetails>;
1057
1131
  }> {
1058
1132
  const rawSelector = isRawSelector(parsed);
@@ -1085,7 +1159,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1085
1159
 
1086
1160
  const blocks: string[] = [];
1087
1161
  const notices: string[] = [];
1162
+ const visibleSpans: Array<{ startLine: number; endLine: number }> = [];
1163
+ const displayLineByNumber = new Map<number, string>();
1164
+ const fullLines = rawSelector ? undefined : await readBracketContextFullLines(absolutePath, fileSize);
1088
1165
  let columnTruncated = 0;
1166
+ let displayContent: { text: string; startLine: number } | undefined;
1089
1167
 
1090
1168
  for (const range of ranges) {
1091
1169
  const rangeStart = range.startLine - 1; // 0-indexed
@@ -1125,11 +1203,43 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1125
1203
  }
1126
1204
  if (cloned) displayLines = cloned;
1127
1205
  }
1128
- const blockText = displayLines.join("\n");
1129
- blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
1206
+ const endLine = range.startLine + Math.max(0, displayLines.length - 1);
1207
+ visibleSpans.push({ startLine: range.startLine, endLine });
1208
+ for (let i = 0; i < displayLines.length; i++) {
1209
+ displayLineByNumber.set(range.startLine + i, displayLines[i] ?? "");
1210
+ }
1211
+ if (!fullLines || rawSelector) {
1212
+ const blockText = displayLines.join("\n");
1213
+ blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
1214
+ }
1130
1215
  }
1131
1216
 
1132
- let outputText = blocks.join("\n\n…\n\n");
1217
+ let outputText: string;
1218
+ if (!rawSelector && fullLines && visibleSpans.length > 0) {
1219
+ const entries = buildLineEntriesWithBlockContext(
1220
+ fullLines,
1221
+ visibleSpans,
1222
+ { path: absolutePath },
1223
+ {
1224
+ lineText: (lineNumber, sourceText) => {
1225
+ const visibleText = displayLineByNumber.get(lineNumber);
1226
+ if (visibleText !== undefined) return visibleText;
1227
+ if (maxColumns <= 0) return sourceText;
1228
+ const truncated = truncateLine(sourceText, maxColumns);
1229
+ if (truncated.wasTruncated) columnTruncated = maxColumns;
1230
+ return truncated.text;
1231
+ },
1232
+ },
1233
+ );
1234
+ const firstLine = entries.find(entry => entry.kind === "line");
1235
+ displayContent = {
1236
+ text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
1237
+ startLine: firstLine?.kind === "line" ? firstLine.lineNumber : (visibleSpans[0]?.startLine ?? 1),
1238
+ };
1239
+ outputText = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
1240
+ } else {
1241
+ outputText = blocks.join("\n\n…\n\n");
1242
+ }
1133
1243
  if (shouldAddHashLines && outputText) {
1134
1244
  const tag = await recordFileSnapshot(this.session, absolutePath);
1135
1245
  if (tag) {
@@ -1139,7 +1249,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1139
1249
  if (notices.length > 0) {
1140
1250
  outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
1141
1251
  }
1142
- return { outputText, columnTruncated };
1252
+ return { outputText, columnTruncated, displayContent };
1143
1253
  }
1144
1254
 
1145
1255
  async #readArchiveDirectory(
@@ -1818,6 +1928,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1818
1928
  const multiResult = await this.#readLocalFileMultiRange(
1819
1929
  absolutePath,
1820
1930
  parsed.ranges,
1931
+ fileSize,
1821
1932
  parsed,
1822
1933
  displayMode,
1823
1934
  suffixResolution,
@@ -1826,7 +1937,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1826
1937
  if (multiResult.bridgeResult) return multiResult.bridgeResult;
1827
1938
  content = [{ type: "text", text: multiResult.outputText }];
1828
1939
  sourcePath = absolutePath;
1829
- details = {};
1940
+ details = multiResult.displayContent ? { displayContent: multiResult.displayContent } : {};
1830
1941
  if (multiResult.columnTruncated > 0) {
1831
1942
  columnTruncated = multiResult.columnTruncated;
1832
1943
  }
@@ -1930,6 +2041,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1930
2041
  if (cloned) displayLines = cloned;
1931
2042
  }
1932
2043
 
2044
+ const displayLineByNumber = new Map<number, string>();
2045
+ for (let i = 0; i < displayLines.length; i++) {
2046
+ displayLineByNumber.set(startLineDisplay + i, displayLines[i] ?? "");
2047
+ }
2048
+ const bracketContextFullLines = rawSelector
2049
+ ? undefined
2050
+ : await readBracketContextFullLines(absolutePath, fileSize);
2051
+ const displayedEndLine = startLineDisplay + Math.max(0, displayLines.length - 1);
2052
+
1933
2053
  const selectedContent = displayLines.join("\n");
1934
2054
  const userLimitedLines = collectedLines.length;
1935
2055
 
@@ -1979,6 +2099,33 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1979
2099
  emittedHashlineHeader = true;
1980
2100
  return prependHashlineHeader(formatted, hashContext);
1981
2101
  };
2102
+ const formatBracketAwareText = (): string | undefined => {
2103
+ if (!bracketContextFullLines) return undefined;
2104
+ const entries = buildLineEntriesWithBlockContext(
2105
+ bracketContextFullLines,
2106
+ [{ startLine: startLineDisplay, endLine: displayedEndLine }],
2107
+ { path: absolutePath },
2108
+ {
2109
+ lineText: (lineNumber, sourceText) => {
2110
+ const visibleText = displayLineByNumber.get(lineNumber);
2111
+ if (visibleText !== undefined) return visibleText;
2112
+ if (maxColumns <= 0) return sourceText;
2113
+ const truncated = truncateLine(sourceText, maxColumns);
2114
+ if (truncated.wasTruncated) columnTruncated = maxColumns;
2115
+ return truncated.text;
2116
+ },
2117
+ },
2118
+ );
2119
+ const firstLine = entries.find(entry => entry.kind === "line");
2120
+ capturedDisplayContent = {
2121
+ text: lineEntriesToPlainText(entries, BRACKET_CONTEXT_ELLIPSIS),
2122
+ startLine: firstLine?.kind === "line" ? firstLine.lineNumber : startLineDisplay,
2123
+ };
2124
+ const formatted = formatLineEntriesWithMode(entries, shouldAddHashLines, shouldAddLineNumbers);
2125
+ if (!hashContext || emittedHashlineHeader) return formatted;
2126
+ emittedHashlineHeader = true;
2127
+ return prependHashlineHeader(formatted, hashContext);
2128
+ };
1982
2129
 
1983
2130
  let outputText: string;
1984
2131
 
@@ -2005,7 +2152,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2005
2152
  options: { direction: "head", startLine: startLineDisplay, totalFileLines },
2006
2153
  };
2007
2154
  } else if (truncation.truncated) {
2008
- outputText = formatText(truncation.content, startLineDisplay);
2155
+ outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay);
2009
2156
  details = { truncation };
2010
2157
  sourcePath = absolutePath;
2011
2158
  truncationInfo = {
@@ -2016,13 +2163,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2016
2163
  const remaining = totalFileLines - (startLine + userLimitedLines);
2017
2164
  const nextOffset = startLine + userLimitedLines + 1;
2018
2165
 
2019
- outputText = formatText(truncation.content, startLineDisplay);
2166
+ outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay);
2020
2167
  outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
2021
2168
  details = {};
2022
2169
  sourcePath = absolutePath;
2023
2170
  } else {
2024
2171
  // No truncation, no user limit exceeded
2025
- outputText = formatText(truncation.content, startLineDisplay);
2172
+ outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay);
2026
2173
  details = {};
2027
2174
  sourcePath = absolutePath;
2028
2175
  }
@@ -133,6 +133,8 @@ export function formatStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFram
133
133
  switch (status) {
134
134
  case "success":
135
135
  return theme.styledSymbol("status.success", "success");
136
+ case "done":
137
+ return theme.styledSymbol("status.done", "success");
136
138
  case "error":
137
139
  return theme.styledSymbol("status.error", "error");
138
140
  case "warning":
@@ -276,7 +278,7 @@ export function formatCodeFrameLine(
276
278
  // Tool UI Helpers
277
279
  // =============================================================================
278
280
 
279
- export type ToolUIStatus = "success" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
281
+ export type ToolUIStatus = "success" | "done" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
280
282
  export type ToolUIColor = "success" | "error" | "warning" | "accent" | "muted";
281
283
 
282
284
  export interface ToolUITitleOptions {
@@ -241,7 +241,7 @@ export const resolveToolRenderer = {
241
241
  const isApply = action === "apply" && !result.isError;
242
242
  const isFailedApply = action === "apply" && result.isError;
243
243
  const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
244
- const icon = isApply ? uiTheme.status.success : uiTheme.status.error;
244
+ const icon = isApply ? uiTheme.styledSymbol("tool.resolve", "accent") : uiTheme.status.error;
245
245
  const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
246
246
  const separator = ": ";
247
247
  const separatorIndex = label.indexOf(separator);
@@ -169,7 +169,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
169
169
  }`;
170
170
 
171
171
  return new Text(
172
- `${theme.fg("success", theme.status.success)} ${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
172
+ `${theme.styledSymbol("tool.review", "accent")} ${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
173
173
  "dim",
174
174
  location,
175
175
  )}`,
package/src/tools/ssh.ts CHANGED
@@ -273,7 +273,10 @@ export const sshToolRenderer = {
273
273
  const details = result.details;
274
274
  const host = args?.host || "…";
275
275
  const command = args?.command ?? "";
276
- const header = renderStatusLine({ icon: "success", title: "SSH", description: `[${host}]` }, uiTheme);
276
+ const header = renderStatusLine(
277
+ { iconOverride: uiTheme.styledSymbol("tool.ssh", "accent"), title: "SSH", description: `[${host}]` },
278
+ uiTheme,
279
+ );
277
280
  const cmdLines = formatSshCommandLines(command, uiTheme);
278
281
  const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
279
282
  const outputBlock = new CachedOutputBlock();
package/src/tools/todo.ts CHANGED
@@ -881,7 +881,14 @@ export const todoToolRenderer = {
881
881
  keys.add(task.content);
882
882
  }
883
883
  const allTasks = phases.flatMap(phase => phase.tasks);
884
- const header = renderStatusLine({ icon: "success", title: "Todo", meta: [`${allTasks.length} tasks`] }, uiTheme);
884
+ const header = renderStatusLine(
885
+ {
886
+ iconOverride: uiTheme.styledSymbol("tool.todo", "accent"),
887
+ title: "Todo",
888
+ meta: [`${allTasks.length} tasks`],
889
+ },
890
+ uiTheme,
891
+ );
885
892
  if (allTasks.length === 0) {
886
893
  const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
887
894
  return new Text(`${header}\n ${uiTheme.fg("dim", fallback)}`, 0, 0);
@@ -9,7 +9,7 @@ export interface ToolTimeoutConfig {
9
9
 
10
10
  export const TOOL_TIMEOUTS = {
11
11
  bash: { default: 300, min: 1, max: 3600 },
12
- eval: { default: 30, min: 1, max: 600 },
12
+ eval: { default: 30, min: 1, max: 3600 },
13
13
  browser: { default: 30, min: 1, max: 300 },
14
14
  ssh: { default: 60, min: 1, max: 3600 },
15
15
  fetch: { default: 20, min: 1, max: 45 },
@@ -1081,7 +1081,7 @@ export const writeToolRenderer = {
1081
1081
  : "";
1082
1082
  const header = renderStatusLine(
1083
1083
  {
1084
- icon: "success",
1084
+ iconOverride: uiTheme.styledSymbol("tool.write", "accent"),
1085
1085
  title: "Write",
1086
1086
  description: `${langIcon} ${pathDisplay}${lineSuffix}${execSuffix}`,
1087
1087
  },
@@ -50,7 +50,7 @@ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string;
50
50
  if (status) {
51
51
  const icon = formatStatusIcon(
52
52
  status === "complete"
53
- ? "success"
53
+ ? "done"
54
54
  : status === "error"
55
55
  ? "error"
56
56
  : status === "warning"