@mariozechner/pi-coding-agent 0.37.8 → 0.39.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 (165) hide show
  1. package/CHANGELOG.md +115 -4
  2. package/README.md +11 -0
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +8 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +75 -35
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +3 -3
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js +1 -1
  18. package/dist/core/extensions/index.js.map +1 -1
  19. package/dist/core/extensions/loader.d.ts +8 -6
  20. package/dist/core/extensions/loader.d.ts.map +1 -1
  21. package/dist/core/extensions/loader.js +94 -211
  22. package/dist/core/extensions/loader.js.map +1 -1
  23. package/dist/core/extensions/runner.d.ts +27 -30
  24. package/dist/core/extensions/runner.d.ts.map +1 -1
  25. package/dist/core/extensions/runner.js +102 -45
  26. package/dist/core/extensions/runner.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +155 -30
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/extensions/wrapper.d.ts +5 -3
  31. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  32. package/dist/core/extensions/wrapper.js +6 -4
  33. package/dist/core/extensions/wrapper.js.map +1 -1
  34. package/dist/core/index.d.ts +2 -2
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +1 -1
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/model-resolver.d.ts +4 -2
  39. package/dist/core/model-resolver.d.ts.map +1 -1
  40. package/dist/core/model-resolver.js +8 -9
  41. package/dist/core/model-resolver.js.map +1 -1
  42. package/dist/core/sdk.d.ts +8 -5
  43. package/dist/core/sdk.d.ts.map +1 -1
  44. package/dist/core/sdk.js +39 -87
  45. package/dist/core/sdk.js.map +1 -1
  46. package/dist/core/settings-manager.d.ts +8 -0
  47. package/dist/core/settings-manager.d.ts.map +1 -1
  48. package/dist/core/settings-manager.js +9 -1
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.d.ts.map +1 -1
  51. package/dist/core/system-prompt.js +1 -5
  52. package/dist/core/system-prompt.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +25 -1
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +103 -73
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/edit.d.ts +17 -1
  58. package/dist/core/tools/edit.d.ts.map +1 -1
  59. package/dist/core/tools/edit.js +12 -5
  60. package/dist/core/tools/edit.js.map +1 -1
  61. package/dist/core/tools/find.d.ts +18 -1
  62. package/dist/core/tools/find.d.ts.map +1 -1
  63. package/dist/core/tools/find.js +68 -18
  64. package/dist/core/tools/find.js.map +1 -1
  65. package/dist/core/tools/grep.d.ts +15 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +22 -10
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/index.d.ts +7 -7
  70. package/dist/core/tools/index.d.ts.map +1 -1
  71. package/dist/core/tools/index.js +1 -1
  72. package/dist/core/tools/index.js.map +1 -1
  73. package/dist/core/tools/ls.d.ts +21 -1
  74. package/dist/core/tools/ls.d.ts.map +1 -1
  75. package/dist/core/tools/ls.js +80 -72
  76. package/dist/core/tools/ls.js.map +1 -1
  77. package/dist/core/tools/read.d.ts +14 -0
  78. package/dist/core/tools/read.d.ts.map +1 -1
  79. package/dist/core/tools/read.js +12 -5
  80. package/dist/core/tools/read.js.map +1 -1
  81. package/dist/core/tools/write.d.ts +15 -1
  82. package/dist/core/tools/write.d.ts.map +1 -1
  83. package/dist/core/tools/write.js +9 -4
  84. package/dist/core/tools/write.js.map +1 -1
  85. package/dist/index.d.ts +5 -4
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -2
  88. package/dist/index.js.map +1 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +58 -116
  91. package/dist/main.js.map +1 -1
  92. package/dist/modes/index.d.ts +2 -2
  93. package/dist/modes/index.d.ts.map +1 -1
  94. package/dist/modes/index.js.map +1 -1
  95. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/assistant-message.js +7 -3
  97. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  98. package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  99. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
  100. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  101. package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
  102. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  103. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  105. package/dist/modes/interactive/components/extension-input.d.ts +10 -2
  106. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/extension-input.js +18 -14
  108. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
  110. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/extension-selector.js +18 -22
  112. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  114. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/tool-execution.js +50 -23
  116. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  117. package/dist/modes/interactive/interactive-mode.d.ts +44 -3
  118. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/dist/modes/interactive/interactive-mode.js +440 -139
  120. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  122. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  123. package/dist/modes/interactive/theme/theme.js +34 -0
  124. package/dist/modes/interactive/theme/theme.js.map +1 -1
  125. package/dist/modes/print-mode.d.ts +14 -7
  126. package/dist/modes/print-mode.d.ts.map +1 -1
  127. package/dist/modes/print-mode.js +45 -21
  128. package/dist/modes/print-mode.js.map +1 -1
  129. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-mode.js +111 -101
  131. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  132. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  133. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-types.js.map +1 -1
  135. package/dist/utils/clipboard-image.d.ts.map +1 -1
  136. package/dist/utils/clipboard-image.js +1 -1
  137. package/dist/utils/clipboard-image.js.map +1 -1
  138. package/dist/utils/clipboard.d.ts.map +1 -1
  139. package/dist/utils/clipboard.js +35 -7
  140. package/dist/utils/clipboard.js.map +1 -1
  141. package/docs/extensions.md +211 -15
  142. package/docs/sdk.md +68 -9
  143. package/docs/tui.md +81 -4
  144. package/examples/extensions/README.md +3 -0
  145. package/examples/extensions/claude-rules.ts +5 -2
  146. package/examples/extensions/handoff.ts +1 -1
  147. package/examples/extensions/interactive-shell.ts +196 -0
  148. package/examples/extensions/mac-system-theme.ts +25 -0
  149. package/examples/extensions/modal-editor.ts +85 -0
  150. package/examples/extensions/overlay-test.ts +145 -0
  151. package/examples/extensions/pirate.ts +7 -4
  152. package/examples/extensions/preset.ts +3 -3
  153. package/examples/extensions/qna.ts +1 -1
  154. package/examples/extensions/rainbow-editor.ts +95 -0
  155. package/examples/extensions/shutdown-command.ts +63 -0
  156. package/examples/extensions/snake.ts +1 -1
  157. package/examples/extensions/ssh.ts +220 -0
  158. package/examples/extensions/timed-confirm.ts +32 -25
  159. package/examples/extensions/todo.ts +1 -1
  160. package/examples/extensions/tool-override.ts +143 -0
  161. package/examples/extensions/tools.ts +1 -1
  162. package/examples/extensions/with-deps/package-lock.json +2 -2
  163. package/examples/extensions/with-deps/package.json +1 -1
  164. package/examples/sdk/04-skills.ts +4 -1
  165. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAc,MAAM,sBAAsB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElF;;GAEG;AACH,qBAAa,YAAa,SAAQ,MAAM;IACvC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,cAAc,CAAyC;IAGxD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,2EAA2E;IACpE,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAEvD,YAAY,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAG9D;IAED;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAErD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgD9B;CACD","sourcesContent":["import { Editor, type EditorTheme, matchesKey } from \"@mariozechner/pi-tui\";\nimport type { AppAction, KeybindingsManager } from \"../../../core/keybindings.js\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tprivate actionHandlers: Map<AppAction, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\t/** Handler for extension-registered shortcuts. Returns true if handled. */\n\tpublic onExtensionShortcut?: (data: string) => boolean;\n\n\tconstructor(theme: EditorTheme, keybindings: KeybindingsManager) {\n\t\tsuper(theme);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppAction, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check extension-registered shortcuts first\n\t\tif (this.onExtensionShortcut?.(data)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for Ctrl+V to handle clipboard image paste\n\t\tif (matchesKey(data, \"ctrl+v\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t}\n\t\t\treturn; // Always consume\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"interrupt\" && action !== \"exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAc,MAAM,sBAAsB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElF;;GAEG;AACH,qBAAa,YAAa,SAAQ,MAAM;IACvC,OAAO,CAAC,WAAW,CAAqB;IACjC,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,CAAa;IAGvD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,2EAA2E;IACpE,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAEvD,YAAY,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAG9D;IAED;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAErD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgD9B;CACD","sourcesContent":["import { Editor, type EditorTheme, matchesKey } from \"@mariozechner/pi-tui\";\nimport type { AppAction, KeybindingsManager } from \"../../../core/keybindings.js\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppAction, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\t/** Handler for extension-registered shortcuts. Returns true if handled. */\n\tpublic onExtensionShortcut?: (data: string) => boolean;\n\n\tconstructor(theme: EditorTheme, keybindings: KeybindingsManager) {\n\t\tsuper(theme);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppAction, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check extension-registered shortcuts first\n\t\tif (this.onExtensionShortcut?.(data)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for Ctrl+V to handle clipboard image paste\n\t\tif (matchesKey(data, \"ctrl+v\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t}\n\t\t\treturn; // Always consume\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"interrupt\" && action !== \"exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAG5E;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAC/B,WAAW,CAAqB;IAChC,cAAc,GAA+B,IAAI,GAAG,EAAE,CAAC;IAE/D,oDAAoD;IAC7C,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,YAAY,CAAc;IACjC,2EAA2E;IACpE,mBAAmB,CAA6B;IAEvD,YAAY,KAAkB,EAAE,WAA+B,EAAE;QAChE,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAAA,CAC/B;IAED;;OAEG;IACH,QAAQ,CAAC,MAAiB,EAAE,OAAmB,EAAQ;QACtD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAA,CACzC;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,6CAA6C;QAC7C,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,OAAO;QACR,CAAC;QAED,mDAAmD;QACnD,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,8BAA8B;QAE9B,wDAAwD;QACxD,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;gBACnC,4DAA4D;gBAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACtE,IAAI,OAAO,EAAE,CAAC;oBACb,OAAO,EAAE,CAAC;oBACV,OAAO;gBACR,CAAC;YACF,CAAC;YACD,yDAAyD;YACzD,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,4CAA4C;QAC5C,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YAC5C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAChE,IAAI,OAAO;oBAAE,OAAO,EAAE,CAAC;YACxB,CAAC;YACD,OAAO,CAAC,iBAAiB;QAC1B,CAAC;QAED,8BAA8B;QAC9B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACrD,IAAI,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;gBAC3F,OAAO,EAAE,CAAC;gBACV,OAAO;YACR,CAAC;QACF,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import { Editor, type EditorTheme, matchesKey } from \"@mariozechner/pi-tui\";\nimport type { AppAction, KeybindingsManager } from \"../../../core/keybindings.js\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tprivate actionHandlers: Map<AppAction, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\t/** Handler for extension-registered shortcuts. Returns true if handled. */\n\tpublic onExtensionShortcut?: (data: string) => boolean;\n\n\tconstructor(theme: EditorTheme, keybindings: KeybindingsManager) {\n\t\tsuper(theme);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppAction, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check extension-registered shortcuts first\n\t\tif (this.onExtensionShortcut?.(data)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for Ctrl+V to handle clipboard image paste\n\t\tif (matchesKey(data, \"ctrl+v\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t}\n\t\t\treturn; // Always consume\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"interrupt\" && action !== \"exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAG5E;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAC/B,WAAW,CAAqB;IACjC,cAAc,GAA+B,IAAI,GAAG,EAAE,CAAC;IAE9D,oDAAoD;IAC7C,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,YAAY,CAAc;IACjC,2EAA2E;IACpE,mBAAmB,CAA6B;IAEvD,YAAY,KAAkB,EAAE,WAA+B,EAAE;QAChE,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAAA,CAC/B;IAED;;OAEG;IACH,QAAQ,CAAC,MAAiB,EAAE,OAAmB,EAAQ;QACtD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAA,CACzC;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,6CAA6C;QAC7C,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,OAAO;QACR,CAAC;QAED,mDAAmD;QACnD,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,8BAA8B;QAE9B,wDAAwD;QACxD,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;gBACnC,4DAA4D;gBAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACtE,IAAI,OAAO,EAAE,CAAC;oBACb,OAAO,EAAE,CAAC;oBACV,OAAO;gBACR,CAAC;YACF,CAAC;YACD,yDAAyD;YACzD,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,4CAA4C;QAC5C,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YAC5C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAChE,IAAI,OAAO;oBAAE,OAAO,EAAE,CAAC;YACxB,CAAC;YACD,OAAO,CAAC,iBAAiB;QAC1B,CAAC;QAED,8BAA8B;QAC9B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACrD,IAAI,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;gBAC3F,OAAO,EAAE,CAAC;gBACV,OAAO;YACR,CAAC;QACF,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import { Editor, type EditorTheme, matchesKey } from \"@mariozechner/pi-tui\";\nimport type { AppAction, KeybindingsManager } from \"../../../core/keybindings.js\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppAction, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\t/** Handler for extension-registered shortcuts. Returns true if handled. */\n\tpublic onExtensionShortcut?: (data: string) => boolean;\n\n\tconstructor(theme: EditorTheme, keybindings: KeybindingsManager) {\n\t\tsuper(theme);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppAction, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check extension-registered shortcuts first\n\t\tif (this.onExtensionShortcut?.(data)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for Ctrl+V to handle clipboard image paste\n\t\tif (matchesKey(data, \"ctrl+v\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t}\n\t\t\treturn; // Always consume\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"interrupt\" && action !== \"exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
@@ -1,12 +1,20 @@
1
1
  /**
2
2
  * Simple text input component for extensions.
3
3
  */
4
- import { Container } from "@mariozechner/pi-tui";
4
+ import { Container, type TUI } from "@mariozechner/pi-tui";
5
+ export interface ExtensionInputOptions {
6
+ tui?: TUI;
7
+ timeout?: number;
8
+ }
5
9
  export declare class ExtensionInputComponent extends Container {
6
10
  private input;
7
11
  private onSubmitCallback;
8
12
  private onCancelCallback;
9
- constructor(title: string, _placeholder: string | undefined, onSubmit: (value: string) => void, onCancel: () => void);
13
+ private titleText;
14
+ private baseTitle;
15
+ private countdown;
16
+ constructor(title: string, _placeholder: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, opts?: ExtensionInputOptions);
10
17
  handleInput(keyData: string): void;
18
+ dispose(): void;
11
19
  }
12
20
  //# sourceMappingURL=extension-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension-input.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-input.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAA6C,MAAM,sBAAsB,CAAC;AAI5F,qBAAa,uBAAwB,SAAQ,SAAS;IACrD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YACC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,QAAQ,EAAE,MAAM,IAAI,EA4BpB;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAgBjC;CACD","sourcesContent":["/**\n * Simple text input component for extensions.\n */\n\nimport { Container, getEditorKeybindings, Input, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport class ExtensionInputComponent extends Container {\n\tprivate input: Input;\n\tprivate onSubmitCallback: (value: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\ttitle: string,\n\t\t_placeholder: string | undefined,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create input\n\t\tthis.input = new Input();\n\t\tthis.addChild(this.input);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"enter submit esc cancel\"), 1, 0));\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Enter\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tthis.onSubmitCallback(this.input.getValue());\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape or Ctrl+C to cancel\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\n\t\t// Forward to input\n\t\tthis.input.handleInput(keyData);\n\t}\n}\n"]}
1
+ {"version":3,"file":"extension-input.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-input.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAA6C,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAKtG,MAAM,WAAW,qBAAqB;IACrC,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,uBAAwB,SAAQ,SAAS;IACrD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAA6B;IAE9C,YACC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,QAAQ,EAAE,MAAM,IAAI,EACpB,IAAI,CAAC,EAAE,qBAAqB,EA8B5B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CASjC;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * Simple text input component for extensions.\n */\n\nimport { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport interface ExtensionInputOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class ExtensionInputComponent extends Container {\n\tprivate input: Input;\n\tprivate onSubmitCallback: (value: string) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate countdown: CountdownTimer | undefined;\n\n\tconstructor(\n\t\ttitle: string,\n\t\t_placeholder: string | undefined,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t\topts?: ExtensionInputOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.baseTitle = title;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", title), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", `${this.baseTitle} (${s}s)`)),\n\t\t\t\t() => this.onCancelCallback(),\n\t\t\t);\n\t\t}\n\n\t\tthis.input = new Input();\n\t\tthis.addChild(this.input);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"enter submit esc cancel\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tthis.onSubmitCallback(this.input.getValue());\n\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t} else {\n\t\t\tthis.input.handleInput(keyData);\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
@@ -3,45 +3,49 @@
3
3
  */
4
4
  import { Container, getEditorKeybindings, Input, Spacer, Text } from "@mariozechner/pi-tui";
5
5
  import { theme } from "../theme/theme.js";
6
+ import { CountdownTimer } from "./countdown-timer.js";
6
7
  import { DynamicBorder } from "./dynamic-border.js";
7
8
  export class ExtensionInputComponent extends Container {
8
9
  input;
9
10
  onSubmitCallback;
10
11
  onCancelCallback;
11
- constructor(title, _placeholder, onSubmit, onCancel) {
12
+ titleText;
13
+ baseTitle;
14
+ countdown;
15
+ constructor(title, _placeholder, onSubmit, onCancel, opts) {
12
16
  super();
13
17
  this.onSubmitCallback = onSubmit;
14
18
  this.onCancelCallback = onCancel;
15
- // Add top border
19
+ this.baseTitle = title;
16
20
  this.addChild(new DynamicBorder());
17
21
  this.addChild(new Spacer(1));
18
- // Add title
19
- this.addChild(new Text(theme.fg("accent", title), 1, 0));
22
+ this.titleText = new Text(theme.fg("accent", title), 1, 0);
23
+ this.addChild(this.titleText);
20
24
  this.addChild(new Spacer(1));
21
- // Create input
25
+ if (opts?.timeout && opts.timeout > 0 && opts.tui) {
26
+ this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback());
27
+ }
22
28
  this.input = new Input();
23
29
  this.addChild(this.input);
24
30
  this.addChild(new Spacer(1));
25
- // Add hint
26
31
  this.addChild(new Text(theme.fg("dim", "enter submit esc cancel"), 1, 0));
27
32
  this.addChild(new Spacer(1));
28
- // Add bottom border
29
33
  this.addChild(new DynamicBorder());
30
34
  }
31
35
  handleInput(keyData) {
32
36
  const kb = getEditorKeybindings();
33
- // Enter
34
37
  if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
35
38
  this.onSubmitCallback(this.input.getValue());
36
- return;
37
39
  }
38
- // Escape or Ctrl+C to cancel
39
- if (kb.matches(keyData, "selectCancel")) {
40
+ else if (kb.matches(keyData, "selectCancel")) {
40
41
  this.onCancelCallback();
41
- return;
42
42
  }
43
- // Forward to input
44
- this.input.handleInput(keyData);
43
+ else {
44
+ this.input.handleInput(keyData);
45
+ }
46
+ }
47
+ dispose() {
48
+ this.countdown?.dispose();
45
49
  }
46
50
  }
47
51
  //# sourceMappingURL=extension-input.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension-input.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-input.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5F,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,OAAO,uBAAwB,SAAQ,SAAS;IAC7C,KAAK,CAAQ;IACb,gBAAgB,CAA0B;IAC1C,gBAAgB,CAAa;IAErC,YACC,KAAa,EACb,YAAgC,EAChC,QAAiC,EACjC,QAAoB,EACnB;QACD,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QACjC,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QAEjC,iBAAiB;QACjB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,YAAY;QACZ,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzD,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,eAAe;QACf,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE1B,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,WAAW;QACX,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE3E,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,oBAAoB;QACpB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;IAAA,CACnC;IAED,WAAW,CAAC,OAAe,EAAQ;QAClC,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QAClC,QAAQ;QACR,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,eAAe,CAAC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YAC9D,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7C,OAAO;QACR,CAAC;QAED,6BAA6B;QAC7B,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxB,OAAO;QACR,CAAC;QAED,mBAAmB;QACnB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAAA,CAChC;CACD","sourcesContent":["/**\n * Simple text input component for extensions.\n */\n\nimport { Container, getEditorKeybindings, Input, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport class ExtensionInputComponent extends Container {\n\tprivate input: Input;\n\tprivate onSubmitCallback: (value: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(\n\t\ttitle: string,\n\t\t_placeholder: string | undefined,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create input\n\t\tthis.input = new Input();\n\t\tthis.addChild(this.input);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"enter submit esc cancel\"), 1, 0));\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Enter\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tthis.onSubmitCallback(this.input.getValue());\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape or Ctrl+C to cancel\n\t\tif (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\n\t\t// Forward to input\n\t\tthis.input.handleInput(keyData);\n\t}\n}\n"]}
1
+ {"version":3,"file":"extension-input.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-input.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Simple text input component for extensions.\n */\n\nimport { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport interface ExtensionInputOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class ExtensionInputComponent extends Container {\n\tprivate input: Input;\n\tprivate onSubmitCallback: (value: string) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate countdown: CountdownTimer | undefined;\n\n\tconstructor(\n\t\ttitle: string,\n\t\t_placeholder: string | undefined,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t\topts?: ExtensionInputOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.baseTitle = title;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", title), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", `${this.baseTitle} (${s}s)`)),\n\t\t\t\t() => this.onCancelCallback(),\n\t\t\t);\n\t\t}\n\n\t\tthis.input = new Input();\n\t\tthis.addChild(this.input);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"enter submit esc cancel\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tthis.onSubmitCallback(this.input.getValue());\n\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t} else {\n\t\t\tthis.input.handleInput(keyData);\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
@@ -2,15 +2,23 @@
2
2
  * Generic selector component for extensions.
3
3
  * Displays a list of string options with keyboard navigation.
4
4
  */
5
- import { Container } from "@mariozechner/pi-tui";
5
+ import { Container, type TUI } from "@mariozechner/pi-tui";
6
+ export interface ExtensionSelectorOptions {
7
+ tui?: TUI;
8
+ timeout?: number;
9
+ }
6
10
  export declare class ExtensionSelectorComponent extends Container {
7
11
  private options;
8
12
  private selectedIndex;
9
13
  private listContainer;
10
14
  private onSelectCallback;
11
15
  private onCancelCallback;
12
- constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void);
16
+ private titleText;
17
+ private baseTitle;
18
+ private countdown;
19
+ constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void, opts?: ExtensionSelectorOptions);
13
20
  private updateList;
14
21
  handleInput(keyData: string): void;
22
+ dispose(): void;
15
23
  }
16
24
  //# sourceMappingURL=extension-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-selector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAsC,MAAM,sBAAsB,CAAC;AAIrF,qBAAa,0BAA2B,SAAQ,SAAS;IACxD,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YAAY,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA+BrG;IAED,OAAO,CAAC,UAAU;IAkBlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBjC;CACD","sourcesContent":["/**\n * Generic selector component for extensions.\n * Displays a list of string options with keyboard navigation.\n */\n\nimport { Container, getEditorKeybindings, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport class ExtensionSelectorComponent extends Container {\n\tprivate options: string[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate onSelectCallback: (option: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.options = options;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate enter select esc cancel\"), 1, 0));\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst option = this.options[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\tlet text = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\ttext = theme.fg(\"accent\", \"→ \") + theme.fg(\"accent\", option);\n\t\t\t} else {\n\t\t\t\ttext = ` ${theme.fg(\"text\", option)}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(text, 1, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow or k\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow or j\n\t\telse if (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tconst selected = this.options[this.selectedIndex];\n\t\t\tif (selected) {\n\t\t\t\tthis.onSelectCallback(selected);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"extension-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-selector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAsC,KAAK,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAK/F,MAAM,WAAW,wBAAwB;IACxC,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,0BAA2B,SAAQ,SAAS;IACxD,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAA6B;IAE9C,YACC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAClC,QAAQ,EAAE,MAAM,IAAI,EACpB,IAAI,CAAC,EAAE,wBAAwB,EAiC/B;IAED,OAAO,CAAC,UAAU;IAWlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAcjC;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * Generic selector component for extensions.\n * Displays a list of string options with keyboard navigation.\n */\n\nimport { Container, getEditorKeybindings, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport interface ExtensionSelectorOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class ExtensionSelectorComponent extends Container {\n\tprivate options: string[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate onSelectCallback: (option: string) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate countdown: CountdownTimer | undefined;\n\n\tconstructor(\n\t\ttitle: string,\n\t\toptions: string[],\n\t\tonSelect: (option: string) => void,\n\t\tonCancel: () => void,\n\t\topts?: ExtensionSelectorOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.options = options;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.baseTitle = title;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", title), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", `${this.baseTitle} (${s}s)`)),\n\t\t\t\t() => this.onCancelCallback(),\n\t\t\t);\n\t\t}\n\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate enter select esc cancel\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst text = isSelected\n\t\t\t\t? theme.fg(\"accent\", \"→ \") + theme.fg(\"accent\", this.options[i])\n\t\t\t\t: ` ${theme.fg(\"text\", this.options[i])}`;\n\t\t\tthis.listContainer.addChild(new Text(text, 1, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t} else if (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t} else if (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tconst selected = this.options[this.selectedIndex];\n\t\t\tif (selected) this.onSelectCallback(selected);\n\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { Container, getEditorKeybindings, Spacer, Text } from "@mariozechner/pi-tui";
6
6
  import { theme } from "../theme/theme.js";
7
+ import { CountdownTimer } from "./countdown-timer.js";
7
8
  import { DynamicBorder } from "./dynamic-border.js";
8
9
  export class ExtensionSelectorComponent extends Container {
9
10
  options;
@@ -11,67 +12,62 @@ export class ExtensionSelectorComponent extends Container {
11
12
  listContainer;
12
13
  onSelectCallback;
13
14
  onCancelCallback;
14
- constructor(title, options, onSelect, onCancel) {
15
+ titleText;
16
+ baseTitle;
17
+ countdown;
18
+ constructor(title, options, onSelect, onCancel, opts) {
15
19
  super();
16
20
  this.options = options;
17
21
  this.onSelectCallback = onSelect;
18
22
  this.onCancelCallback = onCancel;
19
- // Add top border
23
+ this.baseTitle = title;
20
24
  this.addChild(new DynamicBorder());
21
25
  this.addChild(new Spacer(1));
22
- // Add title
23
- this.addChild(new Text(theme.fg("accent", title), 1, 0));
26
+ this.titleText = new Text(theme.fg("accent", title), 1, 0);
27
+ this.addChild(this.titleText);
24
28
  this.addChild(new Spacer(1));
25
- // Create list container
29
+ if (opts?.timeout && opts.timeout > 0 && opts.tui) {
30
+ this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback());
31
+ }
26
32
  this.listContainer = new Container();
27
33
  this.addChild(this.listContainer);
28
34
  this.addChild(new Spacer(1));
29
- // Add hint
30
35
  this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0));
31
36
  this.addChild(new Spacer(1));
32
- // Add bottom border
33
37
  this.addChild(new DynamicBorder());
34
- // Initial render
35
38
  this.updateList();
36
39
  }
37
40
  updateList() {
38
41
  this.listContainer.clear();
39
42
  for (let i = 0; i < this.options.length; i++) {
40
- const option = this.options[i];
41
43
  const isSelected = i === this.selectedIndex;
42
- let text = "";
43
- if (isSelected) {
44
- text = theme.fg("accent", "→ ") + theme.fg("accent", option);
45
- }
46
- else {
47
- text = ` ${theme.fg("text", option)}`;
48
- }
44
+ const text = isSelected
45
+ ? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i])
46
+ : ` ${theme.fg("text", this.options[i])}`;
49
47
  this.listContainer.addChild(new Text(text, 1, 0));
50
48
  }
51
49
  }
52
50
  handleInput(keyData) {
53
51
  const kb = getEditorKeybindings();
54
- // Up arrow or k
55
52
  if (kb.matches(keyData, "selectUp") || keyData === "k") {
56
53
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
57
54
  this.updateList();
58
55
  }
59
- // Down arrow or j
60
56
  else if (kb.matches(keyData, "selectDown") || keyData === "j") {
61
57
  this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
62
58
  this.updateList();
63
59
  }
64
- // Enter
65
60
  else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
66
61
  const selected = this.options[this.selectedIndex];
67
- if (selected) {
62
+ if (selected)
68
63
  this.onSelectCallback(selected);
69
- }
70
64
  }
71
- // Escape or Ctrl+C
72
65
  else if (kb.matches(keyData, "selectCancel")) {
73
66
  this.onCancelCallback();
74
67
  }
75
68
  }
69
+ dispose() {
70
+ this.countdown?.dispose();
71
+ }
76
72
  }
77
73
  //# sourceMappingURL=extension-selector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-selector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generic selector component for extensions.\n * Displays a list of string options with keyboard navigation.\n */\n\nimport { Container, getEditorKeybindings, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport class ExtensionSelectorComponent extends Container {\n\tprivate options: string[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate onSelectCallback: (option: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.options = options;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate enter select esc cancel\"), 1, 0));\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst option = this.options[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\tlet text = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\ttext = theme.fg(\"accent\", \"→ \") + theme.fg(\"accent\", option);\n\t\t\t} else {\n\t\t\t\ttext = ` ${theme.fg(\"text\", option)}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(text, 1, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\t// Up arrow or k\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow or j\n\t\telse if (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tconst selected = this.options[this.selectedIndex];\n\t\t\tif (selected) {\n\t\t\t\tthis.onSelectCallback(selected);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"extension-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/extension-selector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generic selector component for extensions.\n * Displays a list of string options with keyboard navigation.\n */\n\nimport { Container, getEditorKeybindings, Spacer, Text, type TUI } from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nexport interface ExtensionSelectorOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class ExtensionSelectorComponent extends Container {\n\tprivate options: string[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate onSelectCallback: (option: string) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate countdown: CountdownTimer | undefined;\n\n\tconstructor(\n\t\ttitle: string,\n\t\toptions: string[],\n\t\tonSelect: (option: string) => void,\n\t\tonCancel: () => void,\n\t\topts?: ExtensionSelectorOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.options = options;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.baseTitle = title;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", title), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", `${this.baseTitle} (${s}s)`)),\n\t\t\t\t() => this.onCancelCallback(),\n\t\t\t);\n\t\t}\n\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate enter select esc cancel\"), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst text = isSelected\n\t\t\t\t? theme.fg(\"accent\", \"→ \") + theme.fg(\"accent\", this.options[i])\n\t\t\t\t: ` ${theme.fg(\"text\", this.options[i])}`;\n\t\t\tthis.listContainer.addChild(new Text(text, 1, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"selectUp\") || keyData === \"k\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t} else if (kb.matches(keyData, \"selectDown\") || keyData === \"j\") {\n\t\t\tthis.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t} else if (kb.matches(keyData, \"selectConfirm\") || keyData === \"\\n\") {\n\t\t\tconst selected = this.options[this.selectedIndex];\n\t\t\tif (selected) this.onSelectCallback(selected);\n\t\t} else if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
@@ -24,6 +24,12 @@ export declare class ToolExecutionComponent extends Container {
24
24
  private editDiffArgsKey?;
25
25
  private convertedImages;
26
26
  constructor(toolName: string, args: any, options: ToolExecutionOptions | undefined, toolDefinition: ToolDefinition | undefined, ui: TUI, cwd?: string);
27
+ /**
28
+ * Check if we should use built-in rendering for this tool.
29
+ * Returns true if the tool name is a built-in AND either there's no toolDefinition
30
+ * or the toolDefinition doesn't provide custom renderers.
31
+ */
32
+ private shouldUseBuiltInRenderer;
27
33
  updateArgs(args: any): void;
28
34
  /**
29
35
  * Signal that args are complete (tool is about to execute).
@@ -1 +1 @@
1
- {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,SAAS,EAOT,KAAK,GAAG,EACR,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AA8BxE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAIb;IAEF,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,eAAe,CAAC,CAAS;IAEjC,OAAO,CAAC,eAAe,CAA8D;IAErF,YACC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,OAAO,kCAA2B,EAClC,cAAc,EAAE,cAAc,GAAG,SAAS,EAC1C,EAAE,EAAE,GAAG,EACP,GAAG,GAAE,MAAsB,EAuB3B;IAED,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAG1B;IAED;;;OAGG;IACH,eAAe,IAAI,IAAI,CAEtB;IAED;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA6B5B,YAAY,CACX,MAAM,EAAE;QACP,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClF,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KACjB,EACD,SAAS,UAAQ,GACf,IAAI,CAMN;IAED;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IA2BlC,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,OAAO,CAAC,aAAa;IA6GrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkEzB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,mBAAmB;CA6Q3B","sourcesContent":["import * as os from \"node:os\";\nimport {\n\tBox,\n\tContainer,\n\tgetCapabilities,\n\tgetImageDimensions,\n\tImage,\n\timageFallback,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport stripAnsi from \"strip-ansi\";\nimport type { ToolDefinition } from \"../../../core/extensions/types.js\";\nimport { computeEditDiff, type EditDiffError, type EditDiffResult } from \"../../../core/tools/edit-diff.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"../../../core/tools/truncate.js\";\nimport { convertToPng } from \"../../../utils/image-convert.js\";\nimport { sanitizeBinaryOutput } from \"../../../utils/shell.js\";\nimport { getLanguageFromPath, highlightCode, theme } from \"../theme/theme.js\";\nimport { renderDiff } from \"./diff.js\";\nimport { truncateToVisualLines } from \"./visual-truncate.js\";\n\n// Preview line limit for bash when not expanded\nconst BASH_PREVIEW_LINES = 5;\n\n/**\n * Convert absolute path to tilde notation if it's in home directory\n */\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\n/**\n * Replace tabs with spaces for consistent rendering\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean; // default: true (only used if terminal supports images)\n}\n\n/**\n * Component that renders a tool call with its result (updateable)\n */\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box; // Used for custom tools and bash visual truncation\n\tprivate contentText: Text; // For built-in tools (with its own padding/bg)\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\t// Cached edit diff preview (computed when args arrive, before tool executes)\n\tprivate editDiffPreview?: EditDiffResult | EditDiffError;\n\tprivate editDiffArgsKey?: string; // Track which args the preview is for\n\t// Cached converted images for Kitty protocol (which requires PNG), keyed by index\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\n\tconstructor(\n\t\ttoolName: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition | undefined,\n\t\tui: TUI,\n\t\tcwd: string = process.cwd(),\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create both - contentBox for custom tools/bash, contentText for other built-ins\n\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\n\t\tif (toolDefinition || toolName === \"bash\") {\n\t\t\tthis.addChild(this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * Signal that args are complete (tool is about to execute).\n\t * This triggers diff computation for edit tool.\n\t */\n\tsetArgsComplete(): void {\n\t\tthis.maybeComputeEditDiff();\n\t}\n\n\t/**\n\t * Compute edit diff preview when we have complete args.\n\t * This runs async and updates display when done.\n\t */\n\tprivate maybeComputeEditDiff(): void {\n\t\tif (this.toolName !== \"edit\") return;\n\n\t\tconst path = this.args?.path;\n\t\tconst oldText = this.args?.oldText;\n\t\tconst newText = this.args?.newText;\n\n\t\t// Need all three params to compute diff\n\t\tif (!path || oldText === undefined || newText === undefined) return;\n\n\t\t// Create a key to track which args this computation is for\n\t\tconst argsKey = JSON.stringify({ path, oldText, newText });\n\n\t\t// Skip if we already computed for these exact args\n\t\tif (this.editDiffArgsKey === argsKey) return;\n\n\t\tthis.editDiffArgsKey = argsKey;\n\n\t\t// Compute diff async\n\t\tcomputeEditDiff(path, oldText, newText, this.cwd).then((result) => {\n\t\t\t// Only update if args haven't changed since we started\n\t\t\tif (this.editDiffArgsKey === argsKey) {\n\t\t\t\tthis.editDiffPreview = result;\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t});\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t\t// Convert non-PNG images to PNG for Kitty protocol (async)\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\t/**\n\t * Convert non-PNG images to PNG for Kitty graphics protocol.\n\t * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.\n\t */\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\t// Only needed for Kitty protocol\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\t// Skip if already PNG or already converted\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\t// Convert async\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Set background based on state\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\t// Check for custom tool rendering\n\t\tif (this.toolDefinition) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\tthis.contentBox.clear();\n\n\t\t\t// Render call component\n\t\t\tif (this.toolDefinition.renderCall) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callComponent = this.toolDefinition.renderCall(this.args, theme);\n\t\t\t\t\tif (callComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(callComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to default on error\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No custom renderCall, show tool name\n\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t}\n\n\t\t\t// Render result component if we have a result\n\t\t\tif (this.result && this.toolDefinition.renderResult) {\n\t\t\t\ttry {\n\t\t\t\t\tconst resultComponent = this.toolDefinition.renderResult(\n\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t);\n\t\t\t\t\tif (resultComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(resultComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to showing raw output on error\n\t\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\t\tif (output) {\n\t\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (this.result) {\n\t\t\t\t// Has result but no custom renderResult\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"bash\") {\n\t\t\t// Bash uses Box with visual line truncation\n\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\tthis.contentBox.clear();\n\t\t\tthis.renderBashContent();\n\t\t} else {\n\t\t\t// Other built-in tools: use Text directly with caching\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t}\n\n\t\t// Handle images (same for both custom and built-in)\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\t\t\tconst caps = getCapabilities();\n\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\t// Use converted PNG for Kitty protocol if available\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\n\t\t\t\t\t// For Kitty, skip non-PNG images that haven't been converted yet\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: 60 },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Render bash content using visual line truncation (like bash-execution.ts)\n\t */\n\tprivate renderBashContent(): void {\n\t\tconst command = this.args?.command || \"\";\n\n\t\t// Header\n\t\tthis.contentBox.addChild(\n\t\t\tnew Text(theme.fg(\"toolTitle\", theme.bold(`$ ${command || theme.fg(\"toolOutput\", \"...\")}`)), 0, 0),\n\t\t);\n\n\t\tif (this.result) {\n\t\t\tconst output = this.getTextOutput().trim();\n\n\t\t\tif (output) {\n\t\t\t\t// Style each line for the output\n\t\t\t\tconst styledOutput = output\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\tif (this.expanded) {\n\t\t\t\t\t// Show all lines when expanded\n\t\t\t\t\tthis.contentBox.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t\t\t} else {\n\t\t\t\t\t// Use visual line truncation when collapsed\n\t\t\t\t\t// Box has paddingX=1, so content width = terminal.columns - 2\n\t\t\t\t\tconst { visualLines, skippedCount } = truncateToVisualLines(\n\t\t\t\t\t\t`\\n${styledOutput}`,\n\t\t\t\t\t\tBASH_PREVIEW_LINES,\n\t\t\t\t\t\tthis.ui.terminal.columns - 2,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (skippedCount > 0) {\n\t\t\t\t\t\tthis.contentBox.addChild(\n\t\t\t\t\t\t\tnew Text(theme.fg(\"toolOutput\", `\\n... (${skippedCount} earlier lines)`), 0, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add pre-rendered visual lines as a raw component\n\t\t\t\t\tthis.contentBox.addChild({\n\t\t\t\t\t\trender: () => visualLines,\n\t\t\t\t\t\tinvalidate: () => {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Truncation warnings\n\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\tconst fullOutputPath = this.result.details?.fullOutputPath;\n\t\t\tif (truncation?.truncated || fullOutputPath) {\n\t\t\t\tconst warnings: string[] = [];\n\t\t\t\tif (fullOutputPath) {\n\t\t\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t\t\t}\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.contentBox.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\tif (!this.result) return \"\";\n\n\t\tconst textBlocks = this.result.content?.filter((c: any) => c.type === \"text\") || [];\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tlet output = textBlocks\n\t\t\t.map((c: any) => {\n\t\t\t\t// Use sanitizeBinaryOutput to handle binary data that crashes string-width\n\t\t\t\treturn sanitizeBinaryOutput(stripAnsi(c.text || \"\")).replace(/\\r/g, \"\");\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\n\t\tconst caps = getCapabilities();\n\t\tif (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {\n\t\t\tconst imageIndicators = imageBlocks\n\t\t\t\t.map((img: any) => {\n\t\t\t\t\tconst dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\t\treturn imageFallback(img.mimeType, dims);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = \"\";\n\n\t\tif (this.toolName === \"read\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\tconst offset = this.args?.offset;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n\t\t\t}\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}`;\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\t\tconst lines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"write\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\t\t\tconst fileContent = this.args?.content || \"\";\n\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\tconst lines = fileContent\n\t\t\t\t? lang\n\t\t\t\t\t? highlightCode(replaceTabs(fileContent), lang)\n\t\t\t\t\t: fileContent.split(\"\\n\")\n\t\t\t\t: [];\n\t\t\tconst totalLines = lines.length;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"write\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\n\t\t\tif (fileContent) {\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines, ${totalLines} total)`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"edit\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\n\t\t\t// Build path display, appending :line if we have diff info\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tconst firstChangedLine =\n\t\t\t\t(this.editDiffPreview && \"firstChangedLine\" in this.editDiffPreview\n\t\t\t\t\t? this.editDiffPreview.firstChangedLine\n\t\t\t\t\t: undefined) ||\n\t\t\t\t(this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);\n\t\t\tif (firstChangedLine) {\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${firstChangedLine}`);\n\t\t\t}\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n\n\t\t\tif (this.result?.isError) {\n\t\t\t\t// Show error from result\n\t\t\t\tconst errorText = this.getTextOutput();\n\t\t\t\tif (errorText) {\n\t\t\t\t\ttext += `\\n\\n${theme.fg(\"error\", errorText)}`;\n\t\t\t\t}\n\t\t\t} else if (this.result?.details?.diff) {\n\t\t\t\t// Tool executed successfully - use the diff from result\n\t\t\t\t// This takes priority over editDiffPreview which may have a stale error\n\t\t\t\t// due to race condition (async preview computed after file was modified)\n\t\t\t\ttext += `\\n\\n${renderDiff(this.result.details.diff, { filePath: rawPath })}`;\n\t\t\t} else if (this.editDiffPreview) {\n\t\t\t\t// Use cached diff preview (before tool executes)\n\t\t\t\tif (\"error\" in this.editDiffPreview) {\n\t\t\t\t\ttext += `\\n\\n${theme.fg(\"error\", this.editDiffPreview.error)}`;\n\t\t\t\t} else if (this.editDiffPreview.diff) {\n\t\t\t\t\ttext += `\\n\\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath })}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"ls\") {\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${theme.fg(\"accent\", path)}`;\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst entryLimit = this.result.details?.entryLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (entryLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (entryLimit) {\n\t\t\t\t\t\twarnings.push(`${entryLimit} entries limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"find\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"find\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", pattern) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resultLimit = this.result.details?.resultLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (resultLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (resultLimit) {\n\t\t\t\t\t\twarnings.push(`${resultLimit} results limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"grep\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst glob = this.args?.glob;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", `/${pattern}/`) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (glob) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (${glob})`);\n\t\t\t}\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 15;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst matchLimit = this.result.details?.matchLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst linesTruncated = this.result.details?.linesTruncated;\n\t\t\t\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (matchLimit) {\n\t\t\t\t\t\twarnings.push(`${matchLimit} matches limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\twarnings.push(\"some lines truncated\");\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Generic tool (shouldn't reach here for custom tools)\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\n\t\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\t\ttext += `\\n\\n${content}`;\n\t\t\tconst output = this.getTextOutput();\n\t\t\tif (output) {\n\t\t\t\ttext += `\\n${output}`;\n\t\t\t}\n\t\t}\n\n\t\treturn text;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,SAAS,EAOT,KAAK,GAAG,EACR,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AA+BxE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAIb;IAEF,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,eAAe,CAAC,CAAS;IAEjC,OAAO,CAAC,eAAe,CAA8D;IAErF,YACC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,OAAO,kCAA2B,EAClC,cAAc,EAAE,cAAc,GAAG,SAAS,EAC1C,EAAE,EAAE,GAAG,EACP,GAAG,GAAE,MAAsB,EAyB3B;IAED;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAMhC,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAG1B;IAED;;;OAGG;IACH,eAAe,IAAI,IAAI,CAEtB;IAED;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA6B5B,YAAY,CACX,MAAM,EAAE;QACP,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClF,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KACjB,EACD,SAAS,UAAQ,GACf,IAAI,CAMN;IAED;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IA2BlC,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,OAAO,CAAC,aAAa;IA+GrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuEzB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,mBAAmB;CA6Q3B","sourcesContent":["import * as os from \"node:os\";\nimport {\n\tBox,\n\tContainer,\n\tgetCapabilities,\n\tgetImageDimensions,\n\tImage,\n\timageFallback,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@mariozechner/pi-tui\";\nimport stripAnsi from \"strip-ansi\";\nimport type { ToolDefinition } from \"../../../core/extensions/types.js\";\nimport { computeEditDiff, type EditDiffError, type EditDiffResult } from \"../../../core/tools/edit-diff.js\";\nimport { allTools } from \"../../../core/tools/index.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"../../../core/tools/truncate.js\";\nimport { convertToPng } from \"../../../utils/image-convert.js\";\nimport { sanitizeBinaryOutput } from \"../../../utils/shell.js\";\nimport { getLanguageFromPath, highlightCode, theme } from \"../theme/theme.js\";\nimport { renderDiff } from \"./diff.js\";\nimport { truncateToVisualLines } from \"./visual-truncate.js\";\n\n// Preview line limit for bash when not expanded\nconst BASH_PREVIEW_LINES = 5;\n\n/**\n * Convert absolute path to tilde notation if it's in home directory\n */\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\n/**\n * Replace tabs with spaces for consistent rendering\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean; // default: true (only used if terminal supports images)\n}\n\n/**\n * Component that renders a tool call with its result (updateable)\n */\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box; // Used for custom tools and bash visual truncation\n\tprivate contentText: Text; // For built-in tools (with its own padding/bg)\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\t// Cached edit diff preview (computed when args arrive, before tool executes)\n\tprivate editDiffPreview?: EditDiffResult | EditDiffError;\n\tprivate editDiffArgsKey?: string; // Track which args the preview is for\n\t// Cached converted images for Kitty protocol (which requires PNG), keyed by index\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\n\tconstructor(\n\t\ttoolName: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition | undefined,\n\t\tui: TUI,\n\t\tcwd: string = process.cwd(),\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.args = args;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create both - contentBox for custom tools/bash, contentText for other built-ins\n\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\n\t\t// Use contentBox for bash (visual truncation) or custom tools with custom renderers\n\t\t// Use contentText for built-in tools (including overrides without custom renderers)\n\t\tif (toolName === \"bash\" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {\n\t\t\tthis.addChild(this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * Check if we should use built-in rendering for this tool.\n\t * Returns true if the tool name is a built-in AND either there's no toolDefinition\n\t * or the toolDefinition doesn't provide custom renderers.\n\t */\n\tprivate shouldUseBuiltInRenderer(): boolean {\n\t\tconst isBuiltInName = this.toolName in allTools;\n\t\tconst hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;\n\t\treturn isBuiltInName && !hasCustomRenderers;\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * Signal that args are complete (tool is about to execute).\n\t * This triggers diff computation for edit tool.\n\t */\n\tsetArgsComplete(): void {\n\t\tthis.maybeComputeEditDiff();\n\t}\n\n\t/**\n\t * Compute edit diff preview when we have complete args.\n\t * This runs async and updates display when done.\n\t */\n\tprivate maybeComputeEditDiff(): void {\n\t\tif (this.toolName !== \"edit\") return;\n\n\t\tconst path = this.args?.path;\n\t\tconst oldText = this.args?.oldText;\n\t\tconst newText = this.args?.newText;\n\n\t\t// Need all three params to compute diff\n\t\tif (!path || oldText === undefined || newText === undefined) return;\n\n\t\t// Create a key to track which args this computation is for\n\t\tconst argsKey = JSON.stringify({ path, oldText, newText });\n\n\t\t// Skip if we already computed for these exact args\n\t\tif (this.editDiffArgsKey === argsKey) return;\n\n\t\tthis.editDiffArgsKey = argsKey;\n\n\t\t// Compute diff async\n\t\tcomputeEditDiff(path, oldText, newText, this.cwd).then((result) => {\n\t\t\t// Only update if args haven't changed since we started\n\t\t\tif (this.editDiffArgsKey === argsKey) {\n\t\t\t\tthis.editDiffPreview = result;\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t});\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t\t// Convert non-PNG images to PNG for Kitty protocol (async)\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\t/**\n\t * Convert non-PNG images to PNG for Kitty graphics protocol.\n\t * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.\n\t */\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\t// Only needed for Kitty protocol\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\t// Skip if already PNG or already converted\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\t// Convert async\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Set background based on state\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\t// Use built-in rendering for built-in tools (or overrides without custom renderers)\n\t\tif (this.shouldUseBuiltInRenderer()) {\n\t\t\tif (this.toolName === \"bash\") {\n\t\t\t\t// Bash uses Box with visual line truncation\n\t\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\t\tthis.contentBox.clear();\n\t\t\t\tthis.renderBashContent();\n\t\t\t} else {\n\t\t\t\t// Other built-in tools: use Text directly with caching\n\t\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\t}\n\t\t} else if (this.toolDefinition) {\n\t\t\t// Custom tools use Box for flexible component rendering\n\t\t\tthis.contentBox.setBgFn(bgFn);\n\t\t\tthis.contentBox.clear();\n\n\t\t\t// Render call component\n\t\t\tif (this.toolDefinition.renderCall) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callComponent = this.toolDefinition.renderCall(this.args, theme);\n\t\t\t\t\tif (callComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(callComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to default on error\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No custom renderCall, show tool name\n\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0));\n\t\t\t}\n\n\t\t\t// Render result component if we have a result\n\t\t\tif (this.result && this.toolDefinition.renderResult) {\n\t\t\t\ttry {\n\t\t\t\t\tconst resultComponent = this.toolDefinition.renderResult(\n\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t);\n\t\t\t\t\tif (resultComponent) {\n\t\t\t\t\t\tthis.contentBox.addChild(resultComponent);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Fall back to showing raw output on error\n\t\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\t\tif (output) {\n\t\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (this.result) {\n\t\t\t\t// Has result but no custom renderResult\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\tthis.contentBox.addChild(new Text(theme.fg(\"toolOutput\", output), 0, 0));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle images (same for both custom and built-in)\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\t\t\tconst caps = getCapabilities();\n\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\t// Use converted PNG for Kitty protocol if available\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\n\t\t\t\t\t// For Kitty, skip non-PNG images that haven't been converted yet\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: 60 },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Render bash content using visual line truncation (like bash-execution.ts)\n\t */\n\tprivate renderBashContent(): void {\n\t\tconst command = this.args?.command || \"\";\n\n\t\t// Header\n\t\tthis.contentBox.addChild(\n\t\t\tnew Text(theme.fg(\"toolTitle\", theme.bold(`$ ${command || theme.fg(\"toolOutput\", \"...\")}`)), 0, 0),\n\t\t);\n\n\t\tif (this.result) {\n\t\t\tconst output = this.getTextOutput().trim();\n\n\t\t\tif (output) {\n\t\t\t\t// Style each line for the output\n\t\t\t\tconst styledOutput = output\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\tif (this.expanded) {\n\t\t\t\t\t// Show all lines when expanded\n\t\t\t\t\tthis.contentBox.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t\t\t} else {\n\t\t\t\t\t// Use visual line truncation when collapsed with width-aware caching\n\t\t\t\t\tconst textContent = `\\n${styledOutput}`;\n\t\t\t\t\tlet cachedWidth: number | undefined;\n\t\t\t\t\tlet cachedLines: string[] | undefined;\n\t\t\t\t\tlet cachedSkipped: number | undefined;\n\n\t\t\t\t\tthis.contentBox.addChild({\n\t\t\t\t\t\trender: (width: number) => {\n\t\t\t\t\t\t\tif (cachedLines === undefined || cachedWidth !== width) {\n\t\t\t\t\t\t\t\tconst result = truncateToVisualLines(textContent, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\t\t\tcachedLines = result.visualLines;\n\t\t\t\t\t\t\t\tcachedSkipped = result.skippedCount;\n\t\t\t\t\t\t\t\tcachedWidth = width;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn cachedSkipped && cachedSkipped > 0\n\t\t\t\t\t\t\t\t? [\"\", theme.fg(\"toolOutput\", `... (${cachedSkipped} earlier lines)`), ...cachedLines]\n\t\t\t\t\t\t\t\t: cachedLines;\n\t\t\t\t\t\t},\n\t\t\t\t\t\tinvalidate: () => {\n\t\t\t\t\t\t\tcachedWidth = undefined;\n\t\t\t\t\t\t\tcachedLines = undefined;\n\t\t\t\t\t\t\tcachedSkipped = undefined;\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Truncation warnings\n\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\tconst fullOutputPath = this.result.details?.fullOutputPath;\n\t\t\tif (truncation?.truncated || fullOutputPath) {\n\t\t\t\tconst warnings: string[] = [];\n\t\t\t\tif (fullOutputPath) {\n\t\t\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t\t\t}\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.contentBox.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\tif (!this.result) return \"\";\n\n\t\tconst textBlocks = this.result.content?.filter((c: any) => c.type === \"text\") || [];\n\t\tconst imageBlocks = this.result.content?.filter((c: any) => c.type === \"image\") || [];\n\n\t\tlet output = textBlocks\n\t\t\t.map((c: any) => {\n\t\t\t\t// Use sanitizeBinaryOutput to handle binary data that crashes string-width\n\t\t\t\treturn sanitizeBinaryOutput(stripAnsi(c.text || \"\")).replace(/\\r/g, \"\");\n\t\t\t})\n\t\t\t.join(\"\\n\");\n\n\t\tconst caps = getCapabilities();\n\t\tif (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {\n\t\t\tconst imageIndicators = imageBlocks\n\t\t\t\t.map((img: any) => {\n\t\t\t\t\tconst dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;\n\t\t\t\t\treturn imageFallback(img.mimeType, dims);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\toutput = output ? `${output}\\n${imageIndicators}` : imageIndicators;\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = \"\";\n\n\t\tif (this.toolName === \"read\") {\n\t\t\tconst path = shortenPath(this.args?.file_path || this.args?.path || \"\");\n\t\t\tconst offset = this.args?.offset;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n\t\t\t}\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}`;\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput();\n\t\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\t\tconst lines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext +=\n\t\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t`[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"write\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\t\t\tconst fileContent = this.args?.content || \"\";\n\t\t\tconst lang = getLanguageFromPath(rawPath);\n\t\t\tconst lines = fileContent\n\t\t\t\t? lang\n\t\t\t\t\t? highlightCode(replaceTabs(fileContent), lang)\n\t\t\t\t\t: fileContent.split(\"\\n\")\n\t\t\t\t: [];\n\t\t\tconst totalLines = lines.length;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"write\")) +\n\t\t\t\t\" \" +\n\t\t\t\t(path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\"));\n\n\t\t\tif (fileContent) {\n\t\t\t\tconst maxLines = this.expanded ? lines.length : 10;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\ttext +=\n\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\tdisplayLines\n\t\t\t\t\t\t.map((line: string) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line))))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines, ${totalLines} total)`);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"edit\") {\n\t\t\tconst rawPath = this.args?.file_path || this.args?.path || \"\";\n\t\t\tconst path = shortenPath(rawPath);\n\n\t\t\t// Build path display, appending :line if we have diff info\n\t\t\tlet pathDisplay = path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\t\t\tconst firstChangedLine =\n\t\t\t\t(this.editDiffPreview && \"firstChangedLine\" in this.editDiffPreview\n\t\t\t\t\t? this.editDiffPreview.firstChangedLine\n\t\t\t\t\t: undefined) ||\n\t\t\t\t(this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);\n\t\t\tif (firstChangedLine) {\n\t\t\t\tpathDisplay += theme.fg(\"warning\", `:${firstChangedLine}`);\n\t\t\t}\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n\n\t\t\tif (this.result?.isError) {\n\t\t\t\t// Show error from result\n\t\t\t\tconst errorText = this.getTextOutput();\n\t\t\t\tif (errorText) {\n\t\t\t\t\ttext += `\\n\\n${theme.fg(\"error\", errorText)}`;\n\t\t\t\t}\n\t\t\t} else if (this.result?.details?.diff) {\n\t\t\t\t// Tool executed successfully - use the diff from result\n\t\t\t\t// This takes priority over editDiffPreview which may have a stale error\n\t\t\t\t// due to race condition (async preview computed after file was modified)\n\t\t\t\ttext += `\\n\\n${renderDiff(this.result.details.diff, { filePath: rawPath })}`;\n\t\t\t} else if (this.editDiffPreview) {\n\t\t\t\t// Use cached diff preview (before tool executes)\n\t\t\t\tif (\"error\" in this.editDiffPreview) {\n\t\t\t\t\ttext += `\\n\\n${theme.fg(\"error\", this.editDiffPreview.error)}`;\n\t\t\t\t} else if (this.editDiffPreview.diff) {\n\t\t\t\t\ttext += `\\n\\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath })}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"ls\") {\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${theme.fg(\"accent\", path)}`;\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst entryLimit = this.result.details?.entryLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (entryLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (entryLimit) {\n\t\t\t\t\t\twarnings.push(`${entryLimit} entries limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"find\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"find\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", pattern) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 20;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resultLimit = this.result.details?.resultLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tif (resultLimit || truncation?.truncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (resultLimit) {\n\t\t\t\t\t\twarnings.push(`${resultLimit} results limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.toolName === \"grep\") {\n\t\t\tconst pattern = this.args?.pattern || \"\";\n\t\t\tconst path = shortenPath(this.args?.path || \".\");\n\t\t\tconst glob = this.args?.glob;\n\t\t\tconst limit = this.args?.limit;\n\n\t\t\ttext =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\t\t\" \" +\n\t\t\t\ttheme.fg(\"accent\", `/${pattern}/`) +\n\t\t\t\ttheme.fg(\"toolOutput\", ` in ${path}`);\n\t\t\tif (glob) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` (${glob})`);\n\t\t\t}\n\t\t\tif (limit !== undefined) {\n\t\t\t\ttext += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst output = this.getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst maxLines = this.expanded ? lines.length : 15;\n\t\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\t\ttext += `\\n\\n${displayLines.map((line: string) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t\ttext += theme.fg(\"toolOutput\", `\\n... (${remaining} more lines)`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst matchLimit = this.result.details?.matchLimitReached;\n\t\t\t\tconst truncation = this.result.details?.truncation;\n\t\t\t\tconst linesTruncated = this.result.details?.linesTruncated;\n\t\t\t\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\t\t\t\tconst warnings: string[] = [];\n\t\t\t\t\tif (matchLimit) {\n\t\t\t\t\t\twarnings.push(`${matchLimit} matches limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (truncation?.truncated) {\n\t\t\t\t\t\twarnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\t\t\t\t}\n\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\twarnings.push(\"some lines truncated\");\n\t\t\t\t\t}\n\t\t\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Generic tool (shouldn't reach here for custom tools)\n\t\t\ttext = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\n\t\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\t\ttext += `\\n\\n${content}`;\n\t\t\tconst output = this.getTextOutput();\n\t\t\tif (output) {\n\t\t\t\ttext += `\\n${output}`;\n\t\t\t}\n\t\t}\n\n\t\treturn text;\n\t}\n}\n"]}