@oh-my-pi/pi-coding-agent 15.10.3 → 15.10.5

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 (161) hide show
  1. package/CHANGELOG.md +72 -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/__tests__/js-context-manager.test.d.ts +1 -0
  10. package/dist/types/eval/backend.d.ts +7 -0
  11. package/dist/types/eval/bridge-timeout.d.ts +1 -1
  12. package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
  13. package/dist/types/eval/idle-timeout.d.ts +1 -1
  14. package/dist/types/eval/js/context-manager.d.ts +1 -0
  15. package/dist/types/eval/js/executor.d.ts +2 -0
  16. package/dist/types/eval/js/index.d.ts +1 -1
  17. package/dist/types/eval/js/shared/helpers.d.ts +6 -0
  18. package/dist/types/eval/js/shared/runtime.d.ts +5 -0
  19. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  20. package/dist/types/eval/py/executor.d.ts +7 -0
  21. package/dist/types/eval/py/index.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +8 -1
  24. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +10 -0
  26. package/dist/types/mcp/oauth-flow.d.ts +2 -2
  27. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  28. package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
  29. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/types.d.ts +31 -2
  31. package/dist/types/modes/image-references.d.ts +8 -3
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/theme/theme.d.ts +2 -1
  34. package/dist/types/modes/types.d.ts +2 -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/tools/ask.d.ts +1 -0
  38. package/dist/types/tools/browser/tab-worker.d.ts +15 -0
  39. package/dist/types/tools/index.d.ts +17 -0
  40. package/dist/types/tools/render-utils.d.ts +1 -1
  41. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  42. package/dist/types/utils/block-context.d.ts +35 -0
  43. package/dist/types/utils/image-loading.d.ts +12 -0
  44. package/package.json +29 -9
  45. package/src/capability/rule-buckets.ts +4 -2
  46. package/src/capability/rule.ts +10 -1
  47. package/src/cli/auth-broker-cli.ts +6 -7
  48. package/src/cli/auth-gateway-cli.ts +1 -1
  49. package/src/cli/list-models.ts +5 -0
  50. package/src/cli/update-cli.ts +138 -16
  51. package/src/config/model-registry.ts +81 -2
  52. package/src/debug/index.ts +4 -8
  53. package/src/discovery/at-imports.ts +273 -0
  54. package/src/discovery/builtin-rules/index.ts +4 -0
  55. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  56. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  57. package/src/discovery/helpers.ts +2 -1
  58. package/src/edit/diff.ts +114 -4
  59. package/src/edit/hashline/diff.ts +1 -1
  60. package/src/edit/hashline/execute.ts +1 -1
  61. package/src/edit/modes/patch.ts +6 -2
  62. package/src/edit/modes/replace.ts +1 -1
  63. package/src/edit/renderer.ts +12 -2
  64. package/src/eval/__tests__/agent-bridge.test.ts +13 -0
  65. package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
  66. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  67. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  68. package/src/eval/agent-bridge.ts +6 -1
  69. package/src/eval/backend.ts +15 -0
  70. package/src/eval/bridge-timeout.ts +1 -1
  71. package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
  72. package/src/eval/idle-timeout.ts +1 -1
  73. package/src/eval/js/context-manager.ts +70 -8
  74. package/src/eval/js/executor.ts +3 -0
  75. package/src/eval/js/index.ts +7 -1
  76. package/src/eval/js/shared/helpers.ts +53 -6
  77. package/src/eval/js/shared/prelude.txt +4 -4
  78. package/src/eval/js/shared/runtime.ts +8 -0
  79. package/src/eval/js/tool-bridge.ts +3 -3
  80. package/src/eval/js/worker-core.ts +1 -0
  81. package/src/eval/js/worker-entry.ts +6 -0
  82. package/src/eval/js/worker-protocol.ts +6 -0
  83. package/src/eval/py/executor.ts +12 -0
  84. package/src/eval/py/index.ts +7 -1
  85. package/src/eval/py/prelude.py +46 -7
  86. package/src/eval/py/runner.py +1 -0
  87. package/src/exa/render.ts +1 -1
  88. package/src/export/ttsr.ts +122 -1
  89. package/src/extensibility/extensions/types.ts +8 -1
  90. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  91. package/src/extensibility/plugins/doctor.ts +1 -1
  92. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
  93. package/src/goals/tools/goal-tool.ts +1 -1
  94. package/src/internal-urls/docs-index.generated.ts +8 -6
  95. package/src/internal-urls/local-protocol.ts +13 -0
  96. package/src/lsp/render.ts +8 -6
  97. package/src/mcp/oauth-flow.ts +3 -3
  98. package/src/mcp/render.ts +7 -1
  99. package/src/modes/components/custom-editor.ts +12 -6
  100. package/src/modes/components/login-dialog.ts +1 -1
  101. package/src/modes/components/oauth-selector.ts +4 -4
  102. package/src/modes/components/read-tool-group.ts +10 -3
  103. package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
  104. package/src/modes/components/status-line/index.ts +1 -0
  105. package/src/modes/components/status-line/types.ts +23 -8
  106. package/src/modes/components/tips.txt +1 -1
  107. package/src/modes/components/tool-execution.ts +1 -1
  108. package/src/modes/components/transcript-container.ts +17 -10
  109. package/src/modes/components/user-message.ts +6 -3
  110. package/src/modes/components/welcome.ts +1 -1
  111. package/src/modes/controllers/extension-ui-controller.ts +143 -127
  112. package/src/modes/controllers/input-controller.ts +36 -10
  113. package/src/modes/controllers/mcp-command-controller.ts +28 -12
  114. package/src/modes/controllers/selector-controller.ts +4 -11
  115. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  116. package/src/modes/image-references.ts +13 -7
  117. package/src/modes/interactive-mode.ts +2 -2
  118. package/src/modes/rpc/rpc-mode.ts +1 -1
  119. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
  120. package/src/modes/theme/theme.ts +95 -1
  121. package/src/modes/types.ts +2 -1
  122. package/src/modes/utils/ui-helpers.ts +14 -5
  123. package/src/prompts/system/tiny-title-system.md +1 -1
  124. package/src/prompts/system/title-system.md +16 -3
  125. package/src/prompts/system/workflow-notice.md +1 -1
  126. package/src/prompts/tools/bash.md +1 -1
  127. package/src/prompts/tools/eval.md +6 -6
  128. package/src/sdk.ts +31 -14
  129. package/src/session/agent-session.ts +213 -155
  130. package/src/session/session-manager.ts +1 -1
  131. package/src/slash-commands/builtin-registry.ts +1 -1
  132. package/src/system-prompt.ts +15 -9
  133. package/src/task/render.ts +20 -8
  134. package/src/tools/ask.ts +14 -5
  135. package/src/tools/bash-interactive.ts +1 -1
  136. package/src/tools/bash.ts +14 -2
  137. package/src/tools/browser/render.ts +5 -2
  138. package/src/tools/browser/tab-worker.ts +211 -91
  139. package/src/tools/debug.ts +5 -2
  140. package/src/tools/eval-render.ts +8 -5
  141. package/src/tools/eval.ts +2 -2
  142. package/src/tools/gh-renderer.ts +29 -15
  143. package/src/tools/index.ts +32 -0
  144. package/src/tools/inspect-image-renderer.ts +12 -5
  145. package/src/tools/job.ts +9 -6
  146. package/src/tools/memory-render.ts +19 -5
  147. package/src/tools/read.ts +165 -18
  148. package/src/tools/render-utils.ts +3 -1
  149. package/src/tools/resolve.ts +1 -1
  150. package/src/tools/review.ts +1 -1
  151. package/src/tools/ssh.ts +4 -1
  152. package/src/tools/todo.ts +8 -1
  153. package/src/tools/tool-timeouts.ts +1 -1
  154. package/src/tools/write.ts +1 -1
  155. package/src/tui/code-cell.ts +1 -1
  156. package/src/utils/block-context.ts +312 -0
  157. package/src/utils/image-loading.ts +31 -1
  158. package/src/utils/title-generator.ts +2 -2
  159. package/src/web/search/providers/codex.ts +1 -1
  160. package/src/web/search/render.ts +14 -6
  161. /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
@@ -163,8 +163,8 @@ function getJobStateVisual(
163
163
  ): { iconRaw: string; iconColor: ToolUIColor; textColor: ThemeColor } {
164
164
  if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion)) {
165
165
  return {
166
- iconRaw: theme.status.success,
167
- iconColor: "success",
166
+ iconRaw: theme.symbol("tool.gh"),
167
+ iconColor: "accent",
168
168
  textColor: "success",
169
169
  };
170
170
  }
@@ -327,14 +327,21 @@ function renderFallbackComponent(
327
327
  const title = formatOpTitle(args.op);
328
328
  const meta = buildOpMeta(args);
329
329
  const isError = result.isError === true;
330
- const status: ToolUIStatus = isError ? "error" : text ? "success" : "warning";
330
+ const success = !isError && Boolean(text);
331
331
  const header = renderStatusLine(
332
- {
333
- icon: status,
334
- title,
335
- titleColor: isError ? "error" : "accent",
336
- meta,
337
- },
332
+ success
333
+ ? {
334
+ iconOverride: theme.styledSymbol("tool.gh", "accent"),
335
+ title,
336
+ titleColor: "accent",
337
+ meta,
338
+ }
339
+ : {
340
+ icon: isError ? "error" : "warning",
341
+ title,
342
+ titleColor: isError ? "error" : "accent",
343
+ meta,
344
+ },
338
345
  theme,
339
346
  );
340
347
 
@@ -438,12 +445,19 @@ export const githubToolRenderer = {
438
445
  if (watch) {
439
446
  const isError = result.isError === true;
440
447
  const header = renderStatusLine(
441
- {
442
- icon: isError ? "error" : "success",
443
- title: "GitHub Run Watch",
444
- titleColor: isError ? "error" : "accent",
445
- meta: [getWatchHeader(watch)],
446
- },
448
+ isError
449
+ ? {
450
+ icon: "error",
451
+ title: "GitHub Run Watch",
452
+ titleColor: "error",
453
+ meta: [getWatchHeader(watch)],
454
+ }
455
+ : {
456
+ iconOverride: uiTheme.styledSymbol("tool.gh", "accent"),
457
+ title: "GitHub Run Watch",
458
+ titleColor: "accent",
459
+ meta: [getWatchHeader(watch)],
460
+ },
447
461
  uiTheme,
448
462
  );
449
463
  return framedBlock(uiTheme, width => {
@@ -342,6 +342,38 @@ export function computeEssentialBuiltinNames(settings: Settings): string[] {
342
342
  return [...DEFAULT_ESSENTIAL_TOOL_NAMES];
343
343
  }
344
344
 
345
+ /**
346
+ * Filter the initial active tool set when `tools.discoveryMode === "all"`.
347
+ *
348
+ * Non-essential discoverable built-ins are hidden — the model rediscovers them
349
+ * via `search_tool_bm25` and activates them on demand. A tool survives hiding
350
+ * when it is essential, explicitly requested, restored from a prior selection,
351
+ * or required by a forced tool_choice feature (`forceActive`). The last case is
352
+ * load-bearing: a named tool_choice (e.g. the eager `todo` prelude) must
353
+ * reference a tool present in the request, or the provider rejects it with 400.
354
+ */
355
+ export function filterInitialToolsForDiscoveryAll(
356
+ initialToolNames: string[],
357
+ opts: {
358
+ loadModeOf: (name: string) => BuiltinToolLoadMode | undefined;
359
+ essentialNames: ReadonlySet<string>;
360
+ explicitlyRequested: ReadonlySet<string>;
361
+ restored: ReadonlySet<string>;
362
+ forceActive: ReadonlySet<string>;
363
+ },
364
+ ): string[] {
365
+ return initialToolNames.filter(name => {
366
+ const loadMode = opts.loadModeOf(name);
367
+ if (!loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
368
+ if (loadMode === "essential") return true;
369
+ if (opts.essentialNames.has(name)) return true;
370
+ if (opts.explicitlyRequested.has(name)) return true;
371
+ if (opts.restored.has(name)) return true;
372
+ if (opts.forceActive.has(name)) return true;
373
+ return false;
374
+ });
375
+ }
376
+
345
377
  /**
346
378
  * Public callable factory map. External callers may invoke `BUILTIN_TOOLS.read(session)` or
347
379
  * `BUILTIN_TOOLS[name](session)` to construct a tool directly.
@@ -53,12 +53,19 @@ export const inspectImageToolRenderer = {
53
53
  const details = result.details;
54
54
  const rawPath = details?.imagePath ?? args?.path ?? "";
55
55
  const pathDisplay = rawPath ? shortenPath(rawPath) : "image";
56
+ const success = !result.isError;
56
57
  const header = renderStatusLine(
57
- {
58
- icon: result.isError ? "error" : "success",
59
- title: "Inspect",
60
- description: pathDisplay,
61
- },
58
+ success
59
+ ? {
60
+ iconOverride: uiTheme.styledSymbol("tool.inspectImage", "accent"),
61
+ title: "Inspect",
62
+ description: pathDisplay,
63
+ }
64
+ : {
65
+ icon: "error",
66
+ title: "Inspect",
67
+ description: pathDisplay,
68
+ },
62
69
  uiTheme,
63
70
  );
64
71
 
package/src/tools/job.ts CHANGED
@@ -355,7 +355,7 @@ const PREVIEW_LINE_WIDTH = 80;
355
355
  function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
356
356
  switch (status) {
357
357
  case "completed":
358
- return "success";
358
+ return "done";
359
359
  case "failed":
360
360
  return "error";
361
361
  case "cancelled":
@@ -468,11 +468,14 @@ export const jobToolRenderer = {
468
468
  itemType: "job",
469
469
  renderItem: job => {
470
470
  const lines: string[] = [];
471
- const icon = formatStatusIcon(
472
- statusToIcon(job.status),
473
- uiTheme,
474
- job.status === "running" ? options.spinnerFrame : undefined,
475
- );
471
+ const icon =
472
+ job.status === "completed"
473
+ ? uiTheme.styledSymbol("tool.job", "accent")
474
+ : formatStatusIcon(
475
+ statusToIcon(job.status),
476
+ uiTheme,
477
+ job.status === "running" ? options.spinnerFrame : undefined,
478
+ );
476
479
  const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
477
480
  const idText = uiTheme.fg("muted", job.id);
478
481
  const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
@@ -49,10 +49,11 @@ function queryHeader(
49
49
  icon: ToolUIStatus,
50
50
  theme: Theme,
51
51
  meta?: string[],
52
+ iconOverride?: string,
52
53
  ): string {
53
54
  const trimmed = replaceTabs((query ?? "").trim());
54
55
  const description = trimmed ? truncateToWidth(trimmed, 80, Ellipsis.Unicode) : undefined;
55
- return renderStatusLine({ icon, title, description, meta }, theme);
56
+ return renderStatusLine({ icon, iconOverride, title, description, meta }, theme);
56
57
  }
57
58
 
58
59
  function retainComponent(contents: string[], header: string, getExpanded: () => boolean, theme: Theme): Component {
@@ -96,7 +97,11 @@ export const retainToolRenderer = {
96
97
  // trailing period so it reads cleanly as a status meta segment.
97
98
  const summary = resultText(result).replace(/\.$/, "");
98
99
  const header = renderStatusLine(
99
- { icon: "success", title: "Retain", meta: summary ? [summary] : undefined },
100
+ {
101
+ iconOverride: theme.styledSymbol("tool.memory", "accent"),
102
+ title: "Retain",
103
+ meta: summary ? [summary] : undefined,
104
+ },
100
105
  theme,
101
106
  );
102
107
  return retainComponent(contents, header, () => options.expanded, theme);
@@ -121,9 +126,11 @@ export const recallToolRenderer = {
121
126
  const text = resultText(result);
122
127
  const match = text.match(/^Found (\d+) relevant/);
123
128
  const found = match ? Number(match[1]) : 0;
124
- const icon: ToolUIStatus = found > 0 ? "success" : "warning";
125
129
  const meta = [found > 0 ? `${found} found` : "no matches"];
126
- const header = queryHeader("Recall", args?.query, icon, theme, meta);
130
+ const header =
131
+ found > 0
132
+ ? queryHeader("Recall", args?.query, "success", theme, meta, theme.styledSymbol("tool.memory", "accent"))
133
+ : queryHeader("Recall", args?.query, "warning", theme, meta);
127
134
  if (found === 0) {
128
135
  return new Text(header, 0, 0);
129
136
  }
@@ -163,7 +170,14 @@ export const reflectToolRenderer = {
163
170
  if (result.isError) {
164
171
  return new Text(formatErrorMessage(resultText(result) || "Reflect failed", theme), 0, 0);
165
172
  }
166
- const header = queryHeader("Reflect", args?.query, "success", theme);
173
+ const header = queryHeader(
174
+ "Reflect",
175
+ args?.query,
176
+ "success",
177
+ theme,
178
+ undefined,
179
+ theme.styledSymbol("tool.memory", "accent"),
180
+ );
167
181
  const answer = resultText(result);
168
182
  const answerLines = answer.split("\n").filter(line => line.trim().length > 0);
169
183
  return createCachedComponent(
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"