@oh-my-pi/pi-coding-agent 16.0.4 → 16.0.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 (270) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli.js +2027 -1396
  3. package/dist/types/advisor/advise-tool.d.ts +31 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/bench-cli.d.ts +6 -0
  10. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  11. package/dist/types/commands/launch.d.ts +3 -0
  12. package/dist/types/commands/ttsr.d.ts +57 -0
  13. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  15. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  16. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  17. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  18. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  19. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  20. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  21. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  22. package/dist/types/commit/changelog/generate.d.ts +12 -13
  23. package/dist/types/commit/shared-llm.d.ts +10 -37
  24. package/dist/types/config/config-file.d.ts +4 -4
  25. package/dist/types/config/keybindings.d.ts +5 -0
  26. package/dist/types/config/models-config-schema.d.ts +625 -990
  27. package/dist/types/config/models-config.d.ts +229 -217
  28. package/dist/types/config/settings-schema.d.ts +144 -25
  29. package/dist/types/edit/hashline/params.d.ts +7 -11
  30. package/dist/types/edit/index.d.ts +2 -1
  31. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  32. package/dist/types/edit/modes/patch.d.ts +15 -24
  33. package/dist/types/edit/modes/replace.d.ts +16 -17
  34. package/dist/types/eval/js/index.d.ts +1 -0
  35. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  36. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  37. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  38. package/dist/types/extensibility/extensions/types.d.ts +14 -10
  39. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  40. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  41. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  42. package/dist/types/extensibility/shared-events.d.ts +22 -1
  43. package/dist/types/extensibility/typebox.d.ts +80 -58
  44. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  45. package/dist/types/index.d.ts +2 -0
  46. package/dist/types/lsp/index.d.ts +11 -26
  47. package/dist/types/lsp/types.d.ts +12 -28
  48. package/dist/types/main.d.ts +1 -0
  49. package/dist/types/mcp/client.d.ts +8 -0
  50. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  51. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  52. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  53. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
  54. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  55. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  58. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  59. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  60. package/dist/types/modes/theme/theme.d.ts +1 -1
  61. package/dist/types/modes/types.d.ts +3 -0
  62. package/dist/types/modes/utils/context-usage.d.ts +12 -0
  63. package/dist/types/sdk.d.ts +8 -1
  64. package/dist/types/session/agent-session.d.ts +24 -0
  65. package/dist/types/session/session-persistence.d.ts +4 -0
  66. package/dist/types/startup-splash.d.ts +12 -0
  67. package/dist/types/task/types.d.ts +47 -48
  68. package/dist/types/tools/ask.d.ts +26 -27
  69. package/dist/types/tools/ast-edit.d.ts +17 -17
  70. package/dist/types/tools/ast-grep.d.ts +12 -13
  71. package/dist/types/tools/bash.d.ts +20 -17
  72. package/dist/types/tools/browser.d.ts +46 -71
  73. package/dist/types/tools/checkpoint.d.ts +14 -15
  74. package/dist/types/tools/debug.d.ts +82 -145
  75. package/dist/types/tools/eval.d.ts +30 -40
  76. package/dist/types/tools/find.d.ts +17 -18
  77. package/dist/types/tools/gh.d.ts +49 -78
  78. package/dist/types/tools/image-gen.d.ts +20 -36
  79. package/dist/types/tools/inspect-image.d.ts +10 -11
  80. package/dist/types/tools/irc.d.ts +22 -33
  81. package/dist/types/tools/job.d.ts +11 -12
  82. package/dist/types/tools/learn.d.ts +21 -28
  83. package/dist/types/tools/manage-skill.d.ts +13 -22
  84. package/dist/types/tools/memory-edit.d.ts +15 -24
  85. package/dist/types/tools/memory-recall.d.ts +7 -8
  86. package/dist/types/tools/memory-reflect.d.ts +9 -10
  87. package/dist/types/tools/memory-retain.d.ts +13 -14
  88. package/dist/types/tools/read.d.ts +8 -8
  89. package/dist/types/tools/resolve.d.ts +11 -18
  90. package/dist/types/tools/review.d.ts +9 -15
  91. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  92. package/dist/types/tools/search.d.ts +16 -17
  93. package/dist/types/tools/ssh.d.ts +14 -15
  94. package/dist/types/tools/todo.d.ts +27 -43
  95. package/dist/types/tools/tts.d.ts +8 -9
  96. package/dist/types/tools/write.d.ts +9 -10
  97. package/dist/types/tui/code-cell.d.ts +2 -0
  98. package/dist/types/tui/index.d.ts +1 -0
  99. package/dist/types/tui/width-aware-text.d.ts +23 -0
  100. package/dist/types/utils/image-vision-fallback.d.ts +28 -0
  101. package/dist/types/utils/markit.d.ts +10 -1
  102. package/dist/types/web/search/index.d.ts +17 -28
  103. package/dist/types/web/search/providers/base.d.ts +1 -0
  104. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  105. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  106. package/dist/types/web/search/types.d.ts +32 -26
  107. package/package.json +14 -13
  108. package/scripts/omp +1 -1
  109. package/src/advisor/__tests__/advisor.test.ts +103 -1
  110. package/src/advisor/advise-tool.ts +47 -11
  111. package/src/autoresearch/tools/init-experiment.ts +13 -16
  112. package/src/autoresearch/tools/log-experiment.ts +15 -18
  113. package/src/autoresearch/tools/run-experiment.ts +3 -3
  114. package/src/autoresearch/tools/update-notes.ts +4 -4
  115. package/src/cli/args.ts +1 -0
  116. package/src/cli/bench-cli.ts +30 -7
  117. package/src/cli/flag-tables.ts +8 -0
  118. package/src/cli/ttsr-cli.ts +995 -0
  119. package/src/cli-commands.ts +1 -0
  120. package/src/cli.ts +7 -1
  121. package/src/collab/host.ts +2 -2
  122. package/src/commands/launch.ts +3 -0
  123. package/src/commands/ttsr.ts +125 -0
  124. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  125. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  126. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  127. package/src/commit/agentic/tools/git-overview.ts +4 -4
  128. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  129. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  130. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  131. package/src/commit/agentic/tools/schemas.ts +8 -20
  132. package/src/commit/agentic/tools/split-commit.ts +19 -23
  133. package/src/commit/analysis/summary.ts +7 -5
  134. package/src/commit/changelog/generate.ts +15 -11
  135. package/src/commit/shared-llm.ts +17 -24
  136. package/src/config/config-file.ts +13 -15
  137. package/src/config/keybindings.ts +6 -0
  138. package/src/config/models-config-schema.ts +206 -179
  139. package/src/config/settings-schema.ts +118 -2
  140. package/src/discovery/builtin-rules/index.ts +2 -0
  141. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  142. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  143. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  144. package/src/edit/hashline/params.ts +12 -11
  145. package/src/edit/index.ts +5 -4
  146. package/src/edit/modes/apply-patch.ts +4 -4
  147. package/src/edit/modes/patch.ts +15 -18
  148. package/src/edit/modes/replace.ts +13 -17
  149. package/src/edit/renderer.ts +0 -1
  150. package/src/eval/agent-bridge.ts +11 -13
  151. package/src/eval/completion-bridge.ts +25 -17
  152. package/src/eval/js/context-manager.ts +17 -2
  153. package/src/eval/js/index.ts +1 -1
  154. package/src/eval/py/executor.ts +2 -2
  155. package/src/eval/py/runner.py +44 -0
  156. package/src/extensibility/custom-commands/loader.ts +5 -3
  157. package/src/extensibility/custom-commands/types.ts +6 -3
  158. package/src/extensibility/custom-tools/loader.ts +4 -2
  159. package/src/extensibility/custom-tools/types.ts +8 -5
  160. package/src/extensibility/extensions/loader.ts +4 -2
  161. package/src/extensibility/extensions/runner.ts +20 -2
  162. package/src/extensibility/extensions/types.ts +22 -8
  163. package/src/extensibility/hooks/loader.ts +5 -2
  164. package/src/extensibility/hooks/types.ts +7 -4
  165. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  166. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  167. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  168. package/src/extensibility/shared-events.ts +24 -0
  169. package/src/extensibility/tool-proxy.ts +4 -1
  170. package/src/extensibility/typebox.ts +778 -251
  171. package/src/goals/guided-setup.ts +12 -3
  172. package/src/goals/tools/goal-tool.ts +6 -6
  173. package/src/index.ts +2 -0
  174. package/src/internal-urls/docs-index.generated.ts +15 -13
  175. package/src/lsp/types.ts +13 -27
  176. package/src/main.ts +29 -21
  177. package/src/mcp/client.ts +38 -13
  178. package/src/mcp/render.ts +102 -89
  179. package/src/modes/components/agent-hub.ts +11 -4
  180. package/src/modes/components/branch-summary-message.ts +1 -0
  181. package/src/modes/components/btw-panel.ts +5 -1
  182. package/src/modes/components/collab-prompt-message.ts +9 -7
  183. package/src/modes/components/compaction-summary-message.ts +1 -0
  184. package/src/modes/components/custom-editor.ts +18 -0
  185. package/src/modes/components/custom-message.ts +1 -0
  186. package/src/modes/components/footer.ts +6 -5
  187. package/src/modes/components/hook-message.ts +1 -0
  188. package/src/modes/components/read-tool-group.ts +9 -3
  189. package/src/modes/components/skill-message.ts +1 -0
  190. package/src/modes/components/status-line/component.ts +139 -15
  191. package/src/modes/components/status-line/context-thresholds.ts +0 -1
  192. package/src/modes/components/todo-reminder.ts +1 -0
  193. package/src/modes/components/tool-execution.ts +17 -10
  194. package/src/modes/components/ttsr-notification.ts +1 -0
  195. package/src/modes/components/user-message.ts +6 -6
  196. package/src/modes/controllers/btw-controller.ts +69 -1
  197. package/src/modes/controllers/event-controller.ts +2 -7
  198. package/src/modes/controllers/input-controller.ts +29 -0
  199. package/src/modes/controllers/selector-controller.ts +10 -3
  200. package/src/modes/interactive-mode.ts +42 -10
  201. package/src/modes/rpc/rpc-types.ts +1 -1
  202. package/src/modes/setup-wizard/index.ts +1 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  204. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  205. package/src/modes/theme/theme.ts +133 -143
  206. package/src/modes/types.ts +3 -0
  207. package/src/modes/utils/context-usage.ts +37 -20
  208. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  209. package/src/prompts/system/system-prompt.md +1 -0
  210. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  211. package/src/prompts/tools/image-attachment-describe.md +10 -0
  212. package/src/sdk.ts +35 -22
  213. package/src/session/agent-session.ts +715 -255
  214. package/src/session/session-history-format.ts +11 -2
  215. package/src/session/session-loader.ts +19 -32
  216. package/src/session/session-persistence.ts +27 -11
  217. package/src/session/snapcompact-inline.ts +1 -1
  218. package/src/slash-commands/builtin-registry.ts +4 -11
  219. package/src/ssh/connection-manager.ts +3 -2
  220. package/src/startup-splash.ts +19 -0
  221. package/src/task/executor.ts +12 -7
  222. package/src/task/types.ts +44 -41
  223. package/src/tool-discovery/tool-index.ts +17 -4
  224. package/src/tools/ask.ts +14 -14
  225. package/src/tools/ast-edit.ts +17 -14
  226. package/src/tools/ast-grep.ts +10 -9
  227. package/src/tools/bash.ts +15 -10
  228. package/src/tools/browser/launch.ts +13 -0
  229. package/src/tools/browser.ts +26 -32
  230. package/src/tools/checkpoint.ts +7 -7
  231. package/src/tools/debug.ts +72 -69
  232. package/src/tools/eval.ts +18 -19
  233. package/src/tools/find.ts +20 -13
  234. package/src/tools/gh.ts +29 -49
  235. package/src/tools/image-gen.ts +94 -57
  236. package/src/tools/inspect-image.ts +8 -9
  237. package/src/tools/irc.ts +12 -12
  238. package/src/tools/job.ts +6 -6
  239. package/src/tools/learn.ts +11 -14
  240. package/src/tools/manage-skill.ts +19 -23
  241. package/src/tools/memory-edit.ts +8 -8
  242. package/src/tools/memory-recall.ts +4 -4
  243. package/src/tools/memory-reflect.ts +5 -5
  244. package/src/tools/memory-retain.ts +9 -11
  245. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  246. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  247. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  248. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  249. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  250. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  251. package/src/tools/read.ts +197 -19
  252. package/src/tools/report-tool-issue.ts +6 -6
  253. package/src/tools/resolve.ts +6 -6
  254. package/src/tools/review.ts +10 -12
  255. package/src/tools/search-tool-bm25.ts +5 -5
  256. package/src/tools/search.ts +20 -29
  257. package/src/tools/ssh.ts +8 -8
  258. package/src/tools/todo.ts +16 -19
  259. package/src/tools/tts.ts +16 -15
  260. package/src/tools/write.ts +5 -5
  261. package/src/tui/code-cell.ts +44 -3
  262. package/src/tui/index.ts +1 -0
  263. package/src/tui/width-aware-text.ts +58 -0
  264. package/src/utils/image-vision-fallback.ts +197 -0
  265. package/src/utils/markit.ts +17 -2
  266. package/src/web/search/index.ts +21 -9
  267. package/src/web/search/providers/base.ts +1 -0
  268. package/src/web/search/providers/gemini.ts +56 -18
  269. package/src/web/search/providers/perplexity.ts +373 -126
  270. package/src/web/search/types.ts +28 -48
@@ -4,6 +4,7 @@ import { stripVTControlCharacters } from "node:util";
4
4
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
5
  import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
6
6
  import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
7
+ import { settings } from "../../config/settings";
7
8
  import { theme } from "../../modes/theme/theme";
8
9
  import type { AgentSession } from "../../session/agent-session";
9
10
  import { shortenPath } from "../../tools/render-utils";
@@ -58,6 +59,8 @@ export class FooterComponent implements Component {
58
59
  this.#gitWatcher = null;
59
60
  }
60
61
 
62
+ if (!settings.get("git.enabled")) return;
63
+
61
64
  void git.head
62
65
  .resolve(getProjectDir())
63
66
  .then(head => {
@@ -102,6 +105,7 @@ export class FooterComponent implements Component {
102
105
  * Returns null if not in a git repo, branch name otherwise.
103
106
  */
104
107
  #getCurrentBranch(): string | null {
108
+ if (!settings.get("git.enabled")) return null;
105
109
  if (this.#cachedBranch !== undefined) {
106
110
  return this.#cachedBranch;
107
111
  }
@@ -182,11 +186,8 @@ export class FooterComponent implements Component {
182
186
  // Colorize context percentage based on usage
183
187
  let contextPercentStr: string;
184
188
  const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
185
- const contextPercentDisplay = `${formatContextUsage(
186
- contextUsage?.percent === null ? null : contextPercentValue,
187
- contextWindow,
188
- )}${autoIndicator}`;
189
- if (contextUsage?.percent !== null && contextUsage?.percent !== undefined) {
189
+ const contextPercentDisplay = `${formatContextUsage(contextPercentValue, contextWindow)}${autoIndicator}`;
190
+ if (contextUsage) {
190
191
  const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
191
192
  contextPercentStr =
192
193
  color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
@@ -25,6 +25,7 @@ export class HookMessageComponent extends Container {
25
25
 
26
26
  // Create box with purple background (used for default rendering)
27
27
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
28
+ this.#box.setIgnoreTight(true);
28
29
 
29
30
  this.#rebuild();
30
31
  }
@@ -57,6 +57,7 @@ type ReadToolResultDetails = {
57
57
  displayContent?: {
58
58
  text?: string;
59
59
  startLine?: number;
60
+ lineNumbers?: Array<number | null>;
60
61
  };
61
62
  meta?: {
62
63
  source?: {
@@ -86,6 +87,8 @@ type ReadEntry = {
86
87
  correctedFrom?: string;
87
88
  contentText?: string;
88
89
  conflictCount?: number;
90
+ codeStartLine?: number;
91
+ codeLineNumbers?: Array<number | null>;
89
92
  };
90
93
 
91
94
  /** Number of code lines to show in collapsed preview mode */
@@ -379,11 +382,12 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
379
382
  entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
380
383
  // Store clean display content for preview/expanded display when the read
381
384
  // tool provides it; fall back to model-facing text for legacy results.
382
- const displayContent =
383
- typeof details?.displayContent?.text === "string" ? details.displayContent.text : undefined;
385
+ const displayContent = details?.displayContent;
384
386
  const textContent = result.content?.find(c => c.type === "text")?.text;
385
387
  if (displayContent !== undefined || textContent !== undefined) {
386
- entry.contentText = displayContent ?? textContent;
388
+ entry.contentText = displayContent?.text ?? textContent;
389
+ entry.codeStartLine = displayContent?.startLine;
390
+ entry.codeLineNumbers = displayContent?.lineNumbers;
387
391
  }
388
392
  this.#updateDisplay();
389
393
  }
@@ -636,6 +640,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
636
640
  status: entry.status === "success" ? "complete" : entry.status,
637
641
  expanded,
638
642
  codeMaxLines: expanded ? undefined : COLLAPSED_PREVIEW_LINES,
643
+ codeStartLine: entry.codeStartLine,
644
+ codeLineNumbers: entry.codeLineNumbers,
639
645
  width,
640
646
  },
641
647
  theme,
@@ -13,6 +13,7 @@ export class SkillMessageComponent extends Container {
13
13
  super();
14
14
 
15
15
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
16
+ this.#box.setIgnoreTight(true);
16
17
  this.#rebuild();
17
18
  }
18
19
 
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
4
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
5
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
5
6
  import { getProjectDir } from "@oh-my-pi/pi-utils";
6
7
  import { $ } from "bun";
@@ -55,21 +56,70 @@ function messageFingerprint(msg: AgentMessage): string {
55
56
  }
56
57
  }
57
58
  } else if (role === "assistant") {
59
+ const assistantMsg = msg as AssistantMessage;
60
+ const usageExt = assistantMsg.usage as unknown as { promptTokensDetails?: unknown };
61
+ const usageTotal = assistantMsg.usage?.totalTokens ?? 0;
62
+ const promptBuckets = usageExt?.promptTokensDetails ? 1 : 0;
63
+ const stopReason = assistantMsg.stopReason ?? "";
64
+
65
+ let signatureLen = 0;
66
+ let redactedLen = 0;
67
+ const msgExt = assistantMsg as unknown as {
68
+ thinkingSignature?: string;
69
+ textSignature?: string;
70
+ thoughtSignature?: string;
71
+ redactedThinking?: { data?: string };
72
+ };
73
+ const thinkingSignature = msgExt.thinkingSignature;
74
+ if (typeof thinkingSignature === "string") {
75
+ signatureLen += thinkingSignature.length;
76
+ }
77
+ const textSignature = msgExt.textSignature;
78
+ if (typeof textSignature === "string") {
79
+ signatureLen += textSignature.length;
80
+ }
81
+ const thoughtSignature = msgExt.thoughtSignature;
82
+ if (typeof thoughtSignature === "string") {
83
+ signatureLen += thoughtSignature.length;
84
+ }
85
+ const redactedData = msgExt.redactedThinking?.data;
86
+ if (typeof redactedData === "string") {
87
+ redactedLen += redactedData.length;
88
+ }
89
+
58
90
  const content = (msg as { content?: unknown }).content;
59
91
  if (Array.isArray(content)) {
60
92
  blocks = content.length;
61
93
  for (const block of content) {
62
94
  if (!block || typeof block !== "object") continue;
63
- const b = block as { type?: string; text?: string; thinking?: string; name?: string; arguments?: unknown };
95
+ const b = block as {
96
+ type?: string;
97
+ text?: string;
98
+ thinking?: string;
99
+ thinkingSignature?: string;
100
+ signature?: string;
101
+ textSignature?: string;
102
+ thoughtSignature?: string;
103
+ data?: string;
104
+ name?: string;
105
+ arguments?: unknown;
106
+ };
64
107
  if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
65
- else if (b.type === "thinking" && typeof b.thinking === "string") textLen += b.thinking.length;
66
- else if (b.type === "toolCall") {
108
+ else if (b.type === "thinking") {
109
+ if (typeof b.thinking === "string") textLen += b.thinking.length;
110
+ if (typeof b.thinkingSignature === "string") signatureLen += b.thinkingSignature.length;
111
+ if (typeof b.signature === "string") signatureLen += b.signature.length;
112
+ if (typeof b.textSignature === "string") signatureLen += b.textSignature.length;
113
+ if (typeof b.thoughtSignature === "string") signatureLen += b.thoughtSignature.length;
114
+ } else if (b.type === "redactedThinking" && typeof b.data === "string") {
115
+ redactedLen += b.data.length;
116
+ } else if (b.type === "toolCall") {
67
117
  if (typeof b.name === "string") textLen += b.name.length;
68
- // Argument bytes vary; a length proxy is enough to detect in-place edits.
69
118
  textLen += b.arguments === undefined ? 0 : JSON.stringify(b.arguments).length;
70
119
  }
71
120
  }
72
121
  }
122
+ return `${role}:${ts}:${textLen}:${blocks}:${images}:${signatureLen}:${redactedLen}:${usageTotal}:${promptBuckets}:${stopReason}`;
73
123
  } else if (role === "toolResult" || role === "hookMessage") {
74
124
  const content = (msg as { content?: unknown }).content;
75
125
  if (typeof content === "string") {
@@ -95,8 +145,12 @@ interface ContextUsageMemo {
95
145
  length: number;
96
146
  lastFingerprint: string | undefined;
97
147
  modelContextWindow: number;
98
- usedTokens: number | null;
148
+ contextUsageRevision: number;
149
+ usedTokens: number;
99
150
  contextWindow: number;
151
+ systemPromptRef: readonly string[] | undefined;
152
+ toolsRef: readonly any[] | undefined;
153
+ skillsRef: readonly any[] | undefined;
100
154
  }
101
155
 
102
156
  const EMPTY_MESSAGES: readonly AgentMessage[] = [];
@@ -104,6 +158,16 @@ const EMPTY_MESSAGES: readonly AgentMessage[] = [];
104
158
  function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
105
159
  return segments.includes("context_pct") || segments.includes("context_total");
106
160
  }
161
+ function hasGitSegment(segments: readonly StatusLineSegmentId[]): boolean {
162
+ return segments.includes("git");
163
+ }
164
+
165
+ function hasPrSegment(segments: readonly StatusLineSegmentId[]): boolean {
166
+ return segments.includes("pr");
167
+ }
168
+ function hasGitBackedSegment(segments: readonly StatusLineSegmentId[]): boolean {
169
+ return hasGitSegment(segments) || hasPrSegment(segments);
170
+ }
107
171
 
108
172
  // ═══════════════════════════════════════════════════════════════════════════
109
173
  // StatusLineComponent
@@ -117,6 +181,7 @@ export class StatusLineComponent implements Component {
117
181
  #cachedBranchCwd: string | undefined = undefined;
118
182
  #gitWatcher: fs.FSWatcher | null = null;
119
183
  #onBranchChange: (() => void) | null = null;
184
+ #disposed = false;
120
185
  #autoCompactEnabled: boolean = true;
121
186
  #hookStatuses: Map<string, string> = new Map();
122
187
  #subagentCount: number = 0;
@@ -166,6 +231,15 @@ export class StatusLineComponent implements Component {
166
231
  transparent: settings.get("statusLine.transparent"),
167
232
  };
168
233
  }
234
+ #gitEnabled(): boolean {
235
+ return settings.get("git.enabled");
236
+ }
237
+ #hasGitBackedSegment(): boolean {
238
+ const effectiveSettings = this.#resolveSettings();
239
+ return (
240
+ hasGitBackedSegment(effectiveSettings.leftSegments) || hasGitBackedSegment(effectiveSettings.rightSegments)
241
+ );
242
+ }
169
243
 
170
244
  /**
171
245
  * Re-point the status line at another session (focus proxy). Invalidate: model/context/usage all derive
@@ -183,6 +257,7 @@ export class StatusLineComponent implements Component {
183
257
  updateSettings(settings: StatusLineSettings): void {
184
258
  this.#settings = settings;
185
259
  this.#effectiveSettings = undefined;
260
+ if (this.#onBranchChange) this.#setupGitWatcher();
186
261
  }
187
262
 
188
263
  getEffectiveSettingsForTest(): EffectiveStatusLineSettings {
@@ -241,6 +316,11 @@ export class StatusLineComponent implements Component {
241
316
  this.#gitWatcher = null;
242
317
  }
243
318
 
319
+ if (!this.#gitEnabled() || !this.#hasGitBackedSegment()) {
320
+ this.#invalidateGitCaches();
321
+ return;
322
+ }
323
+
244
324
  const repository = git.repo.resolveSync(getProjectDir());
245
325
  if (!repository) return;
246
326
 
@@ -250,6 +330,7 @@ export class StatusLineComponent implements Component {
250
330
 
251
331
  try {
252
332
  this.#gitWatcher = fs.watch(watchPath, () => {
333
+ if (this.#disposed) return;
253
334
  this.#invalidateGitCaches();
254
335
  if (this.#onBranchChange) {
255
336
  this.#onBranchChange();
@@ -261,6 +342,8 @@ export class StatusLineComponent implements Component {
261
342
  }
262
343
 
263
344
  dispose(): void {
345
+ this.#disposed = true;
346
+ this.#onBranchChange = null;
264
347
  if (this.#gitWatcher) {
265
348
  this.#gitWatcher.close();
266
349
  this.#gitWatcher = null;
@@ -286,6 +369,8 @@ export class StatusLineComponent implements Component {
286
369
  this.#cachedPrContext = undefined;
287
370
  }
288
371
  #getCurrentBranch(): string | null {
372
+ if (!this.#gitEnabled()) return null;
373
+
289
374
  const cwd = getProjectDir();
290
375
  if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === cwd) {
291
376
  return this.#cachedBranch;
@@ -310,6 +395,7 @@ export class StatusLineComponent implements Component {
310
395
  this.#defaultBranch = "main";
311
396
  (async () => {
312
397
  const resolved = await git.branch.default(getProjectDir());
398
+ if (this.#disposed) return;
313
399
  if (resolved) {
314
400
  this.#defaultBranch = resolved;
315
401
  if (this.#onBranchChange) {
@@ -322,6 +408,7 @@ export class StatusLineComponent implements Component {
322
408
  }
323
409
 
324
410
  #getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
411
+ if (!this.#gitEnabled()) return null;
325
412
  if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
326
413
  return this.#cachedGitStatus;
327
414
  }
@@ -343,6 +430,8 @@ export class StatusLineComponent implements Component {
343
430
  }
344
431
 
345
432
  #lookupPr(): { number: number; url: string } | null {
433
+ if (!this.#gitEnabled()) return null;
434
+
346
435
  const branch = this.#getCurrentBranch();
347
436
  const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
348
437
 
@@ -376,6 +465,7 @@ export class StatusLineComponent implements Component {
376
465
  try {
377
466
  // Requires `gh repo set-default` to be configured; fails gracefully if not
378
467
  const result = await $`gh pr view --json number,url`.quiet().nothrow();
468
+ if (this.#disposed) return;
379
469
  if (result.exitCode !== 0) {
380
470
  setCachedPr(null);
381
471
  return;
@@ -387,10 +477,11 @@ export class StatusLineComponent implements Component {
387
477
  setCachedPr(null);
388
478
  }
389
479
  } catch {
480
+ if (this.#disposed) return;
390
481
  setCachedPr(null);
391
482
  } finally {
392
483
  this.#prLookupInFlight = false;
393
- if (this.#onBranchChange) {
484
+ if (!this.#disposed && this.#onBranchChange) {
394
485
  this.#onBranchChange();
395
486
  }
396
487
  }
@@ -515,11 +606,19 @@ export class StatusLineComponent implements Component {
515
606
  * (right after compaction, before the next response). Exposed (non-private)
516
607
  * for unit tests and the collab host's state broadcast.
517
608
  */
518
- getCachedContextBreakdown(): { usedTokens: number | null; contextWindow: number } {
609
+ getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
519
610
  const messages = this.session.messages ?? EMPTY_MESSAGES;
520
611
  const modelContextWindow = this.session.model?.contextWindow ?? 0;
521
612
  const length = messages.length;
522
613
  const lastFingerprint = length > 0 ? messageFingerprint(messages[length - 1]!) : undefined;
614
+ // Bumps when the in-flight pending snapshot is set/cleared. Without it a
615
+ // value computed mid-turn (estimate of the active tail) would survive after
616
+ // the turn ends/aborts, since clearing the snapshot touches no message.
617
+ const contextUsageRevision = this.session.contextUsageRevision ?? 0;
618
+
619
+ const systemPrompt = this.session.systemPrompt;
620
+ const tools = this.session.agent?.state?.tools;
621
+ const skills = this.session.skills;
523
622
 
524
623
  const cache = this.#contextUsageCache;
525
624
  if (
@@ -527,21 +626,29 @@ export class StatusLineComponent implements Component {
527
626
  cache.messagesRef === messages &&
528
627
  cache.length === length &&
529
628
  cache.lastFingerprint === lastFingerprint &&
530
- cache.modelContextWindow === modelContextWindow
629
+ cache.modelContextWindow === modelContextWindow &&
630
+ cache.contextUsageRevision === contextUsageRevision &&
631
+ cache.systemPromptRef === systemPrompt &&
632
+ cache.toolsRef === tools &&
633
+ cache.skillsRef === skills
531
634
  ) {
532
635
  return { usedTokens: cache.usedTokens, contextWindow: cache.contextWindow };
533
636
  }
534
637
 
535
638
  const usage = this.session.getContextUsage();
536
- const usedTokens = usage?.tokens ?? null;
639
+ const usedTokens = usage?.tokens ?? 0;
537
640
  const contextWindow = usage?.contextWindow ?? modelContextWindow;
538
641
  this.#contextUsageCache = {
539
642
  messagesRef: messages,
540
643
  length,
541
644
  lastFingerprint,
542
645
  modelContextWindow,
646
+ contextUsageRevision,
543
647
  usedTokens,
544
648
  contextWindow,
649
+ systemPromptRef: systemPrompt,
650
+ toolsRef: tools,
651
+ skillsRef: skills,
545
652
  };
546
653
  return { usedTokens, contextWindow };
547
654
  }
@@ -550,6 +657,8 @@ export class StatusLineComponent implements Component {
550
657
  width: number,
551
658
  segmentOptions: StatusLineSettings["segmentOptions"],
552
659
  includeContext: boolean,
660
+ includeGit: boolean,
661
+ includePr: boolean,
553
662
  ): SegmentContext {
554
663
  const state = this.session.state;
555
664
 
@@ -575,8 +684,7 @@ export class StatusLineComponent implements Component {
575
684
  if (includeContext) {
576
685
  const breakdown = this.getCachedContextBreakdown();
577
686
  contextWindow = breakdown.contextWindow || contextWindow;
578
- contextPercent =
579
- breakdown.usedTokens === null ? null : contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
687
+ contextPercent = contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
580
688
  }
581
689
 
582
690
  // Collab guest: context comes from the host's state frames — the local
@@ -587,6 +695,10 @@ export class StatusLineComponent implements Component {
587
695
  contextPercent = collabState.contextUsage.percent ?? contextPercent;
588
696
  }
589
697
 
698
+ const gitBranch = includeGit || includePr ? this.#getCurrentBranch() : null;
699
+ const gitStatus = includeGit ? this.#getGitStatus() : null;
700
+ const gitPr = includePr ? this.#lookupPr() : null;
701
+
590
702
  return {
591
703
  session: this.session,
592
704
  focusedAgentId: this.#focusedAgentId,
@@ -603,9 +715,9 @@ export class StatusLineComponent implements Component {
603
715
  subagentCount: this.#subagentCount,
604
716
  sessionStartTime: this.#sessionStartTime,
605
717
  git: {
606
- branch: this.#getCurrentBranch(),
607
- status: this.#getGitStatus(),
608
- pr: this.#lookupPr(),
718
+ branch: gitBranch,
719
+ status: gitStatus,
720
+ pr: gitPr,
609
721
  },
610
722
  usage: this.#cachedUsage,
611
723
  };
@@ -656,7 +768,19 @@ export class StatusLineComponent implements Component {
656
768
  const effectiveSettings = this.#resolveSettings();
657
769
  const includeContext =
658
770
  hasContextSegment(effectiveSettings.leftSegments) || hasContextSegment(effectiveSettings.rightSegments);
659
- const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions, includeContext);
771
+ const gitEnabled = this.#gitEnabled();
772
+ const includeGit =
773
+ gitEnabled &&
774
+ (hasGitSegment(effectiveSettings.leftSegments) || hasGitSegment(effectiveSettings.rightSegments));
775
+ const includePr =
776
+ gitEnabled && (hasPrSegment(effectiveSettings.leftSegments) || hasPrSegment(effectiveSettings.rightSegments));
777
+ const ctx = this.#buildSegmentContext(
778
+ width,
779
+ effectiveSettings.segmentOptions,
780
+ includeContext,
781
+ includeGit,
782
+ includePr,
783
+ );
660
784
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
661
785
 
662
786
  // `transparent` reuses the empty-string sentinel (`\x1b[49m`) so the bar
@@ -58,7 +58,6 @@ export function getContextUsageLevel(contextPercent: number, contextWindow: numb
58
58
  /**
59
59
  * Format context usage as `<percent>%/<window>` (e.g. `5.1%/1M`), matching the
60
60
  * status line's context gauge so subagent and footer renderers stay in sync.
61
- * A `null`/`undefined` percent (unknown, e.g. right after compaction) renders as `?`.
62
61
  */
63
62
  export function formatContextUsage(contextPercent: number | null | undefined, contextWindow: number): string {
64
63
  const pct = contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`;
@@ -17,6 +17,7 @@ export class TodoReminderComponent extends Container {
17
17
  super();
18
18
 
19
19
  this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
20
+ this.#box.setIgnoreTight(true);
20
21
  this.addChild(this.#box);
21
22
 
22
23
  this.#rebuild();
@@ -34,7 +34,7 @@ import {
34
34
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
35
35
  import { toolRenderers } from "../../tools/renderers";
36
36
  import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
37
- import { isFramedBlockComponent, renderStatusLine } from "../../tui";
37
+ import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
38
38
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
39
39
  import { renderDiff } from "./diff";
40
40
 
@@ -169,7 +169,7 @@ let toolExecutionInstanceSeq = 0;
169
169
  */
170
170
  export class ToolExecutionComponent extends Container implements NativeScrollbackLiveRegion {
171
171
  #contentBox: Box; // Used for custom tools and bash visual truncation
172
- #contentText: Text; // For built-in tools (with its own padding/bg)
172
+ #contentText: WidthAwareText; // Generic fallback (no custom/built-in renderer)
173
173
  #multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
174
174
  #imageComponents: Image[] = [];
175
175
  #imageSpacers: Spacer[] = [];
@@ -272,6 +272,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
272
272
  this.#ui = ui;
273
273
  this.#cwd = cwd;
274
274
  this.#args = args;
275
+ this.#editMode = resolveEditModeForTool(toolName, tool);
275
276
 
276
277
  // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
277
278
  // paddingY is 1 so background-tinted blocks (custom/extension tools and the
@@ -279,7 +280,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
279
280
  // strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
280
281
  // lines and keep their tight spacing — only tinted lines survive.
281
282
  this.#contentBox = new Box(0, 1);
282
- this.#contentText = new Text("", 1, 1);
283
+ this.#contentText = new WidthAwareText(contentWidth => this.#formatToolExecution(contentWidth), 1, 1);
283
284
 
284
285
  // Use Box for custom tools or built-in tools that have renderers
285
286
  const hasRenderer = toolName in toolRenderers;
@@ -289,8 +290,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
289
290
  } else {
290
291
  this.addChild(this.#contentText);
291
292
  }
292
-
293
- this.#editMode = resolveEditModeForTool(toolName, tool);
293
+ // Tool blocks are visually distinct cards (background-tinted or framed),
294
+ // so keep their horizontal padding even when the user enables tight layout.
295
+ this.setIgnoreTight(true);
294
296
 
295
297
  this.#updateDisplay();
296
298
  this.#editDiffInFlight = this.#runPreviewDiff();
@@ -922,9 +924,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
922
924
  }
923
925
  }
924
926
  } else {
925
- // Other built-in tools: use Text directly with caching
927
+ // Generic fallback (no custom/built-in renderer). WidthAwareText
928
+ // reformats at render time so output fills the actual terminal width
929
+ // instead of a fixed column cap.
926
930
  this.#contentText.setCustomBgFn(stateBgFn);
927
- this.#contentText.setText(this.#formatToolExecution());
931
+ this.#contentText.invalidate();
928
932
  }
929
933
 
930
934
  // Handle images (same for both custom and built-in)
@@ -1076,14 +1080,17 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1076
1080
  /**
1077
1081
  * Format a generic tool execution (fallback for tools without custom renderers)
1078
1082
  */
1079
- #formatToolExecution(): string {
1083
+ #formatToolExecution(contentWidth: number): string {
1080
1084
  const lines: string[] = [];
1081
1085
  const icon = this.#isPartial ? "pending" : this.#result?.isError ? "error" : "done";
1082
1086
  lines.push(renderStatusLine({ icon, title: this.#toolLabel }, theme));
1083
1087
 
1084
1088
  const argsObject = this.#args && typeof this.#args === "object" ? (this.#args as Record<string, unknown>) : null;
1085
1089
  if (!this.#expanded && argsObject && Object.keys(argsObject).length > 0) {
1086
- const preview = formatArgsInline(argsObject, 70);
1090
+ // Budget the inline preview against the render width, leaving room for
1091
+ // the ` └─ ` connector prefix instead of a fixed cap.
1092
+ const inlineBudget = Math.max(20, contentWidth - Bun.stringWidth(theme.tree.last) - 2);
1093
+ const preview = formatArgsInline(argsObject, inlineBudget);
1087
1094
  if (preview) {
1088
1095
  lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
1089
1096
  }
@@ -1143,7 +1150,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1143
1150
  const displayLines = outputLines.slice(0, maxOutputLines);
1144
1151
 
1145
1152
  for (const line of displayLines) {
1146
- lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80)));
1153
+ lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), contentWidth)));
1147
1154
  }
1148
1155
 
1149
1156
  if (outputLines.length > maxOutputLines) {
@@ -25,6 +25,7 @@ export class TtsrNotificationComponent extends Container {
25
25
 
26
26
  // Use inverse warning color for yellow background effect
27
27
  this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
28
+ this.#box.setIgnoreTight(true);
28
29
  this.addChild(this.#box);
29
30
 
30
31
  this.#rebuild();
@@ -42,12 +42,12 @@ export class UserMessageComponent extends Container {
42
42
  ? imageReferenceHyperlink(label, index, imageLinks, imageLabel)
43
43
  : theme.fg("accent", `\x1b[1m${label}\x1b[22m`),
44
44
  });
45
- this.addChild(
46
- new Markdown(text, 1, 1, getMarkdownTheme(), {
47
- bgColor,
48
- color,
49
- }),
50
- );
45
+ const md = new Markdown(text, 1, 1, getMarkdownTheme(), {
46
+ bgColor,
47
+ color,
48
+ });
49
+ md.setIgnoreTight(true);
50
+ this.addChild(md);
51
51
  }
52
52
 
53
53
  override render(width: number): readonly string[] {