@gaunt-sloth/core 2.0.0-alpha.8 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/.gsloth.review.md +2 -0
  2. package/README.md +71 -20
  3. package/dist/config/colour.d.ts +38 -0
  4. package/dist/config/colour.js +36 -0
  5. package/dist/config/colour.js.map +1 -0
  6. package/dist/config/configDiscovery.d.ts +79 -0
  7. package/dist/config/configDiscovery.js +80 -0
  8. package/dist/config/configDiscovery.js.map +1 -0
  9. package/dist/config/defaults.d.ts +20 -20
  10. package/dist/config/defaults.js +10 -8
  11. package/dist/config/defaults.js.map +1 -1
  12. package/dist/config/filesystem-tools.d.ts +41 -0
  13. package/dist/config/filesystem-tools.js +56 -0
  14. package/dist/config/filesystem-tools.js.map +1 -0
  15. package/dist/config/loader.d.ts +171 -19
  16. package/dist/config/loader.js +1054 -144
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/config/mouse.d.ts +50 -0
  19. package/dist/config/mouse.js +44 -0
  20. package/dist/config/mouse.js.map +1 -0
  21. package/dist/config/profiles.d.ts +68 -0
  22. package/dist/config/profiles.js +93 -0
  23. package/dist/config/profiles.js.map +1 -0
  24. package/dist/config/providerKeys.d.ts +69 -0
  25. package/dist/config/providerKeys.js +69 -0
  26. package/dist/config/providerKeys.js.map +1 -0
  27. package/dist/config/schema.d.ts +2671 -138
  28. package/dist/config/schema.js +1361 -85
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/config/shell-policy.d.ts +899 -111
  31. package/dist/config/shell-policy.js +800 -70
  32. package/dist/config/shell-policy.js.map +1 -1
  33. package/dist/config/tool-descriptions.d.ts +211 -0
  34. package/dist/config/tool-descriptions.js +272 -0
  35. package/dist/config/tool-descriptions.js.map +1 -0
  36. package/dist/config/types.d.ts +352 -41
  37. package/dist/config/types.js +1 -0
  38. package/dist/config/types.js.map +1 -1
  39. package/dist/config.d.ts +35 -1
  40. package/dist/config.js +16 -1
  41. package/dist/config.js.map +1 -1
  42. package/dist/constants.d.ts +28 -1
  43. package/dist/constants.js +28 -1
  44. package/dist/constants.js.map +1 -1
  45. package/dist/core/GthAbstractAgent.d.ts +166 -11
  46. package/dist/core/GthAbstractAgent.js +484 -45
  47. package/dist/core/GthAbstractAgent.js.map +1 -1
  48. package/dist/core/GthAgentRunner.d.ts +543 -57
  49. package/dist/core/GthAgentRunner.js +1494 -140
  50. package/dist/core/GthAgentRunner.js.map +1 -1
  51. package/dist/core/GthLangChainAgent.d.ts +117 -2
  52. package/dist/core/GthLangChainAgent.js +602 -29
  53. package/dist/core/GthLangChainAgent.js.map +1 -1
  54. package/dist/core/approvals/annotations.d.ts +122 -0
  55. package/dist/core/approvals/annotations.js +137 -0
  56. package/dist/core/approvals/annotations.js.map +1 -0
  57. package/dist/core/approvals/grants.d.ts +216 -0
  58. package/dist/core/approvals/grants.js +469 -0
  59. package/dist/core/approvals/grants.js.map +1 -0
  60. package/dist/core/approvals/matcher.d.ts +202 -0
  61. package/dist/core/approvals/matcher.js +267 -0
  62. package/dist/core/approvals/matcher.js.map +1 -0
  63. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  64. package/dist/core/approvals/mcpSubjects.js +99 -0
  65. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  66. package/dist/core/approvals/promptHeader.d.ts +28 -0
  67. package/dist/core/approvals/promptHeader.js +62 -0
  68. package/dist/core/approvals/promptHeader.js.map +1 -0
  69. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  70. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  71. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  72. package/dist/core/approvals/toolHost.d.ts +46 -0
  73. package/dist/core/approvals/toolHost.js +108 -0
  74. package/dist/core/approvals/toolHost.js.map +1 -0
  75. package/dist/core/debugCapture.d.ts +74 -0
  76. package/dist/core/debugCapture.js +100 -0
  77. package/dist/core/debugCapture.js.map +1 -0
  78. package/dist/core/gthLeanAgentFactory.d.ts +4 -4
  79. package/dist/core/gthLeanAgentFactory.js +4 -4
  80. package/dist/core/launchBanner.d.ts +127 -0
  81. package/dist/core/launchBanner.js +414 -0
  82. package/dist/core/launchBanner.js.map +1 -0
  83. package/dist/core/modelLabel.d.ts +19 -0
  84. package/dist/core/modelLabel.js +26 -0
  85. package/dist/core/modelLabel.js.map +1 -0
  86. package/dist/core/plainToolIndication.d.ts +15 -0
  87. package/dist/core/plainToolIndication.js +174 -0
  88. package/dist/core/plainToolIndication.js.map +1 -0
  89. package/dist/core/reasoningBlocks.d.ts +65 -0
  90. package/dist/core/reasoningBlocks.js +103 -0
  91. package/dist/core/reasoningBlocks.js.map +1 -0
  92. package/dist/core/refusal.d.ts +53 -0
  93. package/dist/core/refusal.js +133 -0
  94. package/dist/core/refusal.js.map +1 -0
  95. package/dist/core/runHeader.d.ts +38 -0
  96. package/dist/core/runHeader.js +42 -0
  97. package/dist/core/runHeader.js.map +1 -0
  98. package/dist/core/runStats.d.ts +14 -3
  99. package/dist/core/runStats.js +48 -3
  100. package/dist/core/runStats.js.map +1 -1
  101. package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
  102. package/dist/core/shell/ShellCommandFailedError.js +3 -4
  103. package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
  104. package/dist/core/shell/abstention.d.ts +88 -0
  105. package/dist/core/shell/abstention.js +184 -0
  106. package/dist/core/shell/abstention.js.map +1 -0
  107. package/dist/core/shell/approvalCapture.d.ts +271 -0
  108. package/dist/core/shell/approvalCapture.js +108 -0
  109. package/dist/core/shell/approvalCapture.js.map +1 -0
  110. package/dist/core/shell/approvalStop.d.ts +123 -0
  111. package/dist/core/shell/approvalStop.js +269 -0
  112. package/dist/core/shell/approvalStop.js.map +1 -0
  113. package/dist/core/shell/arity.d.ts +6 -0
  114. package/dist/core/shell/arity.js +20 -6
  115. package/dist/core/shell/arity.js.map +1 -1
  116. package/dist/core/shell/denylist.d.ts +11 -0
  117. package/dist/core/shell/denylist.js +37 -0
  118. package/dist/core/shell/denylist.js.map +1 -0
  119. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  120. package/dist/core/shell/escalationSeverity.js +89 -0
  121. package/dist/core/shell/escalationSeverity.js.map +1 -0
  122. package/dist/core/shell/framing.d.ts +190 -0
  123. package/dist/core/shell/framing.js +633 -0
  124. package/dist/core/shell/framing.js.map +1 -0
  125. package/dist/core/shell/hardline.d.ts +103 -0
  126. package/dist/core/shell/hardline.js +780 -0
  127. package/dist/core/shell/hardline.js.map +1 -0
  128. package/dist/core/shell/negotiation.d.ts +328 -0
  129. package/dist/core/shell/negotiation.js +488 -0
  130. package/dist/core/shell/negotiation.js.map +1 -0
  131. package/dist/core/shell/normalize.d.ts +44 -4
  132. package/dist/core/shell/normalize.js +61 -7
  133. package/dist/core/shell/normalize.js.map +1 -1
  134. package/dist/core/shell/openWorld.d.ts +263 -0
  135. package/dist/core/shell/openWorld.js +1188 -0
  136. package/dist/core/shell/openWorld.js.map +1 -0
  137. package/dist/core/shell/rater.d.ts +873 -0
  138. package/dist/core/shell/rater.js +1454 -0
  139. package/dist/core/shell/rater.js.map +1 -0
  140. package/dist/core/shell/raterModel.d.ts +41 -0
  141. package/dist/core/shell/raterModel.js +51 -0
  142. package/dist/core/shell/raterModel.js.map +1 -0
  143. package/dist/core/shell/raterVocabulary.d.ts +121 -0
  144. package/dist/core/shell/raterVocabulary.js +116 -0
  145. package/dist/core/shell/raterVocabulary.js.map +1 -0
  146. package/dist/core/shell/rejection.d.ts +69 -0
  147. package/dist/core/shell/rejection.js +38 -0
  148. package/dist/core/shell/rejection.js.map +1 -0
  149. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  150. package/dist/core/toolCallRepair/grammar.js +116 -0
  151. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  152. package/dist/core/toolCallRepair/index.d.ts +2 -0
  153. package/dist/core/toolCallRepair/index.js +7 -0
  154. package/dist/core/toolCallRepair/index.js.map +1 -0
  155. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  156. package/dist/core/toolCallRepair/payload.js +341 -0
  157. package/dist/core/toolCallRepair/payload.js.map +1 -0
  158. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  159. package/dist/core/toolCallRepair/promote.js +90 -0
  160. package/dist/core/toolCallRepair/promote.js.map +1 -0
  161. package/dist/core/toolDisplay.d.ts +123 -0
  162. package/dist/core/toolDisplay.js +451 -0
  163. package/dist/core/toolDisplay.js.map +1 -0
  164. package/dist/core/toolOutputChannel.d.ts +95 -0
  165. package/dist/core/toolOutputChannel.js +165 -0
  166. package/dist/core/toolOutputChannel.js.map +1 -0
  167. package/dist/core/types.d.ts +349 -16
  168. package/dist/core/types.js.map +1 -1
  169. package/dist/history/historyFormat.d.ts +12 -3
  170. package/dist/history/historyFormat.js +50 -8
  171. package/dist/history/historyFormat.js.map +1 -1
  172. package/dist/history/historyStore.d.ts +77 -0
  173. package/dist/history/historyStore.js +173 -6
  174. package/dist/history/historyStore.js.map +1 -1
  175. package/dist/history/recordSession.d.ts +10 -1
  176. package/dist/history/recordSession.js +27 -0
  177. package/dist/history/recordSession.js.map +1 -1
  178. package/dist/providers/anthropic.js +12 -0
  179. package/dist/providers/anthropic.js.map +1 -1
  180. package/dist/providers/configurationPassthrough.d.ts +107 -0
  181. package/dist/providers/configurationPassthrough.js +148 -0
  182. package/dist/providers/configurationPassthrough.js.map +1 -0
  183. package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
  184. package/dist/providers/geminiSchemaSanitizer.js +347 -0
  185. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  186. package/dist/providers/geminiThinking.d.ts +60 -0
  187. package/dist/providers/geminiThinking.js +92 -0
  188. package/dist/providers/geminiThinking.js.map +1 -0
  189. package/dist/providers/google-genai.js +18 -1
  190. package/dist/providers/google-genai.js.map +1 -1
  191. package/dist/providers/groq.js +12 -0
  192. package/dist/providers/groq.js.map +1 -1
  193. package/dist/providers/huggingface.d.ts +25 -0
  194. package/dist/providers/huggingface.js +69 -0
  195. package/dist/providers/huggingface.js.map +1 -0
  196. package/dist/providers/modelCatalog.d.ts +109 -0
  197. package/dist/providers/modelCatalog.js +245 -0
  198. package/dist/providers/modelCatalog.js.map +1 -0
  199. package/dist/providers/modelDiscovery.d.ts +99 -5
  200. package/dist/providers/modelDiscovery.js +191 -35
  201. package/dist/providers/modelDiscovery.js.map +1 -1
  202. package/dist/providers/ollama.d.ts +18 -4
  203. package/dist/providers/ollama.js +67 -37
  204. package/dist/providers/ollama.js.map +1 -1
  205. package/dist/providers/openai.js +34 -0
  206. package/dist/providers/openai.js.map +1 -1
  207. package/dist/providers/openrouter.d.ts +26 -4
  208. package/dist/providers/openrouter.js +83 -26
  209. package/dist/providers/openrouter.js.map +1 -1
  210. package/dist/providers/vertexai.js +19 -1
  211. package/dist/providers/vertexai.js.map +1 -1
  212. package/dist/providers/xai.js +20 -0
  213. package/dist/providers/xai.js.map +1 -1
  214. package/dist/runtime/askStructured.d.ts +105 -0
  215. package/dist/runtime/askStructured.js +120 -0
  216. package/dist/runtime/askStructured.js.map +1 -0
  217. package/dist/runtime/conversation.d.ts +64 -0
  218. package/dist/runtime/conversation.js +171 -0
  219. package/dist/runtime/conversation.js.map +1 -0
  220. package/dist/runtime/singleShot.d.ts +37 -6
  221. package/dist/runtime/singleShot.js +113 -67
  222. package/dist/runtime/singleShot.js.map +1 -1
  223. package/dist/runtime/structuredOutput.d.ts +104 -0
  224. package/dist/runtime/structuredOutput.js +393 -0
  225. package/dist/runtime/structuredOutput.js.map +1 -0
  226. package/dist/utils/ProgressIndicator.d.ts +21 -0
  227. package/dist/utils/ProgressIndicator.js +30 -3
  228. package/dist/utils/ProgressIndicator.js.map +1 -1
  229. package/dist/utils/aiignoreUtils.js.map +1 -1
  230. package/dist/utils/binaryOutputUtils.js.map +1 -1
  231. package/dist/utils/consoleUtils.d.ts +95 -0
  232. package/dist/utils/consoleUtils.js +112 -2
  233. package/dist/utils/consoleUtils.js.map +1 -1
  234. package/dist/utils/crashHandler.d.ts +87 -0
  235. package/dist/utils/crashHandler.js +128 -0
  236. package/dist/utils/crashHandler.js.map +1 -0
  237. package/dist/utils/debugDump.d.ts +134 -0
  238. package/dist/utils/debugDump.js +381 -0
  239. package/dist/utils/debugDump.js.map +1 -0
  240. package/dist/utils/debugUtils.d.ts +13 -4
  241. package/dist/utils/debugUtils.js +36 -13
  242. package/dist/utils/debugUtils.js.map +1 -1
  243. package/dist/utils/displayWidth.d.ts +53 -0
  244. package/dist/utils/displayWidth.js +195 -0
  245. package/dist/utils/displayWidth.js.map +1 -0
  246. package/dist/utils/fileUtils.js.map +1 -1
  247. package/dist/utils/globalConfigUtils.d.ts +14 -2
  248. package/dist/utils/globalConfigUtils.js +22 -4
  249. package/dist/utils/globalConfigUtils.js.map +1 -1
  250. package/dist/utils/llmUtils.d.ts +39 -8
  251. package/dist/utils/llmUtils.js +76 -8
  252. package/dist/utils/llmUtils.js.map +1 -1
  253. package/dist/utils/redactSecrets.d.ts +63 -0
  254. package/dist/utils/redactSecrets.js +286 -0
  255. package/dist/utils/redactSecrets.js.map +1 -0
  256. package/dist/utils/systemPromptNotes.d.ts +222 -0
  257. package/dist/utils/systemPromptNotes.js +338 -0
  258. package/dist/utils/systemPromptNotes.js.map +1 -0
  259. package/dist/utils/systemUtils.d.ts +18 -1
  260. package/dist/utils/systemUtils.js +38 -3
  261. package/dist/utils/systemUtils.js.map +1 -1
  262. package/dist/utils/toolMatching.d.ts +30 -0
  263. package/dist/utils/toolMatching.js +44 -0
  264. package/dist/utils/toolMatching.js.map +1 -0
  265. package/dist/utils/untrustedText.d.ts +86 -0
  266. package/dist/utils/untrustedText.js +101 -0
  267. package/dist/utils/untrustedText.js.map +1 -0
  268. package/package.json +21 -7
  269. package/schema/gsloth-config.schema.json +1921 -326
  270. package/dist/core/shell/allowlist.d.ts +0 -75
  271. package/dist/core/shell/allowlist.js +0 -187
  272. package/dist/core/shell/allowlist.js.map +0 -1
  273. package/dist/core/shell/judge.d.ts +0 -161
  274. package/dist/core/shell/judge.js +0 -261
  275. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,90 @@
1
+ // EXT-35 — promote a text-emitted tool call to a native LangChain tool_call.
2
+ //
3
+ // The LangChain-message adaptation of the openclaw `@openclaw/tool-call-repair` reference
4
+ // (`packages/tool-call-repair/src/promote.ts`'s `promoteStandalonePlainTextToolCallMessage`).
5
+ // Small/local models (Gemma, lmstudio, gpt-oss) often serialise a tool call as assistant TEXT
6
+ // rather than a native `tool_call`; the ReAct graph then sees no tool_calls and ends the turn
7
+ // ("no tool calls = done"), stalling the loop. This module rescues a STANDALONE text-emitted call
8
+ // by rewriting the assistant message into one carrying a native `tool_call`, so the graph routes to
9
+ // the tools node and the loop continues.
10
+ //
11
+ // Three hard gates keep prose from ever being misread as a call:
12
+ // 1. name allow-list — only a call whose tool name is in the bound toolset promotes (an empty
13
+ // allow-list promotes nothing, the prose-safe default);
14
+ // 2. payload-size cap — an oversized JSON blob is not treated as a call;
15
+ // 3. standalone-only — the message content must be ESSENTIALLY JUST the call (enforced by
16
+ // {@link parseStandalonePlainTextToolCallBlocks}'s whole-input walk).
17
+ //
18
+ // Unlike the reference we do NOT set a `stopReason`/`toolUse` flag: langchain's ReAct router keys
19
+ // purely off the presence of `tool_calls` on the last AIMessage, so promoting the tool_calls array
20
+ // is sufficient to continue the loop.
21
+ import { AIMessage } from '@langchain/core/messages';
22
+ import { randomUUID } from 'node:crypto';
23
+ import { MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES, parseStandalonePlainTextToolCallBlocks, } from './payload.js';
24
+ /**
25
+ * A sane default cap on a single repaired call's serialized payload. A text blob larger than this
26
+ * is never treated as a tool call. Overridable per call so a test can assert the cap with a small
27
+ * value; production leaves it at the default. The single source of truth lives in `./payload.js`
28
+ * and is re-exported here under its public name (EXT-43 unified the former duplicated literal).
29
+ */
30
+ export { MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES } from './payload.js';
31
+ /**
32
+ * Parse assistant `text` as one-or-more STANDALONE text-emitted tool calls and return them as
33
+ * native LangChain tool_calls, or `undefined` when the text is not a standalone call, names a tool
34
+ * outside the allow-list, or exceeds the payload cap. Fresh `id`s are minted per call (the model
35
+ * emitted none). Empty allow-list ⇒ `undefined` (never promote).
36
+ */
37
+ export function textToNativeToolCalls(text, options) {
38
+ const allowedToolNames = new Set(options.allowedToolNames);
39
+ // Prose-safety default: with no bound tools nothing can be a call.
40
+ if (allowedToolNames.size === 0) {
41
+ return undefined;
42
+ }
43
+ const trimmed = (text ?? '').trim();
44
+ if (!trimmed) {
45
+ return undefined;
46
+ }
47
+ const parseOptions = {
48
+ allowedToolNames,
49
+ maxPayloadBytes: options.maxPayloadBytes ?? MAX_TEXT_EMITTED_TOOL_CALL_PAYLOAD_BYTES,
50
+ };
51
+ const blocks = parseStandalonePlainTextToolCallBlocks(trimmed, parseOptions);
52
+ if (!blocks) {
53
+ return undefined;
54
+ }
55
+ return blocks.map((block) => ({
56
+ id: randomUUID(),
57
+ name: block.name,
58
+ args: block.arguments,
59
+ type: 'tool_call',
60
+ }));
61
+ }
62
+ /**
63
+ * Repair one assistant {@link AIMessage}: if it has NO native tool_calls but its content is a
64
+ * standalone text-emitted call (allow-listed, within the payload cap), return a NEW AIMessage that
65
+ * carries the promoted native tool_calls; otherwise return `undefined` (the caller keeps the
66
+ * original, so the native happy path is untouched).
67
+ *
68
+ * The returned message PRESERVES the original `id`. That is load-bearing: LangGraph's message-state
69
+ * reducer merges by id, so a same-id message REPLACES the original in graph state (rather than
70
+ * appending a duplicate/dangling assistant message). Content is cleared to '' since the raw
71
+ * text-call is now represented natively as tool_calls.
72
+ */
73
+ export function promoteTextEmittedToolCallMessage(message, options) {
74
+ // Happy path untouched: repair engages ONLY when there are no native tool_calls.
75
+ if (message.tool_calls && message.tool_calls.length > 0) {
76
+ return undefined;
77
+ }
78
+ const toolCalls = textToNativeToolCalls(message.text, options);
79
+ if (!toolCalls) {
80
+ return undefined;
81
+ }
82
+ return new AIMessage({
83
+ id: message.id,
84
+ content: '',
85
+ tool_calls: toolCalls,
86
+ additional_kwargs: message.additional_kwargs,
87
+ response_metadata: message.response_metadata,
88
+ });
89
+ }
90
+ //# sourceMappingURL=promote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promote.js","sourceRoot":"","sources":["../../../src/core/toolCallRepair/promote.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,0FAA0F;AAC1F,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,kGAAkG;AAClG,oGAAoG;AACpG,yCAAyC;AACzC,EAAE;AACF,iEAAiE;AACjE,gGAAgG;AAChG,6DAA6D;AAC7D,2EAA2E;AAC3E,4FAA4F;AAC5F,2EAA2E;AAC3E,EAAE;AACF,kGAAkG;AAClG,mGAAmG;AACnG,sCAAsC;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,wCAAwC,EACxC,sCAAsC,GAEvC,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,OAAO,EAAE,wCAAwC,EAAE,MAAM,cAAc,CAAC;AAsBxE;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAwB,EACxB,OAAkC;IAElC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3D,mEAAmE;IACnE,IAAI,gBAAgB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,YAAY,GAAkC;QAClD,gBAAgB;QAChB,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,wCAAwC;KACrF,CAAC;IACF,MAAM,MAAM,GAAG,sCAAsC,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,EAAE,EAAE,UAAU,EAAE;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,SAAS;QACrB,IAAI,EAAE,WAAoB;KAC3B,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iCAAiC,CAC/C,OAAkB,EAClB,OAAkC;IAElC,iFAAiF;IACjF,IAAI,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,qBAAqB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,IAAI,SAAS,CAAC;QACnB,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,OAAO,EAAE,EAAE;QACX,UAAU,EAAE,SAAS;QACrB,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;KAC7C,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,123 @@
1
+ /** The single canonical output-preview cap (lines). No other preview length exists anywhere. */
2
+ export declare const TOOL_OUTPUT_PREVIEW_LINES = 10;
3
+ /**
4
+ * Per-line cap for preview lines, in terminal COLUMNS (a one-line minified bundle must not flood a
5
+ * row). The `CHARS` in the name is historical; every cap here is a column budget, because a
6
+ * character count is not a width — see `#src/utils/displayWidth.js`.
7
+ */
8
+ export declare const TOOL_PREVIEW_LINE_MAX_CHARS = 200;
9
+ /** Per-value cap inside a params summary, in terminal COLUMNS. */
10
+ export declare const TOOL_PARAM_VALUE_MAX_CHARS = 48;
11
+ /** Whole params-summary cap (everything inside the parentheses), in terminal COLUMNS. */
12
+ export declare const TOOL_SUMMARY_MAX_CHARS = 120;
13
+ /** The overflow/truncation marker used everywhere in this module. */
14
+ export declare const ELLIPSIS = "\u2026";
15
+ /**
16
+ * Columns {@link ELLIPSIS} itself occupies, which every truncator has to reserve out of its budget
17
+ * before slicing. Derived rather than written as `1` so it stays true of whatever marker is used.
18
+ */
19
+ export declare const ELLIPSIS_WIDTH: number;
20
+ /**
21
+ * Style tag for one rendered line. The two surfaces map these to their own colour systems
22
+ * (Ink `<Text>` props / raw ANSI): `dim` = greyed preview text, `added`/`removed` = diff
23
+ * green/red. DL-8 colour semantics; monochrome surfaces render the raw text unchanged.
24
+ */
25
+ export type ToolDisplayStyle = 'dim' | 'added' | 'removed';
26
+ /** One line of a tool call's rendered body/preview. */
27
+ export interface ToolDisplayLine {
28
+ text: string;
29
+ style: ToolDisplayStyle;
30
+ }
31
+ /**
32
+ * Everything a formatter may look at for one tool call. Both surfaces build this from their
33
+ * own state (the TUI from `ToolCallViewModel`, the plain surface from the message stream).
34
+ */
35
+ export interface ToolCallDisplayInput {
36
+ /** The gth tool name (`read_file`, `run_shell_command`, a custom tool's name). */
37
+ name: string;
38
+ /** The raw streamed args JSON (possibly partial mid-stream, possibly invalid). */
39
+ argsText?: string;
40
+ /** The final model-facing tool result, when it has arrived. */
41
+ result?: string;
42
+ /** Live streamed child output (TUI only — the `tool_output` channel's accumulation). */
43
+ output?: string;
44
+ /** The real `ToolMessage.status === 'error'` signal (TUI-C7) — never sniffed from text. */
45
+ isError?: boolean;
46
+ /**
47
+ * True when the live child output ALREADY streamed raw to the user's terminal (the plain
48
+ * surface's default sink). Formatters then suppress the duplicated output body and render
49
+ * only the closing status — the TUI-C17 "output AND result repeat each other" dedupe.
50
+ */
51
+ liveOutputAlreadyShown?: boolean;
52
+ }
53
+ /**
54
+ * Tolerant parse of a (possibly partial) streamed args JSON. Mirrors the view-model's
55
+ * defensive posture: a half-streamed or malformed buffer never throws — it returns `null`.
56
+ */
57
+ export declare function parseToolArgsSafe(argsText: string | undefined): Record<string, unknown> | null;
58
+ /**
59
+ * Register the live config for inline-secret collection (TUI-C32 residual a). Resets the secret
60
+ * cache so the next {@link getDefaultSecrets} recomputes with the config's inline literals folded
61
+ * in. Idempotent; a later call with a fresh config supersedes the previous one.
62
+ */
63
+ export declare function setToolDisplayConfig(config: unknown): void;
64
+ /** Test seam: drop the cached secret literals + registered config so specs can vary both. */
65
+ export declare function resetToolDisplaySecretsCacheForTests(): void;
66
+ /**
67
+ * Parse the shared shell-result shape (`Executing '…'…\n\n<COMMAND_OUTPUT>…</COMMAND_OUTPUT>\n\n
68
+ * <status line>`). Returns the captured output body and the trailing status text, or `null`
69
+ * when the result is not shell-shaped.
70
+ */
71
+ export declare function parseCommandOutputResult(result: string | undefined): {
72
+ body: string;
73
+ tail: string;
74
+ } | null;
75
+ /**
76
+ * TUI-C32 residual c — is THIS tool call genuinely a shell-shaped result (name allows it AND the
77
+ * result actually carries the `<COMMAND_OUTPUT>` shape)? Used by the plain surface to decide
78
+ * `liveOutputAlreadyShown` (the child streamed live via the tool-output channel's default sink),
79
+ * so a non-shell tool whose result merely quotes the marker no longer has its body suppressed.
80
+ */
81
+ export declare function isShellShapedResult(name: string, result: string | undefined): boolean;
82
+ /** The registry glyph for a tool name (generic `⚙` when unknown). */
83
+ export declare function getToolGlyph(name: string): string;
84
+ /**
85
+ * One-line call summary: `name(arg=val, other=…)`. Key args only (per the registry entry, or
86
+ * all args for unknown tools), each value inlined + truncated, the whole parenthesised part
87
+ * capped at {@link TOOL_SUMMARY_MAX_CHARS}, and everything secret-redacted (literals +
88
+ * provider patterns). Unparsable (mid-stream/malformed) args render as `name(…)` — never a
89
+ * raw JSON dump. `secrets` defaults to the env-derived literals; pass explicitly for tests.
90
+ *
91
+ * Redaction runs BEFORE every truncation step (per value in {@link formatParamValue}, and again
92
+ * before the whole-summary cap): truncating first would bisect a literal secret longer than a
93
+ * cap so it no longer literal-matches, leaking its head into the rendered summary
94
+ * (fix-cycle-1 finding). The final pass over the assembled string is defense in depth only —
95
+ * `redactText` is idempotent, so re-redacting already-marked text is safe.
96
+ */
97
+ export declare function summariseToolCall(name: string, argsText: string | undefined, secrets?: readonly string[]): string;
98
+ /**
99
+ * The FULL (uncapped) body lines for a call: the registry formatter when one applies, else the
100
+ * shape-based shell formatter, else the generic fallback (live output lines, then the final
101
+ * result — both dim). Every line is secret-redacted. Used by the TUI's EXPANDED panel; cap it
102
+ * with {@link capToolDisplayLines} for the collapsed preview.
103
+ */
104
+ export declare function buildToolBodyLines(input: ToolCallDisplayInput, secrets?: readonly string[]): ToolDisplayLine[];
105
+ /**
106
+ * Apply the canonical render cap: at most `maxLines` lines (each char-capped at
107
+ * {@link TOOL_PREVIEW_LINE_MAX_CHARS} with `…`), plus a dim `… (+N more lines)` overflow
108
+ * marker when anything was cut. The marker line is IN ADDITION to the cap so exactly how much
109
+ * was hidden is always stated (DL-4 transparency).
110
+ */
111
+ export declare function capToolDisplayLines(lines: ToolDisplayLine[], maxLines?: number): ToolDisplayLine[];
112
+ /**
113
+ * The collapsed inline preview: {@link buildToolBodyLines} capped at the canonical
114
+ * {@link TOOL_OUTPUT_PREVIEW_LINES}.
115
+ */
116
+ export declare function buildToolPreviewLines(input: ToolCallDisplayInput, secrets?: readonly string[]): ToolDisplayLine[];
117
+ /**
118
+ * Render one styled line as a raw string for the plain surface. Each line is SELF-STYLED
119
+ * (its own SGR open + reset) so lines compose safely regardless of surrounding styling.
120
+ * With `colour` false the raw text is returned unchanged — the clean monochrome degradation
121
+ * for non-TTY/piped output (DL-7); diff lines still read via their `+`/`-` prefixes.
122
+ */
123
+ export declare function renderToolLineAnsi(line: ToolDisplayLine, colour: boolean): string;
@@ -0,0 +1,451 @@
1
+ /**
2
+ * @module toolDisplay
3
+ * TUI-C30 — the surface-agnostic tool-display registry: how a tool CALL is summarised
4
+ * (`name(arg=val, …)`) and how its OUTPUT is previewed (up to {@link TOOL_OUTPUT_PREVIEW_LINES}
5
+ * greyed lines, diff-coloured for `write_file`/`edit_file`), shared by BOTH render surfaces:
6
+ *
7
+ * - the Ink TUI's `ToolCallPanel` (`packages/app/src/tui/components/LiveTurn.tsx`), and
8
+ * - the plain/readline (`--no-tui`, piped, single-shot) surface
9
+ * (`core/plainToolIndication.ts`).
10
+ *
11
+ * Everything here is PURE STRINGS plus a style tag per line ({@link ToolDisplayLine}), so the
12
+ * module knows nothing about Ink or ANSI: the TUI maps styles to `<Text>` props and the plain
13
+ * surface maps them to raw ANSI via {@link renderToolLineAnsi}. The shape mirrors vue-ui's
14
+ * PLAT-17 `toolDisplay.ts` (name → glyph + which args to summarise + optional result formatter,
15
+ * with a generic fallback — cf. openclaw's `TOOL_DISPLAY_CONFIG` and opencode's
16
+ * `collapseToolOutput`) so the surfaces stay analogous without sharing code.
17
+ *
18
+ * Design rules (from the node spec):
19
+ * - **One canonical output cap: {@link TOOL_OUTPUT_PREVIEW_LINES} lines** (and a per-line char
20
+ * cap), applied by {@link capToolDisplayLines} with a `… (+N more lines)` overflow marker.
21
+ * This is a RENDER-time cap only — the model-facing `OutputBuffer`/EXT-9 head-tail caps are a
22
+ * separate layer beneath it and are never touched here.
23
+ * - **Secret redaction reuses the GS2-47 `redactSecrets` lineage** (literal env/config secrets +
24
+ * provider key patterns) — no new redactor. Applied to the params summary and every
25
+ * preview/body line, fail-safe (redact MORE on any error, never leak).
26
+ * - **`write_file`/`edit_file` render the change as a diff derived from the tool's ARGS**
27
+ * (added = `added` style/green, removed = `removed` style/red); monochrome keeps the `+`/`-`
28
+ * prefixes so the diff still reads without colour (DL-7 graceful degradation).
29
+ */
30
+ import { displayWidth, sliceToWidth } from '#src/utils/displayWidth.js';
31
+ import { collectSecretValues, redactText } from '#src/utils/redactSecrets.js';
32
+ import { env } from '#src/utils/systemUtils.js';
33
+ /** The single canonical output-preview cap (lines). No other preview length exists anywhere. */
34
+ export const TOOL_OUTPUT_PREVIEW_LINES = 10;
35
+ /**
36
+ * Per-line cap for preview lines, in terminal COLUMNS (a one-line minified bundle must not flood a
37
+ * row). The `CHARS` in the name is historical; every cap here is a column budget, because a
38
+ * character count is not a width — see `#src/utils/displayWidth.js`.
39
+ */
40
+ export const TOOL_PREVIEW_LINE_MAX_CHARS = 200;
41
+ /** Per-value cap inside a params summary, in terminal COLUMNS. */
42
+ export const TOOL_PARAM_VALUE_MAX_CHARS = 48;
43
+ /** Whole params-summary cap (everything inside the parentheses), in terminal COLUMNS. */
44
+ export const TOOL_SUMMARY_MAX_CHARS = 120;
45
+ /** The overflow/truncation marker used everywhere in this module. */
46
+ export const ELLIPSIS = '…';
47
+ /**
48
+ * Columns {@link ELLIPSIS} itself occupies, which every truncator has to reserve out of its budget
49
+ * before slicing. Derived rather than written as `1` so it stays true of whatever marker is used.
50
+ */
51
+ export const ELLIPSIS_WIDTH = displayWidth(ELLIPSIS);
52
+ const FALLBACK_GLYPH = '⚙';
53
+ const FILE_GLYPH = '📁';
54
+ const SHELL_GLYPH = '🔧';
55
+ /* ------------------------------------------------------------------------- *
56
+ * Small shared helpers *
57
+ * ------------------------------------------------------------------------- */
58
+ /**
59
+ * Tolerant parse of a (possibly partial) streamed args JSON. Mirrors the view-model's
60
+ * defensive posture: a half-streamed or malformed buffer never throws — it returns `null`.
61
+ */
62
+ export function parseToolArgsSafe(argsText) {
63
+ if (!argsText || !argsText.trim())
64
+ return null;
65
+ try {
66
+ const parsed = JSON.parse(argsText);
67
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
68
+ return parsed;
69
+ }
70
+ return null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * The live gth config, registered once per run so {@link getDefaultSecrets} can harvest INLINE
78
+ * config secrets (a pasted `apiKey`/`token` value) via the GS2-47 config walk — not only the
79
+ * env-derived literals. `GthAgentRunner.init` sets this next to the crash-context hand-off, so
80
+ * both render surfaces (the plain observer and the Ink TUI) see it. `undefined` (no config yet)
81
+ * degrades to env-only collection + the provider patterns, exactly as before.
82
+ */
83
+ let displayConfig = undefined;
84
+ /**
85
+ * Register the live config for inline-secret collection (TUI-C32 residual a). Resets the secret
86
+ * cache so the next {@link getDefaultSecrets} recomputes with the config's inline literals folded
87
+ * in. Idempotent; a later call with a fresh config supersedes the previous one.
88
+ */
89
+ export function setToolDisplayConfig(config) {
90
+ displayConfig = config;
91
+ cachedSecrets = null;
92
+ }
93
+ /**
94
+ * Lazily-computed default secret literals for redaction (GS2-47 technique 1), harvested from the
95
+ * process env AND the registered config (inline `apiKey`/`token` values) the same way
96
+ * `/debug-dump` does. Cached because `collectSecretValues` walks the whole env + config; the set
97
+ * cannot change mid-process in any way this render path must react to (a new config resets it via
98
+ * {@link setToolDisplayConfig}). `redactText` additionally always applies the provider-key
99
+ * patterns (technique 2).
100
+ */
101
+ let cachedSecrets = null;
102
+ function getDefaultSecrets() {
103
+ if (cachedSecrets === null) {
104
+ try {
105
+ cachedSecrets = collectSecretValues(displayConfig, env ?? {});
106
+ }
107
+ catch {
108
+ cachedSecrets = []; // patterns still apply via redactText
109
+ }
110
+ }
111
+ return cachedSecrets;
112
+ }
113
+ /** Test seam: drop the cached secret literals + registered config so specs can vary both. */
114
+ export function resetToolDisplaySecretsCacheForTests() {
115
+ cachedSecrets = null;
116
+ displayConfig = undefined;
117
+ }
118
+ /** Collapse whitespace runs (incl. newlines) so a value stays a one-line token. */
119
+ function inline(value) {
120
+ return value.replace(/\s+/g, ' ').trim();
121
+ }
122
+ /**
123
+ * Truncate to `max` terminal COLUMNS with the {@link ELLIPSIS} marker, reserving the marker's own
124
+ * width out of the budget. Slicing goes through the shared width primitive: a code-point count
125
+ * would read a CJK or emoji value as shorter than it renders and hand it back whole, over-running
126
+ * the row it is drawn on.
127
+ *
128
+ * Whether the value fits is put to the SLICE rather than to the ruler, because the slice stops at
129
+ * the budget: a value that over-runs is recognised from its first `max` columns, where measuring
130
+ * it reads all of it — and the values arriving here are whole tool-output lines, which
131
+ * {@link TOOL_PREVIEW_LINE_MAX_CHARS} exists precisely because they can be a megabyte long.
132
+ */
133
+ function truncate(value, max) {
134
+ const fitted = sliceToWidth(value, max);
135
+ if (fitted === value)
136
+ return value;
137
+ return sliceToWidth(value, max - ELLIPSIS_WIDTH) + ELLIPSIS;
138
+ }
139
+ /**
140
+ * Render one arg value for the summary: strings inline+truncated, the rest JSON-ish.
141
+ *
142
+ * ORDER MATTERS (fix-cycle-1 finding): redaction runs BEFORE any transformation. Truncating
143
+ * first would bisect a literal secret longer than the value cap so it no longer
144
+ * literal-matches — its head would render in the call summary on both surfaces (a partial
145
+ * leak); whitespace-collapsing first could likewise alter a literal out of matching. So:
146
+ * redact the RAW text, then inline, then truncate (`<redacted>` contains no whitespace and is
147
+ * shorter than the cap, so the later steps can never damage the marker).
148
+ */
149
+ function formatParamValue(value, secrets) {
150
+ let text;
151
+ if (typeof value === 'string') {
152
+ text = value;
153
+ }
154
+ else if (value === undefined) {
155
+ text = 'undefined';
156
+ }
157
+ else {
158
+ try {
159
+ text = JSON.stringify(value) ?? String(value);
160
+ }
161
+ catch {
162
+ text = String(value);
163
+ }
164
+ }
165
+ return truncate(inline(redactText(text, secrets)), TOOL_PARAM_VALUE_MAX_CHARS);
166
+ }
167
+ /** Split into lines, dropping a single trailing newline's phantom empty last element. */
168
+ function toLines(text) {
169
+ const lines = text.split('\n');
170
+ if (lines.length > 1 && lines[lines.length - 1] === '')
171
+ lines.pop();
172
+ return lines;
173
+ }
174
+ /** Map raw text lines to styled lines. */
175
+ function styled(lines, style) {
176
+ return lines.map((text) => ({ text, style }));
177
+ }
178
+ /* ------------------------------------------------------------------------- *
179
+ * Shell-shaped results (<COMMAND_OUTPUT>) — run_* / run_shell_command / *
180
+ * custom toolkit tools all share this body shape, so it is detected by *
181
+ * SHAPE, not by name (custom tool names are user-defined). *
182
+ * ------------------------------------------------------------------------- */
183
+ const COMMAND_OUTPUT_RE = /<COMMAND_OUTPUT>\n?([\s\S]*?)<\/COMMAND_OUTPUT>\n?([\s\S]*)$/;
184
+ /**
185
+ * Parse the shared shell-result shape (`Executing '…'…\n\n<COMMAND_OUTPUT>…</COMMAND_OUTPUT>\n\n
186
+ * <status line>`). Returns the captured output body and the trailing status text, or `null`
187
+ * when the result is not shell-shaped.
188
+ */
189
+ export function parseCommandOutputResult(result) {
190
+ if (!result)
191
+ return null;
192
+ const match = COMMAND_OUTPUT_RE.exec(result);
193
+ if (!match)
194
+ return null;
195
+ return { body: match[1] ?? '', tail: (match[2] ?? '').trim() };
196
+ }
197
+ /**
198
+ * TUI-C32 residual c — may this tool NAME be treated as shell-shaped at all? A registered entry
199
+ * must be explicitly {@link ToolDisplayEntry.shellShaped}; an unregistered/custom name falls back
200
+ * to shape-detection (custom toolkit tools own user-defined names yet share the result shape). A
201
+ * registered non-shell tool (`read_file`, `list_directory`, …) is therefore NEVER shell-parsed
202
+ * just because its result happens to contain the `<COMMAND_OUTPUT>` marker.
203
+ */
204
+ function nameAllowsShellShape(name) {
205
+ const entry = TOOL_DISPLAY_REGISTRY[name];
206
+ return entry ? entry.shellShaped === true : true;
207
+ }
208
+ /**
209
+ * TUI-C32 residual c — is THIS tool call genuinely a shell-shaped result (name allows it AND the
210
+ * result actually carries the `<COMMAND_OUTPUT>` shape)? Used by the plain surface to decide
211
+ * `liveOutputAlreadyShown` (the child streamed live via the tool-output channel's default sink),
212
+ * so a non-shell tool whose result merely quotes the marker no longer has its body suppressed.
213
+ */
214
+ export function isShellShapedResult(name, result) {
215
+ return nameAllowsShellShape(name) && parseCommandOutputResult(result) !== null;
216
+ }
217
+ /**
218
+ * Body formatter for shell-shaped calls: the child's output (preferring the LIVE streamed
219
+ * output — verbatim what the child printed — over the result's `<COMMAND_OUTPUT>` copy of it,
220
+ * which is the TUI-C17 dedupe) followed by the closing status line. When the live output
221
+ * already streamed raw to the terminal (plain surface), only the status tail is rendered so
222
+ * nothing the user just watched is repeated.
223
+ */
224
+ function formatShellBody(input) {
225
+ const parsed = parseCommandOutputResult(input.result);
226
+ // Not shell-shaped: only handle the still-running live-output case (a named run_* entry with
227
+ // no result yet). Anything with a non-shell result falls through to the generic rendering so
228
+ // e.g. a hardline refusal text is never dropped.
229
+ if (!parsed && (input.result || !input.output))
230
+ return null;
231
+ const lines = [];
232
+ if (!input.liveOutputAlreadyShown) {
233
+ // TUI-C32 residual d — the streamed live output is normally the verbatim, most-complete copy,
234
+ // so it is preferred (TUI-C17 dedupe). But if the live channel dropped straggler/tail chunks
235
+ // it can be a strict PREFIX of the result's `<COMMAND_OUTPUT>` copy (which the model always
236
+ // receives in full) — in that case fall back to the fuller copy so the expanded view can still
237
+ // recover what the stream missed. The common case (live === result, an independent/capped
238
+ // result, or an empty live output) is unchanged: only a genuine dropped-tail prefix overrides.
239
+ const live = input.output ?? '';
240
+ const resultBody = parsed?.body ?? '';
241
+ const body = resultBody.length > live.length && resultBody.startsWith(live) ? resultBody : live;
242
+ if (body.trim().length > 0)
243
+ lines.push(...styled(toLines(body), 'dim'));
244
+ }
245
+ if (parsed && parsed.tail.length > 0) {
246
+ lines.push(...styled(toLines(parsed.tail), 'dim'));
247
+ }
248
+ return lines.length > 0 ? lines : null;
249
+ }
250
+ /* ------------------------------------------------------------------------- *
251
+ * write_file / edit_file — diff-coloured rendering derived from the ARGS *
252
+ * ------------------------------------------------------------------------- */
253
+ /** `write_file` body: every content line is an ADDED diff line (there is no old content). */
254
+ function formatWriteFileBody(input, args) {
255
+ if (input.isError)
256
+ return null; // fall through: the error text is the story, not the diff
257
+ const content = args?.content;
258
+ if (typeof content !== 'string')
259
+ return null;
260
+ const lines = styled(toLines(content).map((l) => `+ ${l}`), 'added');
261
+ if (input.result && input.result.trim().length > 0) {
262
+ lines.push(...styled(toLines(input.result.trim()), 'dim'));
263
+ }
264
+ return lines;
265
+ }
266
+ /** `edit_file` body: per edit, the removed oldText lines then the added newText lines. */
267
+ function formatEditFileBody(input, args) {
268
+ if (input.isError)
269
+ return null; // fall through: show the recoverable error text instead
270
+ const edits = args?.edits;
271
+ if (!Array.isArray(edits) || edits.length === 0)
272
+ return null;
273
+ const lines = [];
274
+ let rendered = 0;
275
+ for (const raw of edits) {
276
+ const oldText = raw?.oldText;
277
+ const newText = raw?.newText;
278
+ if (typeof oldText !== 'string' || typeof newText !== 'string')
279
+ continue;
280
+ if (rendered > 0)
281
+ lines.push({ text: `${ELLIPSIS}`, style: 'dim' }); // hunk separator
282
+ rendered += 1;
283
+ lines.push(...styled(toLines(oldText).map((l) => `- ${l}`), 'removed'));
284
+ lines.push(...styled(toLines(newText).map((l) => `+ ${l}`), 'added'));
285
+ }
286
+ return rendered > 0 ? lines : null;
287
+ }
288
+ /* ------------------------------------------------------------------------- *
289
+ * The registry *
290
+ * ------------------------------------------------------------------------- */
291
+ /**
292
+ * Named entries. Anything not listed uses the generic fallback (all args summarised,
293
+ * output+result previewed dim) — plus the SHAPE-based shell formatter, which also covers
294
+ * user-named custom toolkit tools (they share the `<COMMAND_OUTPUT>` result shape).
295
+ */
296
+ const TOOL_DISPLAY_REGISTRY = {
297
+ read_file: { glyph: FILE_GLYPH, summariseArgs: ['path', 'offset', 'limit', 'head', 'tail'] },
298
+ read_multiple_files: { glyph: FILE_GLYPH, summariseArgs: ['paths'] },
299
+ gth_read_binary: { glyph: FILE_GLYPH, summariseArgs: ['path'] },
300
+ write_file: { glyph: FILE_GLYPH, summariseArgs: ['path'], formatBody: formatWriteFileBody },
301
+ edit_file: {
302
+ glyph: FILE_GLYPH,
303
+ summariseArgs: ['path', 'dryRun'],
304
+ formatBody: formatEditFileBody,
305
+ },
306
+ create_directory: { glyph: FILE_GLYPH },
307
+ list_directory: { glyph: FILE_GLYPH },
308
+ list_directory_with_sizes: { glyph: FILE_GLYPH },
309
+ directory_tree: { glyph: FILE_GLYPH },
310
+ move_file: { glyph: FILE_GLYPH },
311
+ search_files: { glyph: FILE_GLYPH },
312
+ get_file_info: { glyph: FILE_GLYPH },
313
+ delete_file: { glyph: FILE_GLYPH },
314
+ delete_directory: { glyph: FILE_GLYPH },
315
+ list_allowed_directories: { glyph: FILE_GLYPH },
316
+ run_shell_command: {
317
+ glyph: SHELL_GLYPH,
318
+ summariseArgs: ['command'],
319
+ formatBody: formatShellBody,
320
+ shellShaped: true,
321
+ },
322
+ run_tests: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
323
+ run_single_test: {
324
+ glyph: SHELL_GLYPH,
325
+ summariseArgs: ['testPath'],
326
+ formatBody: formatShellBody,
327
+ shellShaped: true,
328
+ },
329
+ run_lint: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
330
+ run_build: { glyph: SHELL_GLYPH, formatBody: formatShellBody, shellShaped: true },
331
+ task: { glyph: '🤖', summariseArgs: ['subagent_type', 'description'] },
332
+ };
333
+ /** The registry glyph for a tool name (generic `⚙` when unknown). */
334
+ export function getToolGlyph(name) {
335
+ return TOOL_DISPLAY_REGISTRY[name]?.glyph ?? FALLBACK_GLYPH;
336
+ }
337
+ /* ------------------------------------------------------------------------- *
338
+ * Params summary *
339
+ * ------------------------------------------------------------------------- */
340
+ /**
341
+ * One-line call summary: `name(arg=val, other=…)`. Key args only (per the registry entry, or
342
+ * all args for unknown tools), each value inlined + truncated, the whole parenthesised part
343
+ * capped at {@link TOOL_SUMMARY_MAX_CHARS}, and everything secret-redacted (literals +
344
+ * provider patterns). Unparsable (mid-stream/malformed) args render as `name(…)` — never a
345
+ * raw JSON dump. `secrets` defaults to the env-derived literals; pass explicitly for tests.
346
+ *
347
+ * Redaction runs BEFORE every truncation step (per value in {@link formatParamValue}, and again
348
+ * before the whole-summary cap): truncating first would bisect a literal secret longer than a
349
+ * cap so it no longer literal-matches, leaking its head into the rendered summary
350
+ * (fix-cycle-1 finding). The final pass over the assembled string is defense in depth only —
351
+ * `redactText` is idempotent, so re-redacting already-marked text is safe.
352
+ */
353
+ export function summariseToolCall(name, argsText, secrets = getDefaultSecrets()) {
354
+ const label = name || '(tool)';
355
+ const args = parseToolArgsSafe(argsText);
356
+ if (args === null) {
357
+ const hasRawArgs = !!argsText && argsText.trim().length > 0 && argsText.trim() !== '{}';
358
+ return hasRawArgs ? `${label}(${ELLIPSIS})` : `${label}()`;
359
+ }
360
+ const entry = TOOL_DISPLAY_REGISTRY[name];
361
+ const keys = entry?.summariseArgs !== undefined
362
+ ? entry.summariseArgs.filter((k) => args[k] !== undefined)
363
+ : Object.keys(args);
364
+ const parts = keys.map((k) => `${k}=${formatParamValue(args[k], secrets)}`);
365
+ // Anything parsed but not summarised (write_file's content, unlisted keys) is signalled with
366
+ // a trailing ellipsis so the summary never silently pretends to be the whole call.
367
+ const hasHiddenArgs = entry?.summariseArgs !== undefined && Object.keys(args).some((k) => !keys.includes(k));
368
+ if (hasHiddenArgs)
369
+ parts.push(ELLIPSIS);
370
+ // Redact before the whole-summary cap too, so this truncation can no more bisect a secret
371
+ // out of literal-matching than the per-value one can.
372
+ const inner = truncate(redactText(parts.join(', '), secrets), TOOL_SUMMARY_MAX_CHARS);
373
+ return redactText(`${label}(${inner})`, secrets);
374
+ }
375
+ /* ------------------------------------------------------------------------- *
376
+ * Body + preview *
377
+ * ------------------------------------------------------------------------- */
378
+ /**
379
+ * The FULL (uncapped) body lines for a call: the registry formatter when one applies, else the
380
+ * shape-based shell formatter, else the generic fallback (live output lines, then the final
381
+ * result — both dim). Every line is secret-redacted. Used by the TUI's EXPANDED panel; cap it
382
+ * with {@link capToolDisplayLines} for the collapsed preview.
383
+ */
384
+ export function buildToolBodyLines(input, secrets = getDefaultSecrets()) {
385
+ const args = parseToolArgsSafe(input.argsText);
386
+ const entry = TOOL_DISPLAY_REGISTRY[input.name];
387
+ let lines = entry?.formatBody?.(input, args) ?? null;
388
+ // TUI-C32 residual c — only fall back to the shape-based shell formatter for names that may be
389
+ // shell-shaped (flagged registry entries + unregistered custom tools), so a registered non-shell
390
+ // tool whose result merely contains `<COMMAND_OUTPUT>` is rendered generically, not shell-parsed.
391
+ if (lines === null && nameAllowsShellShape(input.name))
392
+ lines = formatShellBody(input);
393
+ if (lines === null) {
394
+ lines = [];
395
+ if (input.output && input.output.trim().length > 0 && !input.liveOutputAlreadyShown) {
396
+ lines.push(...styled(toLines(input.output), 'dim'));
397
+ }
398
+ if (input.result && input.result.trim().length > 0) {
399
+ lines.push(...styled(toLines(input.result), 'dim'));
400
+ }
401
+ }
402
+ return lines.map((l) => ({ ...l, text: redactText(l.text, secrets) }));
403
+ }
404
+ /**
405
+ * Apply the canonical render cap: at most `maxLines` lines (each char-capped at
406
+ * {@link TOOL_PREVIEW_LINE_MAX_CHARS} with `…`), plus a dim `… (+N more lines)` overflow
407
+ * marker when anything was cut. The marker line is IN ADDITION to the cap so exactly how much
408
+ * was hidden is always stated (DL-4 transparency).
409
+ */
410
+ export function capToolDisplayLines(lines, maxLines = TOOL_OUTPUT_PREVIEW_LINES) {
411
+ const capped = lines.slice(0, maxLines).map((l) => ({
412
+ ...l,
413
+ text: truncate(l.text, TOOL_PREVIEW_LINE_MAX_CHARS),
414
+ }));
415
+ const hidden = lines.length - capped.length;
416
+ if (hidden > 0) {
417
+ capped.push({
418
+ text: `${ELLIPSIS} (+${hidden} more line${hidden === 1 ? '' : 's'})`,
419
+ style: 'dim',
420
+ });
421
+ }
422
+ return capped;
423
+ }
424
+ /**
425
+ * The collapsed inline preview: {@link buildToolBodyLines} capped at the canonical
426
+ * {@link TOOL_OUTPUT_PREVIEW_LINES}.
427
+ */
428
+ export function buildToolPreviewLines(input, secrets = getDefaultSecrets()) {
429
+ return capToolDisplayLines(buildToolBodyLines(input, secrets));
430
+ }
431
+ /* ------------------------------------------------------------------------- *
432
+ * ANSI adapter (plain surface) *
433
+ * ------------------------------------------------------------------------- */
434
+ const ANSI_BY_STYLE = {
435
+ dim: '\x1b[2m',
436
+ added: '\x1b[32m',
437
+ removed: '\x1b[31m',
438
+ };
439
+ const ANSI_RESET = '\x1b[0m';
440
+ /**
441
+ * Render one styled line as a raw string for the plain surface. Each line is SELF-STYLED
442
+ * (its own SGR open + reset) so lines compose safely regardless of surrounding styling.
443
+ * With `colour` false the raw text is returned unchanged — the clean monochrome degradation
444
+ * for non-TTY/piped output (DL-7); diff lines still read via their `+`/`-` prefixes.
445
+ */
446
+ export function renderToolLineAnsi(line, colour) {
447
+ if (!colour)
448
+ return line.text;
449
+ return `${ANSI_BY_STYLE[line.style]}${line.text}${ANSI_RESET}`;
450
+ }
451
+ //# sourceMappingURL=toolDisplay.js.map