indusagi 0.1.1

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 (422) hide show
  1. package/README.md +27 -0
  2. package/dist/agent/agent-loop.d.ts +21 -0
  3. package/dist/agent/agent-loop.d.ts.map +1 -0
  4. package/dist/agent/agent-loop.js +307 -0
  5. package/dist/agent/agent.d.ts +122 -0
  6. package/dist/agent/agent.d.ts.map +1 -0
  7. package/dist/agent/agent.js +347 -0
  8. package/dist/agent/index.d.ts +5 -0
  9. package/dist/agent/index.d.ts.map +1 -0
  10. package/dist/agent/index.js +8 -0
  11. package/dist/agent/proxy.d.ts +85 -0
  12. package/dist/agent/proxy.d.ts.map +1 -0
  13. package/dist/agent/proxy.js +267 -0
  14. package/dist/agent/types.d.ts +178 -0
  15. package/dist/agent/types.d.ts.map +1 -0
  16. package/dist/agent/types.js +1 -0
  17. package/dist/agent.d.ts +2 -0
  18. package/dist/agent.d.ts.map +1 -0
  19. package/dist/agent.js +1 -0
  20. package/dist/ai/api-registry.d.ts +20 -0
  21. package/dist/ai/api-registry.d.ts.map +1 -0
  22. package/dist/ai/api-registry.js +43 -0
  23. package/dist/ai/cli.d.ts +3 -0
  24. package/dist/ai/cli.d.ts.map +1 -0
  25. package/dist/ai/cli.js +115 -0
  26. package/dist/ai/env-api-keys.d.ts +9 -0
  27. package/dist/ai/env-api-keys.d.ts.map +1 -0
  28. package/dist/ai/env-api-keys.js +90 -0
  29. package/dist/ai/index.d.ts +20 -0
  30. package/dist/ai/index.d.ts.map +1 -0
  31. package/dist/ai/index.js +19 -0
  32. package/dist/ai/models.d.ts +21 -0
  33. package/dist/ai/models.d.ts.map +1 -0
  34. package/dist/ai/models.generated.d.ts +11500 -0
  35. package/dist/ai/models.generated.d.ts.map +1 -0
  36. package/dist/ai/models.generated.js +11350 -0
  37. package/dist/ai/models.js +47 -0
  38. package/dist/ai/providers/amazon-bedrock.d.ts +15 -0
  39. package/dist/ai/providers/amazon-bedrock.d.ts.map +1 -0
  40. package/dist/ai/providers/amazon-bedrock.js +510 -0
  41. package/dist/ai/providers/anthropic.d.ts +13 -0
  42. package/dist/ai/providers/anthropic.d.ts.map +1 -0
  43. package/dist/ai/providers/anthropic.js +584 -0
  44. package/dist/ai/providers/azure-openai-responses.d.ts +15 -0
  45. package/dist/ai/providers/azure-openai-responses.d.ts.map +1 -0
  46. package/dist/ai/providers/azure-openai-responses.js +183 -0
  47. package/dist/ai/providers/google-gemini-cli.d.ts +74 -0
  48. package/dist/ai/providers/google-gemini-cli.d.ts.map +1 -0
  49. package/dist/ai/providers/google-gemini-cli.js +807 -0
  50. package/dist/ai/providers/google-shared.d.ts +64 -0
  51. package/dist/ai/providers/google-shared.d.ts.map +1 -0
  52. package/dist/ai/providers/google-shared.js +300 -0
  53. package/dist/ai/providers/google-vertex.d.ts +15 -0
  54. package/dist/ai/providers/google-vertex.d.ts.map +1 -0
  55. package/dist/ai/providers/google-vertex.js +370 -0
  56. package/dist/ai/providers/google.d.ts +13 -0
  57. package/dist/ai/providers/google.d.ts.map +1 -0
  58. package/dist/ai/providers/google.js +351 -0
  59. package/dist/ai/providers/openai-codex-responses.d.ts +9 -0
  60. package/dist/ai/providers/openai-codex-responses.d.ts.map +1 -0
  61. package/dist/ai/providers/openai-codex-responses.js +341 -0
  62. package/dist/ai/providers/openai-completions.d.ts +15 -0
  63. package/dist/ai/providers/openai-completions.d.ts.map +1 -0
  64. package/dist/ai/providers/openai-completions.js +701 -0
  65. package/dist/ai/providers/openai-responses-shared.d.ts +17 -0
  66. package/dist/ai/providers/openai-responses-shared.d.ts.map +1 -0
  67. package/dist/ai/providers/openai-responses-shared.js +423 -0
  68. package/dist/ai/providers/openai-responses.d.ts +13 -0
  69. package/dist/ai/providers/openai-responses.d.ts.map +1 -0
  70. package/dist/ai/providers/openai-responses.js +182 -0
  71. package/dist/ai/providers/register-builtins.d.ts +3 -0
  72. package/dist/ai/providers/register-builtins.d.ts.map +1 -0
  73. package/dist/ai/providers/register-builtins.js +62 -0
  74. package/dist/ai/providers/simple-options.d.ts +8 -0
  75. package/dist/ai/providers/simple-options.d.ts.map +1 -0
  76. package/dist/ai/providers/simple-options.js +31 -0
  77. package/dist/ai/providers/transform-messages.d.ts +8 -0
  78. package/dist/ai/providers/transform-messages.d.ts.map +1 -0
  79. package/dist/ai/providers/transform-messages.js +149 -0
  80. package/dist/ai/stream.d.ts +8 -0
  81. package/dist/ai/stream.d.ts.map +1 -0
  82. package/dist/ai/stream.js +34 -0
  83. package/dist/ai/types.d.ts +234 -0
  84. package/dist/ai/types.d.ts.map +1 -0
  85. package/dist/ai/types.js +1 -0
  86. package/dist/ai/utils/event-stream.d.ts +21 -0
  87. package/dist/ai/utils/event-stream.d.ts.map +1 -0
  88. package/dist/ai/utils/event-stream.js +76 -0
  89. package/dist/ai/utils/json-parse.d.ts +9 -0
  90. package/dist/ai/utils/json-parse.d.ts.map +1 -0
  91. package/dist/ai/utils/json-parse.js +28 -0
  92. package/dist/ai/utils/oauth/anthropic.d.ts +17 -0
  93. package/dist/ai/utils/oauth/anthropic.d.ts.map +1 -0
  94. package/dist/ai/utils/oauth/anthropic.js +103 -0
  95. package/dist/ai/utils/oauth/github-copilot.d.ts +30 -0
  96. package/dist/ai/utils/oauth/github-copilot.d.ts.map +1 -0
  97. package/dist/ai/utils/oauth/github-copilot.js +280 -0
  98. package/dist/ai/utils/oauth/google-antigravity.d.ts +26 -0
  99. package/dist/ai/utils/oauth/google-antigravity.d.ts.map +1 -0
  100. package/dist/ai/utils/oauth/google-antigravity.js +355 -0
  101. package/dist/ai/utils/oauth/google-gemini-cli.d.ts +26 -0
  102. package/dist/ai/utils/oauth/google-gemini-cli.d.ts.map +1 -0
  103. package/dist/ai/utils/oauth/google-gemini-cli.js +461 -0
  104. package/dist/ai/utils/oauth/index.d.ts +50 -0
  105. package/dist/ai/utils/oauth/index.d.ts.map +1 -0
  106. package/dist/ai/utils/oauth/index.js +106 -0
  107. package/dist/ai/utils/oauth/openai-codex.d.ts +34 -0
  108. package/dist/ai/utils/oauth/openai-codex.d.ts.map +1 -0
  109. package/dist/ai/utils/oauth/openai-codex.js +379 -0
  110. package/dist/ai/utils/oauth/pkce.d.ts +13 -0
  111. package/dist/ai/utils/oauth/pkce.d.ts.map +1 -0
  112. package/dist/ai/utils/oauth/pkce.js +30 -0
  113. package/dist/ai/utils/oauth/types.d.ts +47 -0
  114. package/dist/ai/utils/oauth/types.d.ts.map +1 -0
  115. package/dist/ai/utils/oauth/types.js +1 -0
  116. package/dist/ai/utils/overflow.d.ts +51 -0
  117. package/dist/ai/utils/overflow.d.ts.map +1 -0
  118. package/dist/ai/utils/overflow.js +111 -0
  119. package/dist/ai/utils/sanitize-unicode.d.ts +22 -0
  120. package/dist/ai/utils/sanitize-unicode.d.ts.map +1 -0
  121. package/dist/ai/utils/sanitize-unicode.js +25 -0
  122. package/dist/ai/utils/typebox-helpers.d.ts +17 -0
  123. package/dist/ai/utils/typebox-helpers.d.ts.map +1 -0
  124. package/dist/ai/utils/typebox-helpers.js +20 -0
  125. package/dist/ai/utils/validation.d.ts +18 -0
  126. package/dist/ai/utils/validation.d.ts.map +1 -0
  127. package/dist/ai/utils/validation.js +71 -0
  128. package/dist/ai.d.ts +2 -0
  129. package/dist/ai.d.ts.map +1 -0
  130. package/dist/ai.js +1 -0
  131. package/dist/index.d.ts +5 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +4 -0
  134. package/dist/tui/autocomplete.d.ts +47 -0
  135. package/dist/tui/autocomplete.d.ts.map +1 -0
  136. package/dist/tui/autocomplete.js +474 -0
  137. package/dist/tui/components/box.d.ts +24 -0
  138. package/dist/tui/components/box.d.ts.map +1 -0
  139. package/dist/tui/components/box.js +100 -0
  140. package/dist/tui/components/cancellable-loader.d.ts +22 -0
  141. package/dist/tui/components/cancellable-loader.d.ts.map +1 -0
  142. package/dist/tui/components/cancellable-loader.js +35 -0
  143. package/dist/tui/components/editor.d.ts +171 -0
  144. package/dist/tui/components/editor.d.ts.map +1 -0
  145. package/dist/tui/components/editor.js +1540 -0
  146. package/dist/tui/components/image.d.ts +28 -0
  147. package/dist/tui/components/image.d.ts.map +1 -0
  148. package/dist/tui/components/image.js +60 -0
  149. package/dist/tui/components/input.d.ts +25 -0
  150. package/dist/tui/components/input.d.ts.map +1 -0
  151. package/dist/tui/components/input.js +297 -0
  152. package/dist/tui/components/loader.d.ts +21 -0
  153. package/dist/tui/components/loader.d.ts.map +1 -0
  154. package/dist/tui/components/loader.js +45 -0
  155. package/dist/tui/components/markdown.d.ts +89 -0
  156. package/dist/tui/components/markdown.d.ts.map +1 -0
  157. package/dist/tui/components/markdown.js +525 -0
  158. package/dist/tui/components/select-list.d.ts +32 -0
  159. package/dist/tui/components/select-list.d.ts.map +1 -0
  160. package/dist/tui/components/select-list.js +147 -0
  161. package/dist/tui/components/settings-list.d.ts +50 -0
  162. package/dist/tui/components/settings-list.d.ts.map +1 -0
  163. package/dist/tui/components/settings-list.js +176 -0
  164. package/dist/tui/components/spacer.d.ts +12 -0
  165. package/dist/tui/components/spacer.d.ts.map +1 -0
  166. package/dist/tui/components/spacer.js +21 -0
  167. package/dist/tui/components/text.d.ts +19 -0
  168. package/dist/tui/components/text.d.ts.map +1 -0
  169. package/dist/tui/components/text.js +80 -0
  170. package/dist/tui/components/truncated-text.d.ts +13 -0
  171. package/dist/tui/components/truncated-text.d.ts.map +1 -0
  172. package/dist/tui/components/truncated-text.js +47 -0
  173. package/dist/tui/editor-component.d.ts +35 -0
  174. package/dist/tui/editor-component.d.ts.map +1 -0
  175. package/dist/tui/editor-component.js +1 -0
  176. package/dist/tui/fuzzy.d.ts +16 -0
  177. package/dist/tui/fuzzy.d.ts.map +1 -0
  178. package/dist/tui/fuzzy.js +106 -0
  179. package/dist/tui/index.d.ts +23 -0
  180. package/dist/tui/index.d.ts.map +1 -0
  181. package/dist/tui/index.js +31 -0
  182. package/dist/tui/keybindings.d.ts +39 -0
  183. package/dist/tui/keybindings.d.ts.map +1 -0
  184. package/dist/tui/keybindings.js +110 -0
  185. package/dist/tui/keys.d.ts +160 -0
  186. package/dist/tui/keys.d.ts.map +1 -0
  187. package/dist/tui/keys.js +939 -0
  188. package/dist/tui/stdin-buffer.d.ts +48 -0
  189. package/dist/tui/stdin-buffer.d.ts.map +1 -0
  190. package/dist/tui/stdin-buffer.js +315 -0
  191. package/dist/tui/terminal-image.d.ts +67 -0
  192. package/dist/tui/terminal-image.d.ts.map +1 -0
  193. package/dist/tui/terminal-image.js +277 -0
  194. package/dist/tui/terminal.d.ts +63 -0
  195. package/dist/tui/terminal.d.ts.map +1 -0
  196. package/dist/tui/terminal.js +174 -0
  197. package/dist/tui/tui.d.ts +191 -0
  198. package/dist/tui/tui.d.ts.map +1 -0
  199. package/dist/tui/tui.js +881 -0
  200. package/dist/tui/utils.d.ts +78 -0
  201. package/dist/tui/utils.d.ts.map +1 -0
  202. package/dist/tui/utils.js +801 -0
  203. package/dist/tui.d.ts +2 -0
  204. package/dist/tui.d.ts.map +1 -0
  205. package/dist/tui.js +1 -0
  206. package/dist/webui/ChatPanel.d.ts +28 -0
  207. package/dist/webui/ChatPanel.d.ts.map +1 -0
  208. package/dist/webui/ChatPanel.js +192 -0
  209. package/dist/webui/components/AgentInterface.d.ts +39 -0
  210. package/dist/webui/components/AgentInterface.d.ts.map +1 -0
  211. package/dist/webui/components/AgentInterface.js +370 -0
  212. package/dist/webui/components/AttachmentTile.d.ts +12 -0
  213. package/dist/webui/components/AttachmentTile.d.ts.map +1 -0
  214. package/dist/webui/components/AttachmentTile.js +110 -0
  215. package/dist/webui/components/ConsoleBlock.d.ts +12 -0
  216. package/dist/webui/components/ConsoleBlock.d.ts.map +1 -0
  217. package/dist/webui/components/ConsoleBlock.js +83 -0
  218. package/dist/webui/components/CustomProviderCard.d.ts +17 -0
  219. package/dist/webui/components/CustomProviderCard.d.ts.map +1 -0
  220. package/dist/webui/components/CustomProviderCard.js +109 -0
  221. package/dist/webui/components/ExpandableSection.d.ts +15 -0
  222. package/dist/webui/components/ExpandableSection.d.ts.map +1 -0
  223. package/dist/webui/components/ExpandableSection.js +62 -0
  224. package/dist/webui/components/Input.d.ts +26 -0
  225. package/dist/webui/components/Input.d.ts.map +1 -0
  226. package/dist/webui/components/Input.js +56 -0
  227. package/dist/webui/components/MessageEditor.d.ts +44 -0
  228. package/dist/webui/components/MessageEditor.d.ts.map +1 -0
  229. package/dist/webui/components/MessageEditor.js +413 -0
  230. package/dist/webui/components/MessageList.d.ts +14 -0
  231. package/dist/webui/components/MessageList.d.ts.map +1 -0
  232. package/dist/webui/components/MessageList.js +103 -0
  233. package/dist/webui/components/Messages.d.ts +94 -0
  234. package/dist/webui/components/Messages.d.ts.map +1 -0
  235. package/dist/webui/components/Messages.js +351 -0
  236. package/dist/webui/components/ProviderKeyInput.d.ts +16 -0
  237. package/dist/webui/components/ProviderKeyInput.d.ts.map +1 -0
  238. package/dist/webui/components/ProviderKeyInput.js +170 -0
  239. package/dist/webui/components/SandboxedIframe.d.ts +85 -0
  240. package/dist/webui/components/SandboxedIframe.d.ts.map +1 -0
  241. package/dist/webui/components/SandboxedIframe.js +510 -0
  242. package/dist/webui/components/StreamingMessageContainer.d.ts +19 -0
  243. package/dist/webui/components/StreamingMessageContainer.d.ts.map +1 -0
  244. package/dist/webui/components/StreamingMessageContainer.js +116 -0
  245. package/dist/webui/components/ThinkingBlock.d.ts +11 -0
  246. package/dist/webui/components/ThinkingBlock.d.ts.map +1 -0
  247. package/dist/webui/components/ThinkingBlock.js +57 -0
  248. package/dist/webui/components/message-renderer-registry.d.ts +12 -0
  249. package/dist/webui/components/message-renderer-registry.d.ts.map +1 -0
  250. package/dist/webui/components/message-renderer-registry.js +11 -0
  251. package/dist/webui/components/sandbox/ArtifactsRuntimeProvider.d.ts +35 -0
  252. package/dist/webui/components/sandbox/ArtifactsRuntimeProvider.d.ts.map +1 -0
  253. package/dist/webui/components/sandbox/ArtifactsRuntimeProvider.js +188 -0
  254. package/dist/webui/components/sandbox/AttachmentsRuntimeProvider.d.ts +17 -0
  255. package/dist/webui/components/sandbox/AttachmentsRuntimeProvider.d.ts.map +1 -0
  256. package/dist/webui/components/sandbox/AttachmentsRuntimeProvider.js +63 -0
  257. package/dist/webui/components/sandbox/ConsoleRuntimeProvider.d.ts +42 -0
  258. package/dist/webui/components/sandbox/ConsoleRuntimeProvider.d.ts.map +1 -0
  259. package/dist/webui/components/sandbox/ConsoleRuntimeProvider.js +160 -0
  260. package/dist/webui/components/sandbox/FileDownloadRuntimeProvider.d.ts +30 -0
  261. package/dist/webui/components/sandbox/FileDownloadRuntimeProvider.d.ts.map +1 -0
  262. package/dist/webui/components/sandbox/FileDownloadRuntimeProvider.js +96 -0
  263. package/dist/webui/components/sandbox/RuntimeMessageBridge.d.ts +19 -0
  264. package/dist/webui/components/sandbox/RuntimeMessageBridge.d.ts.map +1 -0
  265. package/dist/webui/components/sandbox/RuntimeMessageBridge.js +73 -0
  266. package/dist/webui/components/sandbox/RuntimeMessageRouter.d.ts +65 -0
  267. package/dist/webui/components/sandbox/RuntimeMessageRouter.d.ts.map +1 -0
  268. package/dist/webui/components/sandbox/RuntimeMessageRouter.js +167 -0
  269. package/dist/webui/components/sandbox/SandboxRuntimeProvider.d.ts +48 -0
  270. package/dist/webui/components/sandbox/SandboxRuntimeProvider.d.ts.map +1 -0
  271. package/dist/webui/components/sandbox/SandboxRuntimeProvider.js +1 -0
  272. package/dist/webui/dialogs/ApiKeyPromptDialog.d.ts +15 -0
  273. package/dist/webui/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
  274. package/dist/webui/dialogs/ApiKeyPromptDialog.js +78 -0
  275. package/dist/webui/dialogs/AttachmentOverlay.d.ts +32 -0
  276. package/dist/webui/dialogs/AttachmentOverlay.d.ts.map +1 -0
  277. package/dist/webui/dialogs/AttachmentOverlay.js +575 -0
  278. package/dist/webui/dialogs/CustomProviderDialog.d.ts +25 -0
  279. package/dist/webui/dialogs/CustomProviderDialog.d.ts.map +1 -0
  280. package/dist/webui/dialogs/CustomProviderDialog.js +269 -0
  281. package/dist/webui/dialogs/ModelSelector.d.ts +27 -0
  282. package/dist/webui/dialogs/ModelSelector.d.ts.map +1 -0
  283. package/dist/webui/dialogs/ModelSelector.js +319 -0
  284. package/dist/webui/dialogs/PersistentStorageDialog.d.ts +17 -0
  285. package/dist/webui/dialogs/PersistentStorageDialog.d.ts.map +1 -0
  286. package/dist/webui/dialogs/PersistentStorageDialog.js +146 -0
  287. package/dist/webui/dialogs/ProvidersModelsTab.d.ts +20 -0
  288. package/dist/webui/dialogs/ProvidersModelsTab.d.ts.map +1 -0
  289. package/dist/webui/dialogs/ProvidersModelsTab.js +190 -0
  290. package/dist/webui/dialogs/SessionListDialog.d.ts +19 -0
  291. package/dist/webui/dialogs/SessionListDialog.d.ts.map +1 -0
  292. package/dist/webui/dialogs/SessionListDialog.js +153 -0
  293. package/dist/webui/dialogs/SettingsDialog.d.ts +30 -0
  294. package/dist/webui/dialogs/SettingsDialog.d.ts.map +1 -0
  295. package/dist/webui/dialogs/SettingsDialog.js +228 -0
  296. package/dist/webui/index.d.ts +65 -0
  297. package/dist/webui/index.d.ts.map +1 -0
  298. package/dist/webui/index.js +67 -0
  299. package/dist/webui/prompts/prompts.d.ts +11 -0
  300. package/dist/webui/prompts/prompts.d.ts.map +1 -0
  301. package/dist/webui/prompts/prompts.js +271 -0
  302. package/dist/webui/storage/app-storage.d.ts +33 -0
  303. package/dist/webui/storage/app-storage.d.ts.map +1 -0
  304. package/dist/webui/storage/app-storage.js +37 -0
  305. package/dist/webui/storage/backends/indexeddb-storage-backend.d.ts +27 -0
  306. package/dist/webui/storage/backends/indexeddb-storage-backend.d.ts.map +1 -0
  307. package/dist/webui/storage/backends/indexeddb-storage-backend.js +165 -0
  308. package/dist/webui/storage/store.d.ts +23 -0
  309. package/dist/webui/storage/store.d.ts.map +1 -0
  310. package/dist/webui/storage/store.js +25 -0
  311. package/dist/webui/storage/stores/custom-providers-store.d.ts +25 -0
  312. package/dist/webui/storage/stores/custom-providers-store.d.ts.map +1 -0
  313. package/dist/webui/storage/stores/custom-providers-store.js +34 -0
  314. package/dist/webui/storage/stores/provider-keys-store.d.ts +14 -0
  315. package/dist/webui/storage/stores/provider-keys-store.d.ts.map +1 -0
  316. package/dist/webui/storage/stores/provider-keys-store.js +26 -0
  317. package/dist/webui/storage/stores/sessions-store.d.ts +32 -0
  318. package/dist/webui/storage/stores/sessions-store.d.ts.map +1 -0
  319. package/dist/webui/storage/stores/sessions-store.js +112 -0
  320. package/dist/webui/storage/stores/settings-store.d.ts +14 -0
  321. package/dist/webui/storage/stores/settings-store.d.ts.map +1 -0
  322. package/dist/webui/storage/stores/settings-store.js +27 -0
  323. package/dist/webui/storage/types.d.ts +176 -0
  324. package/dist/webui/storage/types.d.ts.map +1 -0
  325. package/dist/webui/storage/types.js +1 -0
  326. package/dist/webui/tools/artifacts/ArtifactElement.d.ts +9 -0
  327. package/dist/webui/tools/artifacts/ArtifactElement.d.ts.map +1 -0
  328. package/dist/webui/tools/artifacts/ArtifactElement.js +10 -0
  329. package/dist/webui/tools/artifacts/ArtifactPill.d.ts +4 -0
  330. package/dist/webui/tools/artifacts/ArtifactPill.d.ts.map +1 -0
  331. package/dist/webui/tools/artifacts/ArtifactPill.js +22 -0
  332. package/dist/webui/tools/artifacts/Console.d.ts +18 -0
  333. package/dist/webui/tools/artifacts/Console.d.ts.map +1 -0
  334. package/dist/webui/tools/artifacts/Console.js +94 -0
  335. package/dist/webui/tools/artifacts/DocxArtifact.d.ts +22 -0
  336. package/dist/webui/tools/artifacts/DocxArtifact.d.ts.map +1 -0
  337. package/dist/webui/tools/artifacts/DocxArtifact.js +207 -0
  338. package/dist/webui/tools/artifacts/ExcelArtifact.d.ts +24 -0
  339. package/dist/webui/tools/artifacts/ExcelArtifact.d.ts.map +1 -0
  340. package/dist/webui/tools/artifacts/ExcelArtifact.js +215 -0
  341. package/dist/webui/tools/artifacts/GenericArtifact.d.ts +19 -0
  342. package/dist/webui/tools/artifacts/GenericArtifact.d.ts.map +1 -0
  343. package/dist/webui/tools/artifacts/GenericArtifact.js +116 -0
  344. package/dist/webui/tools/artifacts/HtmlArtifact.d.ts +27 -0
  345. package/dist/webui/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
  346. package/dist/webui/tools/artifacts/HtmlArtifact.js +188 -0
  347. package/dist/webui/tools/artifacts/ImageArtifact.d.ts +20 -0
  348. package/dist/webui/tools/artifacts/ImageArtifact.d.ts.map +1 -0
  349. package/dist/webui/tools/artifacts/ImageArtifact.js +119 -0
  350. package/dist/webui/tools/artifacts/MarkdownArtifact.d.ts +19 -0
  351. package/dist/webui/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
  352. package/dist/webui/tools/artifacts/MarkdownArtifact.js +81 -0
  353. package/dist/webui/tools/artifacts/PdfArtifact.d.ts +25 -0
  354. package/dist/webui/tools/artifacts/PdfArtifact.d.ts.map +1 -0
  355. package/dist/webui/tools/artifacts/PdfArtifact.js +183 -0
  356. package/dist/webui/tools/artifacts/SvgArtifact.d.ts +18 -0
  357. package/dist/webui/tools/artifacts/SvgArtifact.d.ts.map +1 -0
  358. package/dist/webui/tools/artifacts/SvgArtifact.js +77 -0
  359. package/dist/webui/tools/artifacts/TextArtifact.d.ts +19 -0
  360. package/dist/webui/tools/artifacts/TextArtifact.d.ts.map +1 -0
  361. package/dist/webui/tools/artifacts/TextArtifact.js +143 -0
  362. package/dist/webui/tools/artifacts/artifacts-tool-renderer.d.ts +11 -0
  363. package/dist/webui/tools/artifacts/artifacts-tool-renderer.d.ts.map +1 -0
  364. package/dist/webui/tools/artifacts/artifacts-tool-renderer.js +271 -0
  365. package/dist/webui/tools/artifacts/artifacts.d.ts +63 -0
  366. package/dist/webui/tools/artifacts/artifacts.d.ts.map +1 -0
  367. package/dist/webui/tools/artifacts/artifacts.js +658 -0
  368. package/dist/webui/tools/artifacts/index.d.ts +8 -0
  369. package/dist/webui/tools/artifacts/index.d.ts.map +1 -0
  370. package/dist/webui/tools/artifacts/index.js +7 -0
  371. package/dist/webui/tools/extract-document.d.ts +24 -0
  372. package/dist/webui/tools/extract-document.d.ts.map +1 -0
  373. package/dist/webui/tools/extract-document.js +215 -0
  374. package/dist/webui/tools/index.d.ts +16 -0
  375. package/dist/webui/tools/index.d.ts.map +1 -0
  376. package/dist/webui/tools/index.js +32 -0
  377. package/dist/webui/tools/javascript-repl.d.ts +44 -0
  378. package/dist/webui/tools/javascript-repl.d.ts.map +1 -0
  379. package/dist/webui/tools/javascript-repl.js +223 -0
  380. package/dist/webui/tools/renderer-registry.d.ts +23 -0
  381. package/dist/webui/tools/renderer-registry.d.ts.map +1 -0
  382. package/dist/webui/tools/renderer-registry.js +106 -0
  383. package/dist/webui/tools/renderers/BashRenderer.d.ts +10 -0
  384. package/dist/webui/tools/renderers/BashRenderer.d.ts.map +1 -0
  385. package/dist/webui/tools/renderers/BashRenderer.js +41 -0
  386. package/dist/webui/tools/renderers/CalculateRenderer.d.ts +10 -0
  387. package/dist/webui/tools/renderers/CalculateRenderer.d.ts.map +1 -0
  388. package/dist/webui/tools/renderers/CalculateRenderer.js +44 -0
  389. package/dist/webui/tools/renderers/DefaultRenderer.d.ts +6 -0
  390. package/dist/webui/tools/renderers/DefaultRenderer.d.ts.map +1 -0
  391. package/dist/webui/tools/renderers/DefaultRenderer.js +93 -0
  392. package/dist/webui/tools/renderers/GetCurrentTimeRenderer.d.ts +10 -0
  393. package/dist/webui/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
  394. package/dist/webui/tools/renderers/GetCurrentTimeRenderer.js +71 -0
  395. package/dist/webui/tools/types.d.ts +10 -0
  396. package/dist/webui/tools/types.d.ts.map +1 -0
  397. package/dist/webui/tools/types.js +1 -0
  398. package/dist/webui/utils/attachment-utils.d.ts +19 -0
  399. package/dist/webui/utils/attachment-utils.d.ts.map +1 -0
  400. package/dist/webui/utils/attachment-utils.js +414 -0
  401. package/dist/webui/utils/auth-token.d.ts +3 -0
  402. package/dist/webui/utils/auth-token.d.ts.map +1 -0
  403. package/dist/webui/utils/auth-token.js +18 -0
  404. package/dist/webui/utils/format.d.ts +6 -0
  405. package/dist/webui/utils/format.d.ts.map +1 -0
  406. package/dist/webui/utils/format.js +46 -0
  407. package/dist/webui/utils/i18n.d.ts +636 -0
  408. package/dist/webui/utils/i18n.d.ts.map +1 -0
  409. package/dist/webui/utils/i18n.js +417 -0
  410. package/dist/webui/utils/model-discovery.d.ts +38 -0
  411. package/dist/webui/utils/model-discovery.d.ts.map +1 -0
  412. package/dist/webui/utils/model-discovery.js +242 -0
  413. package/dist/webui/utils/proxy-utils.d.ts +45 -0
  414. package/dist/webui/utils/proxy-utils.d.ts.map +1 -0
  415. package/dist/webui/utils/proxy-utils.js +115 -0
  416. package/dist/webui/utils/test-sessions.d.ts +359 -0
  417. package/dist/webui/utils/test-sessions.d.ts.map +1 -0
  418. package/dist/webui/utils/test-sessions.js +2324 -0
  419. package/dist/webui.d.ts +2 -0
  420. package/dist/webui.d.ts.map +1 -0
  421. package/dist/webui.js +1 -0
  422. package/package.json +105 -0
@@ -0,0 +1,1540 @@
1
+ import { getEditorKeybindings } from "../keybindings.js";
2
+ import { matchesKey } from "../keys.js";
3
+ import { CURSOR_MARKER } from "../tui.js";
4
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
5
+ import { SelectList } from "./select-list.js";
6
+ const segmenter = getSegmenter();
7
+ /**
8
+ * Split a line into word-wrapped chunks.
9
+ * Wraps at word boundaries when possible, falling back to character-level
10
+ * wrapping for words longer than the available width.
11
+ *
12
+ * @param line - The text line to wrap
13
+ * @param maxWidth - Maximum visible width per chunk
14
+ * @returns Array of chunks with text and position information
15
+ */
16
+ export function wordWrapLine(line, maxWidth) {
17
+ if (!line || maxWidth <= 0) {
18
+ return [{ text: "", startIndex: 0, endIndex: 0 }];
19
+ }
20
+ const lineWidth = visibleWidth(line);
21
+ if (lineWidth <= maxWidth) {
22
+ return [{ text: line, startIndex: 0, endIndex: line.length }];
23
+ }
24
+ const chunks = [];
25
+ const segments = [...segmenter.segment(line)];
26
+ let currentWidth = 0;
27
+ let chunkStart = 0;
28
+ // Wrap opportunity: the position after the last whitespace before a non-whitespace
29
+ // grapheme, i.e. where a line break is allowed.
30
+ let wrapOppIndex = -1;
31
+ let wrapOppWidth = 0;
32
+ for (let i = 0; i < segments.length; i++) {
33
+ const seg = segments[i];
34
+ const grapheme = seg.segment;
35
+ const gWidth = visibleWidth(grapheme);
36
+ const charIndex = seg.index;
37
+ const isWs = isWhitespaceChar(grapheme);
38
+ // Overflow check before advancing.
39
+ if (currentWidth + gWidth > maxWidth) {
40
+ if (wrapOppIndex >= 0) {
41
+ // Backtrack to last wrap opportunity.
42
+ chunks.push({ text: line.slice(chunkStart, wrapOppIndex), startIndex: chunkStart, endIndex: wrapOppIndex });
43
+ chunkStart = wrapOppIndex;
44
+ currentWidth -= wrapOppWidth;
45
+ }
46
+ else if (chunkStart < charIndex) {
47
+ // No wrap opportunity: force-break at current position.
48
+ chunks.push({ text: line.slice(chunkStart, charIndex), startIndex: chunkStart, endIndex: charIndex });
49
+ chunkStart = charIndex;
50
+ currentWidth = 0;
51
+ }
52
+ wrapOppIndex = -1;
53
+ }
54
+ // Advance.
55
+ currentWidth += gWidth;
56
+ // Record wrap opportunity: whitespace followed by non-whitespace.
57
+ // Multiple spaces join (no break between them); the break point is
58
+ // after the last space before the next word.
59
+ const next = segments[i + 1];
60
+ if (isWs && next && !isWhitespaceChar(next.segment)) {
61
+ wrapOppIndex = next.index;
62
+ wrapOppWidth = currentWidth;
63
+ }
64
+ }
65
+ // Push final chunk.
66
+ chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
67
+ return chunks;
68
+ }
69
+ // Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints.
70
+ const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
71
+ const KITTY_MOD_SHIFT = 1;
72
+ const KITTY_MOD_ALT = 2;
73
+ const KITTY_MOD_CTRL = 4;
74
+ // Decode a printable CSI-u sequence, preferring the shifted key when present.
75
+ function decodeKittyPrintable(data) {
76
+ const match = data.match(KITTY_CSI_U_REGEX);
77
+ if (!match)
78
+ return undefined;
79
+ // CSI-u groups: <codepoint>[:<shifted>[:<base>]];<mod>u
80
+ const codepoint = Number.parseInt(match[1] ?? "", 10);
81
+ if (!Number.isFinite(codepoint))
82
+ return undefined;
83
+ const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
84
+ const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
85
+ // Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
86
+ const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
87
+ // Ignore CSI-u sequences used for Alt/Ctrl shortcuts.
88
+ if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL))
89
+ return undefined;
90
+ // Prefer the shifted keycode when Shift is held.
91
+ let effectiveCodepoint = codepoint;
92
+ if (modifier & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
93
+ effectiveCodepoint = shiftedKey;
94
+ }
95
+ // Drop control characters or invalid codepoints.
96
+ if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32)
97
+ return undefined;
98
+ try {
99
+ return String.fromCodePoint(effectiveCodepoint);
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ }
105
+ export class Editor {
106
+ constructor(tui, theme, options = {}) {
107
+ this.state = {
108
+ lines: [""],
109
+ cursorLine: 0,
110
+ cursorCol: 0,
111
+ };
112
+ /** Focusable interface - set by TUI when focus changes */
113
+ this.focused = false;
114
+ this.paddingX = 0;
115
+ // Store last render width for cursor navigation
116
+ this.lastWidth = 80;
117
+ // Vertical scrolling support
118
+ this.scrollOffset = 0;
119
+ this.isAutocompleting = false;
120
+ this.autocompletePrefix = "";
121
+ // Paste tracking for large pastes
122
+ this.pastes = new Map();
123
+ this.pasteCounter = 0;
124
+ // Bracketed paste mode buffering
125
+ this.pasteBuffer = "";
126
+ this.isInPaste = false;
127
+ this.pendingShiftEnter = false;
128
+ // Prompt history for up/down navigation
129
+ this.history = [];
130
+ this.historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
131
+ // Kill ring for Emacs-style kill/yank operations
132
+ // Also tracks undo coalescing: "type-word" means we're mid-word (coalescing)
133
+ this.killRing = [];
134
+ this.lastAction = null;
135
+ // Undo support
136
+ this.undoStack = [];
137
+ this.disableSubmit = false;
138
+ this.tui = tui;
139
+ this.theme = theme;
140
+ this.borderColor = theme.borderColor;
141
+ const paddingX = options.paddingX ?? 0;
142
+ this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0;
143
+ }
144
+ getPaddingX() {
145
+ return this.paddingX;
146
+ }
147
+ setPaddingX(padding) {
148
+ const newPadding = Number.isFinite(padding) ? Math.max(0, Math.floor(padding)) : 0;
149
+ if (this.paddingX !== newPadding) {
150
+ this.paddingX = newPadding;
151
+ this.tui.requestRender();
152
+ }
153
+ }
154
+ setAutocompleteProvider(provider) {
155
+ this.autocompleteProvider = provider;
156
+ }
157
+ /**
158
+ * Add a prompt to history for up/down arrow navigation.
159
+ * Called after successful submission.
160
+ */
161
+ addToHistory(text) {
162
+ const trimmed = text.trim();
163
+ if (!trimmed)
164
+ return;
165
+ // Don't add consecutive duplicates
166
+ if (this.history.length > 0 && this.history[0] === trimmed)
167
+ return;
168
+ this.history.unshift(trimmed);
169
+ // Limit history size
170
+ if (this.history.length > 100) {
171
+ this.history.pop();
172
+ }
173
+ }
174
+ isEditorEmpty() {
175
+ return this.state.lines.length === 1 && this.state.lines[0] === "";
176
+ }
177
+ isOnFirstVisualLine() {
178
+ const visualLines = this.buildVisualLineMap(this.lastWidth);
179
+ const currentVisualLine = this.findCurrentVisualLine(visualLines);
180
+ return currentVisualLine === 0;
181
+ }
182
+ isOnLastVisualLine() {
183
+ const visualLines = this.buildVisualLineMap(this.lastWidth);
184
+ const currentVisualLine = this.findCurrentVisualLine(visualLines);
185
+ return currentVisualLine === visualLines.length - 1;
186
+ }
187
+ navigateHistory(direction) {
188
+ this.lastAction = null;
189
+ if (this.history.length === 0)
190
+ return;
191
+ const newIndex = this.historyIndex - direction; // Up(-1) increases index, Down(1) decreases
192
+ if (newIndex < -1 || newIndex >= this.history.length)
193
+ return;
194
+ // Capture state when first entering history browsing mode
195
+ if (this.historyIndex === -1 && newIndex >= 0) {
196
+ this.pushUndoSnapshot();
197
+ }
198
+ this.historyIndex = newIndex;
199
+ if (this.historyIndex === -1) {
200
+ // Returned to "current" state - clear editor
201
+ this.setTextInternal("");
202
+ }
203
+ else {
204
+ this.setTextInternal(this.history[this.historyIndex] || "");
205
+ }
206
+ }
207
+ /** Internal setText that doesn't reset history state - used by navigateHistory */
208
+ setTextInternal(text) {
209
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
210
+ this.state.lines = lines.length === 0 ? [""] : lines;
211
+ this.state.cursorLine = this.state.lines.length - 1;
212
+ this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
213
+ // Reset scroll - render() will adjust to show cursor
214
+ this.scrollOffset = 0;
215
+ if (this.onChange) {
216
+ this.onChange(this.getText());
217
+ }
218
+ }
219
+ invalidate() {
220
+ // No cached state to invalidate currently
221
+ }
222
+ render(width) {
223
+ const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
224
+ const paddingX = Math.min(this.paddingX, maxPadding);
225
+ const contentWidth = Math.max(1, width - paddingX * 2);
226
+ // Layout width: with padding the cursor can overflow into it,
227
+ // without padding we reserve 1 column for the cursor.
228
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
229
+ // Store for cursor navigation (must match wrapping width)
230
+ this.lastWidth = layoutWidth;
231
+ const horizontal = this.borderColor("─");
232
+ // Layout the text
233
+ const layoutLines = this.layoutText(layoutWidth);
234
+ // Calculate max visible lines: 30% of terminal height, minimum 5 lines
235
+ const terminalRows = this.tui.terminal.rows;
236
+ const maxVisibleLines = Math.max(5, Math.floor(terminalRows * 0.3));
237
+ // Find the cursor line index in layoutLines
238
+ let cursorLineIndex = layoutLines.findIndex((line) => line.hasCursor);
239
+ if (cursorLineIndex === -1)
240
+ cursorLineIndex = 0;
241
+ // Adjust scroll offset to keep cursor visible
242
+ if (cursorLineIndex < this.scrollOffset) {
243
+ this.scrollOffset = cursorLineIndex;
244
+ }
245
+ else if (cursorLineIndex >= this.scrollOffset + maxVisibleLines) {
246
+ this.scrollOffset = cursorLineIndex - maxVisibleLines + 1;
247
+ }
248
+ // Clamp scroll offset to valid range
249
+ const maxScrollOffset = Math.max(0, layoutLines.length - maxVisibleLines);
250
+ this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScrollOffset));
251
+ // Get visible lines slice
252
+ const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);
253
+ const result = [];
254
+ const leftPadding = " ".repeat(paddingX);
255
+ const rightPadding = leftPadding;
256
+ // Render top border (with scroll indicator if scrolled down)
257
+ if (this.scrollOffset > 0) {
258
+ const indicator = `─── ↑ ${this.scrollOffset} more `;
259
+ const remaining = width - visibleWidth(indicator);
260
+ result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
261
+ }
262
+ else {
263
+ result.push(horizontal.repeat(width));
264
+ }
265
+ // Render each visible layout line
266
+ // Emit hardware cursor marker only when focused and not showing autocomplete
267
+ const emitCursorMarker = this.focused && !this.isAutocompleting;
268
+ for (const layoutLine of visibleLines) {
269
+ let displayText = layoutLine.text;
270
+ let lineVisibleWidth = visibleWidth(layoutLine.text);
271
+ let cursorInPadding = false;
272
+ // Add cursor if this line has it
273
+ if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
274
+ const before = displayText.slice(0, layoutLine.cursorPos);
275
+ const after = displayText.slice(layoutLine.cursorPos);
276
+ // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
277
+ const marker = emitCursorMarker ? CURSOR_MARKER : "";
278
+ if (after.length > 0) {
279
+ // Cursor is on a character (grapheme) - replace it with highlighted version
280
+ // Get the first grapheme from 'after'
281
+ const afterGraphemes = [...segmenter.segment(after)];
282
+ const firstGrapheme = afterGraphemes[0]?.segment || "";
283
+ const restAfter = after.slice(firstGrapheme.length);
284
+ const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
285
+ displayText = before + marker + cursor + restAfter;
286
+ // lineVisibleWidth stays the same - we're replacing, not adding
287
+ }
288
+ else {
289
+ // Cursor is at the end - add highlighted space
290
+ const cursor = "\x1b[7m \x1b[0m";
291
+ displayText = before + marker + cursor;
292
+ lineVisibleWidth = lineVisibleWidth + 1;
293
+ // If cursor overflows content width into the padding, flag it
294
+ if (lineVisibleWidth > contentWidth && paddingX > 0) {
295
+ cursorInPadding = true;
296
+ }
297
+ }
298
+ }
299
+ // Calculate padding based on actual visible width
300
+ const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));
301
+ const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;
302
+ // Render the line (no side borders, just horizontal lines above and below)
303
+ result.push(`${leftPadding}${displayText}${padding}${lineRightPadding}`);
304
+ }
305
+ // Render bottom border (with scroll indicator if more content below)
306
+ const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
307
+ if (linesBelow > 0) {
308
+ const indicator = `─── ↓ ${linesBelow} more `;
309
+ const remaining = width - visibleWidth(indicator);
310
+ result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
311
+ }
312
+ else {
313
+ result.push(horizontal.repeat(width));
314
+ }
315
+ // Add autocomplete list if active
316
+ if (this.isAutocompleting && this.autocompleteList) {
317
+ const autocompleteResult = this.autocompleteList.render(contentWidth);
318
+ for (const line of autocompleteResult) {
319
+ const lineWidth = visibleWidth(line);
320
+ const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth));
321
+ result.push(`${leftPadding}${line}${linePadding}${rightPadding}`);
322
+ }
323
+ }
324
+ return result;
325
+ }
326
+ handleInput(data) {
327
+ const kb = getEditorKeybindings();
328
+ // Handle bracketed paste mode
329
+ if (data.includes("\x1b[200~")) {
330
+ this.isInPaste = true;
331
+ this.pasteBuffer = "";
332
+ data = data.replace("\x1b[200~", "");
333
+ }
334
+ if (this.isInPaste) {
335
+ this.pasteBuffer += data;
336
+ const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
337
+ if (endIndex !== -1) {
338
+ const pasteContent = this.pasteBuffer.substring(0, endIndex);
339
+ if (pasteContent.length > 0) {
340
+ this.handlePaste(pasteContent);
341
+ }
342
+ this.isInPaste = false;
343
+ const remaining = this.pasteBuffer.substring(endIndex + 6);
344
+ this.pasteBuffer = "";
345
+ if (remaining.length > 0) {
346
+ this.handleInput(remaining);
347
+ }
348
+ return;
349
+ }
350
+ return;
351
+ }
352
+ if (this.pendingShiftEnter) {
353
+ if (data === "\r") {
354
+ this.pendingShiftEnter = false;
355
+ this.addNewLine();
356
+ return;
357
+ }
358
+ this.pendingShiftEnter = false;
359
+ this.insertCharacter("\\");
360
+ }
361
+ if (data === "\\") {
362
+ this.pendingShiftEnter = true;
363
+ return;
364
+ }
365
+ // Ctrl+C - let parent handle (exit/clear)
366
+ if (kb.matches(data, "copy")) {
367
+ return;
368
+ }
369
+ // Undo
370
+ if (kb.matches(data, "undo")) {
371
+ this.undo();
372
+ return;
373
+ }
374
+ // Handle autocomplete mode
375
+ if (this.isAutocompleting && this.autocompleteList) {
376
+ if (kb.matches(data, "selectCancel")) {
377
+ this.cancelAutocomplete();
378
+ return;
379
+ }
380
+ if (kb.matches(data, "selectUp") || kb.matches(data, "selectDown")) {
381
+ this.autocompleteList.handleInput(data);
382
+ return;
383
+ }
384
+ if (kb.matches(data, "tab")) {
385
+ const selected = this.autocompleteList.getSelectedItem();
386
+ if (selected && this.autocompleteProvider) {
387
+ this.pushUndoSnapshot();
388
+ this.lastAction = null;
389
+ const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
390
+ this.state.lines = result.lines;
391
+ this.state.cursorLine = result.cursorLine;
392
+ this.state.cursorCol = result.cursorCol;
393
+ this.cancelAutocomplete();
394
+ if (this.onChange)
395
+ this.onChange(this.getText());
396
+ }
397
+ return;
398
+ }
399
+ if (kb.matches(data, "selectConfirm")) {
400
+ const selected = this.autocompleteList.getSelectedItem();
401
+ if (selected && this.autocompleteProvider) {
402
+ this.pushUndoSnapshot();
403
+ this.lastAction = null;
404
+ const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
405
+ this.state.lines = result.lines;
406
+ this.state.cursorLine = result.cursorLine;
407
+ this.state.cursorCol = result.cursorCol;
408
+ if (this.autocompletePrefix.startsWith("/")) {
409
+ this.cancelAutocomplete();
410
+ // Fall through to submit
411
+ }
412
+ else {
413
+ this.cancelAutocomplete();
414
+ if (this.onChange)
415
+ this.onChange(this.getText());
416
+ return;
417
+ }
418
+ }
419
+ }
420
+ }
421
+ // Tab - trigger completion
422
+ if (kb.matches(data, "tab") && !this.isAutocompleting) {
423
+ this.handleTabCompletion();
424
+ return;
425
+ }
426
+ // Deletion actions
427
+ if (kb.matches(data, "deleteToLineEnd")) {
428
+ this.deleteToEndOfLine();
429
+ return;
430
+ }
431
+ if (kb.matches(data, "deleteToLineStart")) {
432
+ this.deleteToStartOfLine();
433
+ return;
434
+ }
435
+ if (kb.matches(data, "deleteWordBackward")) {
436
+ this.deleteWordBackwards();
437
+ return;
438
+ }
439
+ if (kb.matches(data, "deleteWordForward")) {
440
+ this.deleteWordForward();
441
+ return;
442
+ }
443
+ if (kb.matches(data, "deleteCharBackward") || matchesKey(data, "shift+backspace")) {
444
+ this.handleBackspace();
445
+ return;
446
+ }
447
+ if (kb.matches(data, "deleteCharForward") || matchesKey(data, "shift+delete")) {
448
+ this.handleForwardDelete();
449
+ return;
450
+ }
451
+ // Kill ring actions
452
+ if (kb.matches(data, "yank")) {
453
+ this.yank();
454
+ return;
455
+ }
456
+ if (kb.matches(data, "yankPop")) {
457
+ this.yankPop();
458
+ return;
459
+ }
460
+ // Cursor movement actions
461
+ if (kb.matches(data, "cursorLineStart")) {
462
+ this.moveToLineStart();
463
+ return;
464
+ }
465
+ if (kb.matches(data, "cursorLineEnd")) {
466
+ this.moveToLineEnd();
467
+ return;
468
+ }
469
+ if (kb.matches(data, "cursorWordLeft")) {
470
+ this.moveWordBackwards();
471
+ return;
472
+ }
473
+ if (kb.matches(data, "cursorWordRight")) {
474
+ this.moveWordForwards();
475
+ return;
476
+ }
477
+ // New line (Shift+Enter, Alt+Enter, etc.)
478
+ if (kb.matches(data, "newLine") ||
479
+ (data.charCodeAt(0) === 10 && data.length > 1) ||
480
+ data === "\x1b\r" ||
481
+ data === "\x1b[13;2~" ||
482
+ (data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
483
+ (data === "\n" && data.length === 1) ||
484
+ data === "\\\r") {
485
+ this.addNewLine();
486
+ return;
487
+ }
488
+ // Submit (Enter)
489
+ if (kb.matches(data, "submit")) {
490
+ if (this.disableSubmit)
491
+ return;
492
+ let result = this.state.lines.join("\n").trim();
493
+ for (const [pasteId, pasteContent] of this.pastes) {
494
+ const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
495
+ result = result.replace(markerRegex, pasteContent);
496
+ }
497
+ this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
498
+ this.pastes.clear();
499
+ this.pasteCounter = 0;
500
+ this.historyIndex = -1;
501
+ this.scrollOffset = 0;
502
+ this.undoStack.length = 0;
503
+ this.lastAction = null;
504
+ if (this.onChange)
505
+ this.onChange("");
506
+ if (this.onSubmit)
507
+ this.onSubmit(result);
508
+ return;
509
+ }
510
+ // Arrow key navigation (with history support)
511
+ if (kb.matches(data, "cursorUp")) {
512
+ if (this.isEditorEmpty()) {
513
+ this.navigateHistory(-1);
514
+ }
515
+ else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
516
+ this.navigateHistory(-1);
517
+ }
518
+ else {
519
+ this.moveCursor(-1, 0);
520
+ }
521
+ return;
522
+ }
523
+ if (kb.matches(data, "cursorDown")) {
524
+ if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
525
+ this.navigateHistory(1);
526
+ }
527
+ else {
528
+ this.moveCursor(1, 0);
529
+ }
530
+ return;
531
+ }
532
+ if (kb.matches(data, "cursorRight")) {
533
+ this.moveCursor(0, 1);
534
+ return;
535
+ }
536
+ if (kb.matches(data, "cursorLeft")) {
537
+ this.moveCursor(0, -1);
538
+ return;
539
+ }
540
+ // Page up/down - scroll by page and move cursor
541
+ if (kb.matches(data, "pageUp")) {
542
+ this.pageScroll(-1);
543
+ return;
544
+ }
545
+ if (kb.matches(data, "pageDown")) {
546
+ this.pageScroll(1);
547
+ return;
548
+ }
549
+ // Shift+Space - insert regular space
550
+ if (matchesKey(data, "shift+space")) {
551
+ this.insertCharacter(" ");
552
+ return;
553
+ }
554
+ const kittyPrintable = decodeKittyPrintable(data);
555
+ if (kittyPrintable !== undefined) {
556
+ this.insertCharacter(kittyPrintable);
557
+ return;
558
+ }
559
+ // Regular characters
560
+ if (data.charCodeAt(0) >= 32) {
561
+ this.insertCharacter(data);
562
+ }
563
+ }
564
+ layoutText(contentWidth) {
565
+ const layoutLines = [];
566
+ if (this.state.lines.length === 0 || (this.state.lines.length === 1 && this.state.lines[0] === "")) {
567
+ // Empty editor
568
+ layoutLines.push({
569
+ text: "",
570
+ hasCursor: true,
571
+ cursorPos: 0,
572
+ });
573
+ return layoutLines;
574
+ }
575
+ // Process each logical line
576
+ for (let i = 0; i < this.state.lines.length; i++) {
577
+ const line = this.state.lines[i] || "";
578
+ const isCurrentLine = i === this.state.cursorLine;
579
+ const lineVisibleWidth = visibleWidth(line);
580
+ if (lineVisibleWidth <= contentWidth) {
581
+ // Line fits in one layout line
582
+ if (isCurrentLine) {
583
+ layoutLines.push({
584
+ text: line,
585
+ hasCursor: true,
586
+ cursorPos: this.state.cursorCol,
587
+ });
588
+ }
589
+ else {
590
+ layoutLines.push({
591
+ text: line,
592
+ hasCursor: false,
593
+ });
594
+ }
595
+ }
596
+ else {
597
+ // Line needs wrapping - use word-aware wrapping
598
+ const chunks = wordWrapLine(line, contentWidth);
599
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
600
+ const chunk = chunks[chunkIndex];
601
+ if (!chunk)
602
+ continue;
603
+ const cursorPos = this.state.cursorCol;
604
+ const isLastChunk = chunkIndex === chunks.length - 1;
605
+ // Determine if cursor is in this chunk
606
+ // For word-wrapped chunks, we need to handle the case where
607
+ // cursor might be in trimmed whitespace at end of chunk
608
+ let hasCursorInChunk = false;
609
+ let adjustedCursorPos = 0;
610
+ if (isCurrentLine) {
611
+ if (isLastChunk) {
612
+ // Last chunk: cursor belongs here if >= startIndex
613
+ hasCursorInChunk = cursorPos >= chunk.startIndex;
614
+ adjustedCursorPos = cursorPos - chunk.startIndex;
615
+ }
616
+ else {
617
+ // Non-last chunk: cursor belongs here if in range [startIndex, endIndex)
618
+ // But we need to handle the visual position in the trimmed text
619
+ hasCursorInChunk = cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex;
620
+ if (hasCursorInChunk) {
621
+ adjustedCursorPos = cursorPos - chunk.startIndex;
622
+ // Clamp to text length (in case cursor was in trimmed whitespace)
623
+ if (adjustedCursorPos > chunk.text.length) {
624
+ adjustedCursorPos = chunk.text.length;
625
+ }
626
+ }
627
+ }
628
+ }
629
+ if (hasCursorInChunk) {
630
+ layoutLines.push({
631
+ text: chunk.text,
632
+ hasCursor: true,
633
+ cursorPos: adjustedCursorPos,
634
+ });
635
+ }
636
+ else {
637
+ layoutLines.push({
638
+ text: chunk.text,
639
+ hasCursor: false,
640
+ });
641
+ }
642
+ }
643
+ }
644
+ }
645
+ return layoutLines;
646
+ }
647
+ getText() {
648
+ return this.state.lines.join("\n");
649
+ }
650
+ /**
651
+ * Get text with paste markers expanded to their actual content.
652
+ * Use this when you need the full content (e.g., for external editor).
653
+ */
654
+ getExpandedText() {
655
+ let result = this.state.lines.join("\n");
656
+ for (const [pasteId, pasteContent] of this.pastes) {
657
+ const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
658
+ result = result.replace(markerRegex, pasteContent);
659
+ }
660
+ return result;
661
+ }
662
+ getLines() {
663
+ return [...this.state.lines];
664
+ }
665
+ getCursor() {
666
+ return { line: this.state.cursorLine, col: this.state.cursorCol };
667
+ }
668
+ setText(text) {
669
+ this.lastAction = null;
670
+ this.historyIndex = -1; // Exit history browsing mode
671
+ // Push undo snapshot if content differs (makes programmatic changes undoable)
672
+ if (this.getText() !== text) {
673
+ this.pushUndoSnapshot();
674
+ }
675
+ this.setTextInternal(text);
676
+ }
677
+ /**
678
+ * Insert text at the current cursor position.
679
+ * Used for programmatic insertion (e.g., clipboard image markers).
680
+ * This is atomic for undo - single undo restores entire pre-insert state.
681
+ */
682
+ insertTextAtCursor(text) {
683
+ if (!text)
684
+ return;
685
+ this.pushUndoSnapshot();
686
+ this.lastAction = null;
687
+ this.historyIndex = -1;
688
+ this.insertTextAtCursorInternal(text);
689
+ }
690
+ /**
691
+ * Internal text insertion at cursor. Handles single and multi-line text.
692
+ * Does not push undo snapshots or trigger autocomplete - caller is responsible.
693
+ * Normalizes line endings and calls onChange once at the end.
694
+ */
695
+ insertTextAtCursorInternal(text) {
696
+ if (!text)
697
+ return;
698
+ // Normalize line endings
699
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
700
+ const insertedLines = normalized.split("\n");
701
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
702
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
703
+ const afterCursor = currentLine.slice(this.state.cursorCol);
704
+ if (insertedLines.length === 1) {
705
+ // Single line - insert at cursor position
706
+ this.state.lines[this.state.cursorLine] = beforeCursor + normalized + afterCursor;
707
+ this.state.cursorCol += normalized.length;
708
+ }
709
+ else {
710
+ // Multi-line insertion
711
+ this.state.lines = [
712
+ // All lines before current line
713
+ ...this.state.lines.slice(0, this.state.cursorLine),
714
+ // The first inserted line merged with text before cursor
715
+ beforeCursor + insertedLines[0],
716
+ // All middle inserted lines
717
+ ...insertedLines.slice(1, -1),
718
+ // The last inserted line with text after cursor
719
+ insertedLines[insertedLines.length - 1] + afterCursor,
720
+ // All lines after current line
721
+ ...this.state.lines.slice(this.state.cursorLine + 1),
722
+ ];
723
+ this.state.cursorLine += insertedLines.length - 1;
724
+ this.state.cursorCol = (insertedLines[insertedLines.length - 1] || "").length;
725
+ }
726
+ if (this.onChange) {
727
+ this.onChange(this.getText());
728
+ }
729
+ }
730
+ // All the editor methods from before...
731
+ insertCharacter(char, skipUndoCoalescing) {
732
+ this.historyIndex = -1; // Exit history browsing mode
733
+ // Undo coalescing (fish-style):
734
+ // - Consecutive word chars coalesce into one undo unit
735
+ // - Space captures state before itself (so undo removes space+following word together)
736
+ // - Each space is separately undoable
737
+ // Skip coalescing when called from atomic operations (e.g., handlePaste)
738
+ if (!skipUndoCoalescing) {
739
+ if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
740
+ this.pushUndoSnapshot();
741
+ }
742
+ this.lastAction = "type-word";
743
+ }
744
+ const line = this.state.lines[this.state.cursorLine] || "";
745
+ const before = line.slice(0, this.state.cursorCol);
746
+ const after = line.slice(this.state.cursorCol);
747
+ this.state.lines[this.state.cursorLine] = before + char + after;
748
+ this.state.cursorCol += char.length; // Fix: increment by the length of the inserted string
749
+ if (this.onChange) {
750
+ this.onChange(this.getText());
751
+ }
752
+ // Check if we should trigger or update autocomplete
753
+ if (!this.isAutocompleting) {
754
+ // Auto-trigger for "/" at the start of a line (slash commands)
755
+ if (char === "/" && this.isAtStartOfMessage()) {
756
+ this.tryTriggerAutocomplete();
757
+ }
758
+ // Auto-trigger for "@" file reference (fuzzy search)
759
+ else if (char === "@") {
760
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
761
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
762
+ // Only trigger if @ is after whitespace or at start of line
763
+ const charBeforeAt = textBeforeCursor[textBeforeCursor.length - 2];
764
+ if (textBeforeCursor.length === 1 || charBeforeAt === " " || charBeforeAt === "\t") {
765
+ this.tryTriggerAutocomplete();
766
+ }
767
+ }
768
+ // Also auto-trigger when typing letters in a slash command context
769
+ else if (/[a-zA-Z0-9.\-_]/.test(char)) {
770
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
771
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
772
+ // Check if we're in a slash command (with or without space for arguments)
773
+ if (this.isInSlashCommandContext(textBeforeCursor)) {
774
+ this.tryTriggerAutocomplete();
775
+ }
776
+ // Check if we're in an @ file reference context
777
+ else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
778
+ this.tryTriggerAutocomplete();
779
+ }
780
+ }
781
+ }
782
+ else {
783
+ this.updateAutocomplete();
784
+ }
785
+ }
786
+ handlePaste(pastedText) {
787
+ this.historyIndex = -1; // Exit history browsing mode
788
+ this.lastAction = null;
789
+ this.pushUndoSnapshot();
790
+ // Clean the pasted text
791
+ const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
792
+ // Convert tabs to spaces (4 spaces per tab)
793
+ const tabExpandedText = cleanText.replace(/\t/g, " ");
794
+ // Filter out non-printable characters except newlines
795
+ let filteredText = tabExpandedText
796
+ .split("")
797
+ .filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
798
+ .join("");
799
+ // If pasting a file path (starts with /, ~, or .) and the character before
800
+ // the cursor is a word character, prepend a space for better readability
801
+ if (/^[/~.]/.test(filteredText)) {
802
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
803
+ const charBeforeCursor = this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : "";
804
+ if (charBeforeCursor && /\w/.test(charBeforeCursor)) {
805
+ filteredText = ` ${filteredText}`;
806
+ }
807
+ }
808
+ // Split into lines to check for large paste
809
+ const pastedLines = filteredText.split("\n");
810
+ // Check if this is a large paste (> 10 lines or > 1000 characters)
811
+ const totalChars = filteredText.length;
812
+ if (pastedLines.length > 10 || totalChars > 1000) {
813
+ // Store the paste and insert a marker
814
+ this.pasteCounter++;
815
+ const pasteId = this.pasteCounter;
816
+ this.pastes.set(pasteId, filteredText);
817
+ // Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]"
818
+ const marker = pastedLines.length > 10
819
+ ? `[paste #${pasteId} +${pastedLines.length} lines]`
820
+ : `[paste #${pasteId} ${totalChars} chars]`;
821
+ this.insertTextAtCursorInternal(marker);
822
+ return;
823
+ }
824
+ if (pastedLines.length === 1) {
825
+ // Single line - insert character by character to trigger autocomplete
826
+ for (const char of filteredText) {
827
+ this.insertCharacter(char, true);
828
+ }
829
+ return;
830
+ }
831
+ // Multi-line paste - use direct state manipulation
832
+ this.insertTextAtCursorInternal(filteredText);
833
+ }
834
+ addNewLine() {
835
+ this.historyIndex = -1; // Exit history browsing mode
836
+ this.lastAction = null;
837
+ this.pushUndoSnapshot();
838
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
839
+ const before = currentLine.slice(0, this.state.cursorCol);
840
+ const after = currentLine.slice(this.state.cursorCol);
841
+ // Split current line
842
+ this.state.lines[this.state.cursorLine] = before;
843
+ this.state.lines.splice(this.state.cursorLine + 1, 0, after);
844
+ // Move cursor to start of new line
845
+ this.state.cursorLine++;
846
+ this.state.cursorCol = 0;
847
+ if (this.onChange) {
848
+ this.onChange(this.getText());
849
+ }
850
+ }
851
+ handleBackspace() {
852
+ this.historyIndex = -1; // Exit history browsing mode
853
+ this.lastAction = null;
854
+ if (this.state.cursorCol > 0) {
855
+ this.pushUndoSnapshot();
856
+ // Delete grapheme before cursor (handles emojis, combining characters, etc.)
857
+ const line = this.state.lines[this.state.cursorLine] || "";
858
+ const beforeCursor = line.slice(0, this.state.cursorCol);
859
+ // Find the last grapheme in the text before cursor
860
+ const graphemes = [...segmenter.segment(beforeCursor)];
861
+ const lastGrapheme = graphemes[graphemes.length - 1];
862
+ const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
863
+ const before = line.slice(0, this.state.cursorCol - graphemeLength);
864
+ const after = line.slice(this.state.cursorCol);
865
+ this.state.lines[this.state.cursorLine] = before + after;
866
+ this.state.cursorCol -= graphemeLength;
867
+ }
868
+ else if (this.state.cursorLine > 0) {
869
+ this.pushUndoSnapshot();
870
+ // Merge with previous line
871
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
872
+ const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
873
+ this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
874
+ this.state.lines.splice(this.state.cursorLine, 1);
875
+ this.state.cursorLine--;
876
+ this.state.cursorCol = previousLine.length;
877
+ }
878
+ if (this.onChange) {
879
+ this.onChange(this.getText());
880
+ }
881
+ // Update or re-trigger autocomplete after backspace
882
+ if (this.isAutocompleting) {
883
+ this.updateAutocomplete();
884
+ }
885
+ else {
886
+ // If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
887
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
888
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
889
+ // Slash command context
890
+ if (this.isInSlashCommandContext(textBeforeCursor)) {
891
+ this.tryTriggerAutocomplete();
892
+ }
893
+ // @ file reference context
894
+ else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
895
+ this.tryTriggerAutocomplete();
896
+ }
897
+ }
898
+ }
899
+ moveToLineStart() {
900
+ this.lastAction = null;
901
+ this.state.cursorCol = 0;
902
+ }
903
+ moveToLineEnd() {
904
+ this.lastAction = null;
905
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
906
+ this.state.cursorCol = currentLine.length;
907
+ }
908
+ deleteToStartOfLine() {
909
+ this.historyIndex = -1; // Exit history browsing mode
910
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
911
+ if (this.state.cursorCol > 0) {
912
+ this.pushUndoSnapshot();
913
+ // Calculate text to be deleted and save to kill ring (backward deletion = prepend)
914
+ const deletedText = currentLine.slice(0, this.state.cursorCol);
915
+ this.addToKillRing(deletedText, true);
916
+ this.lastAction = "kill";
917
+ // Delete from start of line up to cursor
918
+ this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
919
+ this.state.cursorCol = 0;
920
+ }
921
+ else if (this.state.cursorLine > 0) {
922
+ this.pushUndoSnapshot();
923
+ // At start of line - merge with previous line, treating newline as deleted text
924
+ this.addToKillRing("\n", true);
925
+ this.lastAction = "kill";
926
+ const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
927
+ this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
928
+ this.state.lines.splice(this.state.cursorLine, 1);
929
+ this.state.cursorLine--;
930
+ this.state.cursorCol = previousLine.length;
931
+ }
932
+ if (this.onChange) {
933
+ this.onChange(this.getText());
934
+ }
935
+ }
936
+ deleteToEndOfLine() {
937
+ this.historyIndex = -1; // Exit history browsing mode
938
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
939
+ if (this.state.cursorCol < currentLine.length) {
940
+ this.pushUndoSnapshot();
941
+ // Calculate text to be deleted and save to kill ring (forward deletion = append)
942
+ const deletedText = currentLine.slice(this.state.cursorCol);
943
+ this.addToKillRing(deletedText, false);
944
+ this.lastAction = "kill";
945
+ // Delete from cursor to end of line
946
+ this.state.lines[this.state.cursorLine] = currentLine.slice(0, this.state.cursorCol);
947
+ }
948
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
949
+ this.pushUndoSnapshot();
950
+ // At end of line - merge with next line, treating newline as deleted text
951
+ this.addToKillRing("\n", false);
952
+ this.lastAction = "kill";
953
+ const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
954
+ this.state.lines[this.state.cursorLine] = currentLine + nextLine;
955
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
956
+ }
957
+ if (this.onChange) {
958
+ this.onChange(this.getText());
959
+ }
960
+ }
961
+ deleteWordBackwards() {
962
+ this.historyIndex = -1; // Exit history browsing mode
963
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
964
+ // If at start of line, behave like backspace at column 0 (merge with previous line)
965
+ if (this.state.cursorCol === 0) {
966
+ if (this.state.cursorLine > 0) {
967
+ this.pushUndoSnapshot();
968
+ // Treat newline as deleted text (backward deletion = prepend)
969
+ this.addToKillRing("\n", true);
970
+ this.lastAction = "kill";
971
+ const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
972
+ this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
973
+ this.state.lines.splice(this.state.cursorLine, 1);
974
+ this.state.cursorLine--;
975
+ this.state.cursorCol = previousLine.length;
976
+ }
977
+ }
978
+ else {
979
+ this.pushUndoSnapshot();
980
+ // Save lastAction before cursor movement (moveWordBackwards resets it)
981
+ const wasKill = this.lastAction === "kill";
982
+ const oldCursorCol = this.state.cursorCol;
983
+ this.moveWordBackwards();
984
+ const deleteFrom = this.state.cursorCol;
985
+ this.state.cursorCol = oldCursorCol;
986
+ // Restore kill state for accumulation check, then save to kill ring
987
+ this.lastAction = wasKill ? "kill" : null;
988
+ const deletedText = currentLine.slice(deleteFrom, this.state.cursorCol);
989
+ this.addToKillRing(deletedText, true);
990
+ this.lastAction = "kill";
991
+ this.state.lines[this.state.cursorLine] =
992
+ currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
993
+ this.state.cursorCol = deleteFrom;
994
+ }
995
+ if (this.onChange) {
996
+ this.onChange(this.getText());
997
+ }
998
+ }
999
+ deleteWordForward() {
1000
+ this.historyIndex = -1; // Exit history browsing mode
1001
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1002
+ // If at end of line, merge with next line (delete the newline)
1003
+ if (this.state.cursorCol >= currentLine.length) {
1004
+ if (this.state.cursorLine < this.state.lines.length - 1) {
1005
+ this.pushUndoSnapshot();
1006
+ // Treat newline as deleted text (forward deletion = append)
1007
+ this.addToKillRing("\n", false);
1008
+ this.lastAction = "kill";
1009
+ const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
1010
+ this.state.lines[this.state.cursorLine] = currentLine + nextLine;
1011
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
1012
+ }
1013
+ }
1014
+ else {
1015
+ this.pushUndoSnapshot();
1016
+ // Save lastAction before cursor movement (moveWordForwards resets it)
1017
+ const wasKill = this.lastAction === "kill";
1018
+ const oldCursorCol = this.state.cursorCol;
1019
+ this.moveWordForwards();
1020
+ const deleteTo = this.state.cursorCol;
1021
+ this.state.cursorCol = oldCursorCol;
1022
+ // Restore kill state for accumulation check, then save to kill ring
1023
+ this.lastAction = wasKill ? "kill" : null;
1024
+ const deletedText = currentLine.slice(this.state.cursorCol, deleteTo);
1025
+ this.addToKillRing(deletedText, false);
1026
+ this.lastAction = "kill";
1027
+ this.state.lines[this.state.cursorLine] =
1028
+ currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
1029
+ }
1030
+ if (this.onChange) {
1031
+ this.onChange(this.getText());
1032
+ }
1033
+ }
1034
+ handleForwardDelete() {
1035
+ this.historyIndex = -1; // Exit history browsing mode
1036
+ this.lastAction = null;
1037
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1038
+ if (this.state.cursorCol < currentLine.length) {
1039
+ this.pushUndoSnapshot();
1040
+ // Delete grapheme at cursor position (handles emojis, combining characters, etc.)
1041
+ const afterCursor = currentLine.slice(this.state.cursorCol);
1042
+ // Find the first grapheme at cursor
1043
+ const graphemes = [...segmenter.segment(afterCursor)];
1044
+ const firstGrapheme = graphemes[0];
1045
+ const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
1046
+ const before = currentLine.slice(0, this.state.cursorCol);
1047
+ const after = currentLine.slice(this.state.cursorCol + graphemeLength);
1048
+ this.state.lines[this.state.cursorLine] = before + after;
1049
+ }
1050
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
1051
+ this.pushUndoSnapshot();
1052
+ // At end of line - merge with next line
1053
+ const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
1054
+ this.state.lines[this.state.cursorLine] = currentLine + nextLine;
1055
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
1056
+ }
1057
+ if (this.onChange) {
1058
+ this.onChange(this.getText());
1059
+ }
1060
+ // Update or re-trigger autocomplete after forward delete
1061
+ if (this.isAutocompleting) {
1062
+ this.updateAutocomplete();
1063
+ }
1064
+ else {
1065
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1066
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1067
+ // Slash command context
1068
+ if (this.isInSlashCommandContext(textBeforeCursor)) {
1069
+ this.tryTriggerAutocomplete();
1070
+ }
1071
+ // @ file reference context
1072
+ else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1073
+ this.tryTriggerAutocomplete();
1074
+ }
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Build a mapping from visual lines to logical positions.
1079
+ * Returns an array where each element represents a visual line with:
1080
+ * - logicalLine: index into this.state.lines
1081
+ * - startCol: starting column in the logical line
1082
+ * - length: length of this visual line segment
1083
+ */
1084
+ buildVisualLineMap(width) {
1085
+ const visualLines = [];
1086
+ for (let i = 0; i < this.state.lines.length; i++) {
1087
+ const line = this.state.lines[i] || "";
1088
+ const lineVisWidth = visibleWidth(line);
1089
+ if (line.length === 0) {
1090
+ // Empty line still takes one visual line
1091
+ visualLines.push({ logicalLine: i, startCol: 0, length: 0 });
1092
+ }
1093
+ else if (lineVisWidth <= width) {
1094
+ visualLines.push({ logicalLine: i, startCol: 0, length: line.length });
1095
+ }
1096
+ else {
1097
+ // Line needs wrapping - use word-aware wrapping
1098
+ const chunks = wordWrapLine(line, width);
1099
+ for (const chunk of chunks) {
1100
+ visualLines.push({
1101
+ logicalLine: i,
1102
+ startCol: chunk.startIndex,
1103
+ length: chunk.endIndex - chunk.startIndex,
1104
+ });
1105
+ }
1106
+ }
1107
+ }
1108
+ return visualLines;
1109
+ }
1110
+ /**
1111
+ * Find the visual line index for the current cursor position.
1112
+ */
1113
+ findCurrentVisualLine(visualLines) {
1114
+ for (let i = 0; i < visualLines.length; i++) {
1115
+ const vl = visualLines[i];
1116
+ if (!vl)
1117
+ continue;
1118
+ if (vl.logicalLine === this.state.cursorLine) {
1119
+ const colInSegment = this.state.cursorCol - vl.startCol;
1120
+ // Cursor is in this segment if it's within range
1121
+ // For the last segment of a logical line, cursor can be at length (end position)
1122
+ const isLastSegmentOfLine = i === visualLines.length - 1 || visualLines[i + 1]?.logicalLine !== vl.logicalLine;
1123
+ if (colInSegment >= 0 && (colInSegment < vl.length || (isLastSegmentOfLine && colInSegment <= vl.length))) {
1124
+ return i;
1125
+ }
1126
+ }
1127
+ }
1128
+ // Fallback: return last visual line
1129
+ return visualLines.length - 1;
1130
+ }
1131
+ moveCursor(deltaLine, deltaCol) {
1132
+ this.lastAction = null;
1133
+ const width = this.lastWidth;
1134
+ if (deltaLine !== 0) {
1135
+ // Build visual line map for navigation
1136
+ const visualLines = this.buildVisualLineMap(width);
1137
+ const currentVisualLine = this.findCurrentVisualLine(visualLines);
1138
+ // Calculate column position within current visual line
1139
+ const currentVL = visualLines[currentVisualLine];
1140
+ const visualCol = currentVL ? this.state.cursorCol - currentVL.startCol : 0;
1141
+ // Move to target visual line
1142
+ const targetVisualLine = currentVisualLine + deltaLine;
1143
+ if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) {
1144
+ const targetVL = visualLines[targetVisualLine];
1145
+ if (targetVL) {
1146
+ this.state.cursorLine = targetVL.logicalLine;
1147
+ // Try to maintain visual column position, clamped to line length
1148
+ const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
1149
+ const logicalLine = this.state.lines[targetVL.logicalLine] || "";
1150
+ this.state.cursorCol = Math.min(targetCol, logicalLine.length);
1151
+ }
1152
+ }
1153
+ }
1154
+ if (deltaCol !== 0) {
1155
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1156
+ if (deltaCol > 0) {
1157
+ // Moving right - move by one grapheme (handles emojis, combining characters, etc.)
1158
+ if (this.state.cursorCol < currentLine.length) {
1159
+ const afterCursor = currentLine.slice(this.state.cursorCol);
1160
+ const graphemes = [...segmenter.segment(afterCursor)];
1161
+ const firstGrapheme = graphemes[0];
1162
+ this.state.cursorCol += firstGrapheme ? firstGrapheme.segment.length : 1;
1163
+ }
1164
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
1165
+ // Wrap to start of next logical line
1166
+ this.state.cursorLine++;
1167
+ this.state.cursorCol = 0;
1168
+ }
1169
+ }
1170
+ else {
1171
+ // Moving left - move by one grapheme (handles emojis, combining characters, etc.)
1172
+ if (this.state.cursorCol > 0) {
1173
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
1174
+ const graphemes = [...segmenter.segment(beforeCursor)];
1175
+ const lastGrapheme = graphemes[graphemes.length - 1];
1176
+ this.state.cursorCol -= lastGrapheme ? lastGrapheme.segment.length : 1;
1177
+ }
1178
+ else if (this.state.cursorLine > 0) {
1179
+ // Wrap to end of previous logical line
1180
+ this.state.cursorLine--;
1181
+ const prevLine = this.state.lines[this.state.cursorLine] || "";
1182
+ this.state.cursorCol = prevLine.length;
1183
+ }
1184
+ }
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Scroll by a page (direction: -1 for up, 1 for down).
1189
+ * Moves cursor by the page size while keeping it in bounds.
1190
+ */
1191
+ pageScroll(direction) {
1192
+ this.lastAction = null;
1193
+ const width = this.lastWidth;
1194
+ const terminalRows = this.tui.terminal.rows;
1195
+ const pageSize = Math.max(5, Math.floor(terminalRows * 0.3));
1196
+ // Build visual line map
1197
+ const visualLines = this.buildVisualLineMap(width);
1198
+ const currentVisualLine = this.findCurrentVisualLine(visualLines);
1199
+ // Calculate target visual line
1200
+ const targetVisualLine = Math.max(0, Math.min(visualLines.length - 1, currentVisualLine + direction * pageSize));
1201
+ // Move cursor to target visual line
1202
+ const targetVL = visualLines[targetVisualLine];
1203
+ if (targetVL) {
1204
+ // Preserve column position within the line
1205
+ const currentVL = visualLines[currentVisualLine];
1206
+ const visualCol = currentVL ? this.state.cursorCol - currentVL.startCol : 0;
1207
+ this.state.cursorLine = targetVL.logicalLine;
1208
+ const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
1209
+ const logicalLine = this.state.lines[targetVL.logicalLine] || "";
1210
+ this.state.cursorCol = Math.min(targetCol, logicalLine.length);
1211
+ }
1212
+ }
1213
+ moveWordBackwards() {
1214
+ this.lastAction = null;
1215
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1216
+ // If at start of line, move to end of previous line
1217
+ if (this.state.cursorCol === 0) {
1218
+ if (this.state.cursorLine > 0) {
1219
+ this.state.cursorLine--;
1220
+ const prevLine = this.state.lines[this.state.cursorLine] || "";
1221
+ this.state.cursorCol = prevLine.length;
1222
+ }
1223
+ return;
1224
+ }
1225
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1226
+ const graphemes = [...segmenter.segment(textBeforeCursor)];
1227
+ let newCol = this.state.cursorCol;
1228
+ // Skip trailing whitespace
1229
+ while (graphemes.length > 0 && isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
1230
+ newCol -= graphemes.pop()?.segment.length || 0;
1231
+ }
1232
+ if (graphemes.length > 0) {
1233
+ const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
1234
+ if (isPunctuationChar(lastGrapheme)) {
1235
+ // Skip punctuation run
1236
+ while (graphemes.length > 0 && isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")) {
1237
+ newCol -= graphemes.pop()?.segment.length || 0;
1238
+ }
1239
+ }
1240
+ else {
1241
+ // Skip word run
1242
+ while (graphemes.length > 0 &&
1243
+ !isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
1244
+ !isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")) {
1245
+ newCol -= graphemes.pop()?.segment.length || 0;
1246
+ }
1247
+ }
1248
+ }
1249
+ this.state.cursorCol = newCol;
1250
+ }
1251
+ /**
1252
+ * Yank (paste) the most recent kill ring entry at cursor position.
1253
+ */
1254
+ yank() {
1255
+ if (this.killRing.length === 0)
1256
+ return;
1257
+ this.pushUndoSnapshot();
1258
+ const text = this.killRing[this.killRing.length - 1] || "";
1259
+ this.insertYankedText(text);
1260
+ this.lastAction = "yank";
1261
+ }
1262
+ /**
1263
+ * Cycle through kill ring (only works immediately after yank or yank-pop).
1264
+ * Replaces the last yanked text with the previous entry in the ring.
1265
+ */
1266
+ yankPop() {
1267
+ // Only works if we just yanked and have more than one entry
1268
+ if (this.lastAction !== "yank" || this.killRing.length <= 1)
1269
+ return;
1270
+ this.pushUndoSnapshot();
1271
+ // Delete the previously yanked text (still at end of ring before rotation)
1272
+ this.deleteYankedText();
1273
+ // Rotate the ring: move end to front
1274
+ const lastEntry = this.killRing.pop();
1275
+ this.killRing.unshift(lastEntry);
1276
+ // Insert the new most recent entry (now at end after rotation)
1277
+ const text = this.killRing[this.killRing.length - 1];
1278
+ this.insertYankedText(text);
1279
+ this.lastAction = "yank";
1280
+ }
1281
+ /**
1282
+ * Insert text at cursor position (used by yank operations).
1283
+ */
1284
+ insertYankedText(text) {
1285
+ this.historyIndex = -1; // Exit history browsing mode
1286
+ const lines = text.split("\n");
1287
+ if (lines.length === 1) {
1288
+ // Single line - insert at cursor
1289
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1290
+ const before = currentLine.slice(0, this.state.cursorCol);
1291
+ const after = currentLine.slice(this.state.cursorCol);
1292
+ this.state.lines[this.state.cursorLine] = before + text + after;
1293
+ this.state.cursorCol += text.length;
1294
+ }
1295
+ else {
1296
+ // Multi-line insert
1297
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1298
+ const before = currentLine.slice(0, this.state.cursorCol);
1299
+ const after = currentLine.slice(this.state.cursorCol);
1300
+ // First line merges with text before cursor
1301
+ this.state.lines[this.state.cursorLine] = before + (lines[0] || "");
1302
+ // Insert middle lines
1303
+ for (let i = 1; i < lines.length - 1; i++) {
1304
+ this.state.lines.splice(this.state.cursorLine + i, 0, lines[i] || "");
1305
+ }
1306
+ // Last line merges with text after cursor
1307
+ const lastLineIndex = this.state.cursorLine + lines.length - 1;
1308
+ this.state.lines.splice(lastLineIndex, 0, (lines[lines.length - 1] || "") + after);
1309
+ // Update cursor position
1310
+ this.state.cursorLine = lastLineIndex;
1311
+ this.state.cursorCol = (lines[lines.length - 1] || "").length;
1312
+ }
1313
+ if (this.onChange) {
1314
+ this.onChange(this.getText());
1315
+ }
1316
+ }
1317
+ /**
1318
+ * Delete the previously yanked text (used by yank-pop).
1319
+ * The yanked text is derived from killRing[end] since it hasn't been rotated yet.
1320
+ */
1321
+ deleteYankedText() {
1322
+ const yankedText = this.killRing[this.killRing.length - 1] || "";
1323
+ if (!yankedText)
1324
+ return;
1325
+ const yankLines = yankedText.split("\n");
1326
+ if (yankLines.length === 1) {
1327
+ // Single line - delete backward from cursor
1328
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1329
+ const deleteLen = yankedText.length;
1330
+ const before = currentLine.slice(0, this.state.cursorCol - deleteLen);
1331
+ const after = currentLine.slice(this.state.cursorCol);
1332
+ this.state.lines[this.state.cursorLine] = before + after;
1333
+ this.state.cursorCol -= deleteLen;
1334
+ }
1335
+ else {
1336
+ // Multi-line delete - cursor is at end of last yanked line
1337
+ const startLine = this.state.cursorLine - (yankLines.length - 1);
1338
+ const startCol = (this.state.lines[startLine] || "").length - (yankLines[0] || "").length;
1339
+ // Get text after cursor on current line
1340
+ const afterCursor = (this.state.lines[this.state.cursorLine] || "").slice(this.state.cursorCol);
1341
+ // Get text before yank start position
1342
+ const beforeYank = (this.state.lines[startLine] || "").slice(0, startCol);
1343
+ // Remove all lines from startLine to cursorLine and replace with merged line
1344
+ this.state.lines.splice(startLine, yankLines.length, beforeYank + afterCursor);
1345
+ // Update cursor
1346
+ this.state.cursorLine = startLine;
1347
+ this.state.cursorCol = startCol;
1348
+ }
1349
+ if (this.onChange) {
1350
+ this.onChange(this.getText());
1351
+ }
1352
+ }
1353
+ /**
1354
+ * Add text to the kill ring.
1355
+ * If lastAction is "kill", accumulates with the previous entry.
1356
+ * @param text - The text to add
1357
+ * @param prepend - If accumulating, prepend (true) or append (false) to existing entry
1358
+ */
1359
+ addToKillRing(text, prepend) {
1360
+ if (!text)
1361
+ return;
1362
+ if (this.lastAction === "kill" && this.killRing.length > 0) {
1363
+ // Accumulate with the most recent entry (at end of array)
1364
+ const lastEntry = this.killRing.pop();
1365
+ if (prepend) {
1366
+ this.killRing.push(text + lastEntry);
1367
+ }
1368
+ else {
1369
+ this.killRing.push(lastEntry + text);
1370
+ }
1371
+ }
1372
+ else {
1373
+ // Add new entry to end of ring
1374
+ this.killRing.push(text);
1375
+ }
1376
+ }
1377
+ captureUndoSnapshot() {
1378
+ return structuredClone(this.state);
1379
+ }
1380
+ restoreUndoSnapshot(snapshot) {
1381
+ Object.assign(this.state, structuredClone(snapshot));
1382
+ }
1383
+ pushUndoSnapshot() {
1384
+ this.undoStack.push(this.captureUndoSnapshot());
1385
+ }
1386
+ undo() {
1387
+ this.historyIndex = -1; // Exit history browsing mode
1388
+ if (this.undoStack.length === 0)
1389
+ return;
1390
+ const snapshot = this.undoStack.pop();
1391
+ this.restoreUndoSnapshot(snapshot);
1392
+ this.lastAction = null;
1393
+ if (this.onChange) {
1394
+ this.onChange(this.getText());
1395
+ }
1396
+ }
1397
+ moveWordForwards() {
1398
+ this.lastAction = null;
1399
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1400
+ // If at end of line, move to start of next line
1401
+ if (this.state.cursorCol >= currentLine.length) {
1402
+ if (this.state.cursorLine < this.state.lines.length - 1) {
1403
+ this.state.cursorLine++;
1404
+ this.state.cursorCol = 0;
1405
+ }
1406
+ return;
1407
+ }
1408
+ const textAfterCursor = currentLine.slice(this.state.cursorCol);
1409
+ const segments = segmenter.segment(textAfterCursor);
1410
+ const iterator = segments[Symbol.iterator]();
1411
+ let next = iterator.next();
1412
+ // Skip leading whitespace
1413
+ while (!next.done && isWhitespaceChar(next.value.segment)) {
1414
+ this.state.cursorCol += next.value.segment.length;
1415
+ next = iterator.next();
1416
+ }
1417
+ if (!next.done) {
1418
+ const firstGrapheme = next.value.segment;
1419
+ if (isPunctuationChar(firstGrapheme)) {
1420
+ // Skip punctuation run
1421
+ while (!next.done && isPunctuationChar(next.value.segment)) {
1422
+ this.state.cursorCol += next.value.segment.length;
1423
+ next = iterator.next();
1424
+ }
1425
+ }
1426
+ else {
1427
+ // Skip word run
1428
+ while (!next.done && !isWhitespaceChar(next.value.segment) && !isPunctuationChar(next.value.segment)) {
1429
+ this.state.cursorCol += next.value.segment.length;
1430
+ next = iterator.next();
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ // Slash menu only allowed when all other lines are empty (no mixed content)
1436
+ isSlashMenuAllowed() {
1437
+ for (let i = 0; i < this.state.lines.length; i++) {
1438
+ if (i === this.state.cursorLine)
1439
+ continue;
1440
+ if (this.state.lines[i].trim() !== "")
1441
+ return false;
1442
+ }
1443
+ return true;
1444
+ }
1445
+ // Helper method to check if cursor is at start of message (for slash command detection)
1446
+ isAtStartOfMessage() {
1447
+ if (!this.isSlashMenuAllowed())
1448
+ return false;
1449
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1450
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
1451
+ return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
1452
+ }
1453
+ isInSlashCommandContext(textBeforeCursor) {
1454
+ return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
1455
+ }
1456
+ // Autocomplete methods
1457
+ tryTriggerAutocomplete(explicitTab = false) {
1458
+ if (!this.autocompleteProvider)
1459
+ return;
1460
+ // Check if we should trigger file completion on Tab
1461
+ if (explicitTab) {
1462
+ const provider = this.autocompleteProvider;
1463
+ const shouldTrigger = !provider.shouldTriggerFileCompletion ||
1464
+ provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1465
+ if (!shouldTrigger) {
1466
+ return;
1467
+ }
1468
+ }
1469
+ const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1470
+ if (suggestions && suggestions.items.length > 0) {
1471
+ this.autocompletePrefix = suggestions.prefix;
1472
+ this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1473
+ this.isAutocompleting = true;
1474
+ }
1475
+ else {
1476
+ this.cancelAutocomplete();
1477
+ }
1478
+ }
1479
+ handleTabCompletion() {
1480
+ if (!this.autocompleteProvider)
1481
+ return;
1482
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
1483
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
1484
+ // Check if we're in a slash command context
1485
+ if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
1486
+ this.handleSlashCommandCompletion();
1487
+ }
1488
+ else {
1489
+ this.forceFileAutocomplete();
1490
+ }
1491
+ }
1492
+ handleSlashCommandCompletion() {
1493
+ this.tryTriggerAutocomplete(true);
1494
+ }
1495
+ /*
1496
+ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
1497
+ 17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
1498
+ 536643416/job/55932288317 havea look at .gi
1499
+ */
1500
+ forceFileAutocomplete() {
1501
+ if (!this.autocompleteProvider)
1502
+ return;
1503
+ // Check if provider supports force file suggestions via runtime check
1504
+ const provider = this.autocompleteProvider;
1505
+ if (typeof provider.getForceFileSuggestions !== "function") {
1506
+ this.tryTriggerAutocomplete(true);
1507
+ return;
1508
+ }
1509
+ const suggestions = provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1510
+ if (suggestions && suggestions.items.length > 0) {
1511
+ this.autocompletePrefix = suggestions.prefix;
1512
+ this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1513
+ this.isAutocompleting = true;
1514
+ }
1515
+ else {
1516
+ this.cancelAutocomplete();
1517
+ }
1518
+ }
1519
+ cancelAutocomplete() {
1520
+ this.isAutocompleting = false;
1521
+ this.autocompleteList = undefined;
1522
+ this.autocompletePrefix = "";
1523
+ }
1524
+ isShowingAutocomplete() {
1525
+ return this.isAutocompleting;
1526
+ }
1527
+ updateAutocomplete() {
1528
+ if (!this.isAutocompleting || !this.autocompleteProvider)
1529
+ return;
1530
+ const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
1531
+ if (suggestions && suggestions.items.length > 0) {
1532
+ this.autocompletePrefix = suggestions.prefix;
1533
+ // Always create new SelectList to ensure update
1534
+ this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1535
+ }
1536
+ else {
1537
+ this.cancelAutocomplete();
1538
+ }
1539
+ }
1540
+ }