@vascend/talos 0.1.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 (620) hide show
  1. package/README.md +202 -0
  2. package/dist/backend/asset-events.d.ts +31 -0
  3. package/dist/backend/asset-events.js +149 -0
  4. package/dist/backend/assets.d.ts +31 -0
  5. package/dist/backend/assets.js +210 -0
  6. package/dist/backend/auth.d.ts +18 -0
  7. package/dist/backend/auth.js +59 -0
  8. package/dist/backend/config.d.ts +14 -0
  9. package/dist/backend/config.js +41 -0
  10. package/dist/backend/conversations.d.ts +21 -0
  11. package/dist/backend/conversations.js +141 -0
  12. package/dist/backend/crypto.d.ts +9 -0
  13. package/dist/backend/crypto.js +68 -0
  14. package/dist/backend/engine.d.ts +13 -0
  15. package/dist/backend/engine.js +78 -0
  16. package/dist/backend/http.d.ts +25 -0
  17. package/dist/backend/http.js +112 -0
  18. package/dist/backend/index.d.ts +1 -0
  19. package/dist/backend/index.js +24 -0
  20. package/dist/backend/routes.d.ts +14 -0
  21. package/dist/backend/routes.js +169 -0
  22. package/dist/backend/sdk.d.ts +12 -0
  23. package/dist/backend/sdk.js +11 -0
  24. package/dist/backend/server.d.ts +12 -0
  25. package/dist/backend/server.js +91 -0
  26. package/dist/backend/temp-image-store.d.ts +46 -0
  27. package/dist/backend/temp-image-store.js +197 -0
  28. package/dist/backend/types.d.ts +103 -0
  29. package/dist/backend/types.js +29 -0
  30. package/dist/backend/users.d.ts +13 -0
  31. package/dist/backend/users.js +76 -0
  32. package/dist/backend/ws-handler.d.ts +14 -0
  33. package/dist/backend/ws-handler.js +72 -0
  34. package/dist/backend/ws.d.ts +19 -0
  35. package/dist/backend/ws.js +146 -0
  36. package/dist/core/agent.d.ts +56 -0
  37. package/dist/core/agent.js +1783 -0
  38. package/dist/core/background.d.ts +45 -0
  39. package/dist/core/background.js +140 -0
  40. package/dist/core/bootstrap.d.ts +45 -0
  41. package/dist/core/bootstrap.js +294 -0
  42. package/dist/core/cli-args.d.ts +41 -0
  43. package/dist/core/cli-args.js +282 -0
  44. package/dist/core/collapsible-output.d.ts +38 -0
  45. package/dist/core/collapsible-output.js +143 -0
  46. package/dist/core/compaction-checkpoint.d.ts +9 -0
  47. package/dist/core/compaction-checkpoint.js +58 -0
  48. package/dist/core/compaction.d.ts +42 -0
  49. package/dist/core/compaction.js +270 -0
  50. package/dist/core/config.d.ts +49 -0
  51. package/dist/core/config.js +417 -0
  52. package/dist/core/context-ledger.d.ts +99 -0
  53. package/dist/core/context-ledger.js +222 -0
  54. package/dist/core/context-policy.d.ts +17 -0
  55. package/dist/core/context-policy.js +81 -0
  56. package/dist/core/continuation-review.d.ts +23 -0
  57. package/dist/core/continuation-review.js +64 -0
  58. package/dist/core/crash-report.d.ts +3 -0
  59. package/dist/core/crash-report.js +27 -0
  60. package/dist/core/credentials.d.ts +11 -0
  61. package/dist/core/credentials.js +183 -0
  62. package/dist/core/doctor.d.ts +24 -0
  63. package/dist/core/doctor.js +125 -0
  64. package/dist/core/effort.d.ts +5 -0
  65. package/dist/core/effort.js +42 -0
  66. package/dist/core/env-paths.d.ts +3 -0
  67. package/dist/core/env-paths.js +38 -0
  68. package/dist/core/execution-guidance.d.ts +9 -0
  69. package/dist/core/execution-guidance.js +73 -0
  70. package/dist/core/execution-target.d.ts +41 -0
  71. package/dist/core/execution-target.js +69 -0
  72. package/dist/core/file-coordination.d.ts +66 -0
  73. package/dist/core/file-coordination.js +176 -0
  74. package/dist/core/file-lease-store.d.ts +14 -0
  75. package/dist/core/file-lease-store.js +89 -0
  76. package/dist/core/image-limits.d.ts +26 -0
  77. package/dist/core/image-limits.js +205 -0
  78. package/dist/core/interruptible-completion.d.ts +3 -0
  79. package/dist/core/interruptible-completion.js +42 -0
  80. package/dist/core/kingdoms-mode-state.d.ts +24 -0
  81. package/dist/core/kingdoms-mode-state.js +139 -0
  82. package/dist/core/licenza.d.ts +38 -0
  83. package/dist/core/licenza.js +125 -0
  84. package/dist/core/model-limits.d.ts +2 -0
  85. package/dist/core/model-limits.js +36 -0
  86. package/dist/core/output-lifecycle.d.ts +44 -0
  87. package/dist/core/output-lifecycle.js +190 -0
  88. package/dist/core/package-update.d.ts +15 -0
  89. package/dist/core/package-update.js +121 -0
  90. package/dist/core/permission-queue.d.ts +2 -0
  91. package/dist/core/permission-queue.js +20 -0
  92. package/dist/core/prompt-attachment.d.ts +21 -0
  93. package/dist/core/prompt-attachment.js +123 -0
  94. package/dist/core/request-plan.d.ts +16 -0
  95. package/dist/core/request-plan.js +36 -0
  96. package/dist/core/run-timing.d.ts +17 -0
  97. package/dist/core/run-timing.js +123 -0
  98. package/dist/core/self-update.d.ts +17 -0
  99. package/dist/core/self-update.js +54 -0
  100. package/dist/core/session-refs.d.ts +27 -0
  101. package/dist/core/session-refs.js +126 -0
  102. package/dist/core/session-storage.d.ts +4 -0
  103. package/dist/core/session-storage.js +48 -0
  104. package/dist/core/session-title.d.ts +1 -0
  105. package/dist/core/session-title.js +21 -0
  106. package/dist/core/session-usage.d.ts +17 -0
  107. package/dist/core/session-usage.js +74 -0
  108. package/dist/core/session.d.ts +58 -0
  109. package/dist/core/session.js +296 -0
  110. package/dist/core/settings.d.ts +17 -0
  111. package/dist/core/settings.js +107 -0
  112. package/dist/core/storage-retention.d.ts +21 -0
  113. package/dist/core/storage-retention.js +227 -0
  114. package/dist/core/text-sanitize.d.ts +1 -0
  115. package/dist/core/text-sanitize.js +35 -0
  116. package/dist/core/tool-artifacts.d.ts +6 -0
  117. package/dist/core/tool-artifacts.js +51 -0
  118. package/dist/core/tool-compress.d.ts +8 -0
  119. package/dist/core/tool-compress.js +111 -0
  120. package/dist/core/tool-executor.d.ts +16 -0
  121. package/dist/core/tool-executor.js +97 -0
  122. package/dist/core/tool-result-index.d.ts +52 -0
  123. package/dist/core/tool-result-index.js +327 -0
  124. package/dist/core/tool-result-store.d.ts +38 -0
  125. package/dist/core/tool-result-store.js +190 -0
  126. package/dist/core/tool-scheduler.d.ts +16 -0
  127. package/dist/core/tool-scheduler.js +35 -0
  128. package/dist/core/trust.d.ts +21 -0
  129. package/dist/core/trust.js +95 -0
  130. package/dist/core/types.d.ts +402 -0
  131. package/dist/core/types.js +1 -0
  132. package/dist/core/update-check.d.ts +26 -0
  133. package/dist/core/update-check.js +89 -0
  134. package/dist/core/update-process.d.ts +10 -0
  135. package/dist/core/update-process.js +31 -0
  136. package/dist/core/user-inbox.d.ts +23 -0
  137. package/dist/core/user-inbox.js +56 -0
  138. package/dist/core/user-input.d.ts +10 -0
  139. package/dist/core/user-input.js +35 -0
  140. package/dist/core/version.d.ts +1 -0
  141. package/dist/core/version.js +10 -0
  142. package/dist/core/vibe-modes.d.ts +38 -0
  143. package/dist/core/vibe-modes.js +383 -0
  144. package/dist/core/workspace-roots.d.ts +13 -0
  145. package/dist/core/workspace-roots.js +91 -0
  146. package/dist/core/workspace-sessions.d.ts +51 -0
  147. package/dist/core/workspace-sessions.js +243 -0
  148. package/dist/cowork/client.d.ts +29 -0
  149. package/dist/cowork/client.js +49 -0
  150. package/dist/cowork/engine-bridge.d.ts +22 -0
  151. package/dist/cowork/engine-bridge.js +98 -0
  152. package/dist/cowork/index.d.ts +11 -0
  153. package/dist/cowork/index.js +16 -0
  154. package/dist/cowork/ipc.d.ts +97 -0
  155. package/dist/cowork/ipc.js +96 -0
  156. package/dist/cowork/manager.d.ts +66 -0
  157. package/dist/cowork/manager.js +325 -0
  158. package/dist/cowork/store.d.ts +23 -0
  159. package/dist/cowork/store.js +182 -0
  160. package/dist/cowork/sync.d.ts +20 -0
  161. package/dist/cowork/sync.js +164 -0
  162. package/dist/cowork/types.d.ts +78 -0
  163. package/dist/cowork/types.js +21 -0
  164. package/dist/extensions/ext-tools.d.ts +3 -0
  165. package/dist/extensions/ext-tools.js +73 -0
  166. package/dist/extensions/loader.d.ts +91 -0
  167. package/dist/extensions/loader.js +474 -0
  168. package/dist/extensions/settings.d.ts +20 -0
  169. package/dist/extensions/settings.js +86 -0
  170. package/dist/extensions/types.d.ts +219 -0
  171. package/dist/extensions/types.js +1 -0
  172. package/dist/horizon/async-queue.d.ts +9 -0
  173. package/dist/horizon/async-queue.js +46 -0
  174. package/dist/horizon/engine.d.ts +85 -0
  175. package/dist/horizon/engine.js +603 -0
  176. package/dist/horizon/index.d.ts +4 -0
  177. package/dist/horizon/index.js +2 -0
  178. package/dist/horizon/main-team.d.ts +163 -0
  179. package/dist/horizon/main-team.js +774 -0
  180. package/dist/horizon/types.d.ts +233 -0
  181. package/dist/horizon/types.js +1 -0
  182. package/dist/index.d.ts +2 -0
  183. package/dist/index.js +233 -0
  184. package/dist/mcp/protocol.d.ts +81 -0
  185. package/dist/mcp/protocol.js +122 -0
  186. package/dist/package.json +1 -0
  187. package/dist/packages/packages.d.ts +12 -0
  188. package/dist/packages/packages.js +323 -0
  189. package/dist/prompts/templates.d.ts +13 -0
  190. package/dist/prompts/templates.js +139 -0
  191. package/dist/providers/anthropic-oauth.d.ts +13 -0
  192. package/dist/providers/anthropic-oauth.js +124 -0
  193. package/dist/providers/anthropic.d.ts +27 -0
  194. package/dist/providers/anthropic.js +526 -0
  195. package/dist/providers/catalog.d.ts +15 -0
  196. package/dist/providers/catalog.js +166 -0
  197. package/dist/providers/effort.d.ts +12 -0
  198. package/dist/providers/effort.js +102 -0
  199. package/dist/providers/model-catalog.d.ts +43 -0
  200. package/dist/providers/model-catalog.js +70 -0
  201. package/dist/providers/model-effort.d.ts +5 -0
  202. package/dist/providers/model-effort.js +43 -0
  203. package/dist/providers/model-registry.d.ts +39 -0
  204. package/dist/providers/model-registry.js +142 -0
  205. package/dist/providers/models-dev.d.ts +9 -0
  206. package/dist/providers/models-dev.js +97 -0
  207. package/dist/providers/oauth-callback-server.d.ts +28 -0
  208. package/dist/providers/oauth-callback-server.js +184 -0
  209. package/dist/providers/oauth-orchestrator.d.ts +58 -0
  210. package/dist/providers/oauth-orchestrator.js +159 -0
  211. package/dist/providers/oauth-store.d.ts +12 -0
  212. package/dist/providers/oauth-store.js +139 -0
  213. package/dist/providers/open-browser.d.ts +20 -0
  214. package/dist/providers/open-browser.js +92 -0
  215. package/dist/providers/openai-compat.d.ts +14 -0
  216. package/dist/providers/openai-compat.js +523 -0
  217. package/dist/providers/openai-oauth.d.ts +15 -0
  218. package/dist/providers/openai-oauth.js +382 -0
  219. package/dist/providers/openrouter-auth.d.ts +73 -0
  220. package/dist/providers/openrouter-auth.js +321 -0
  221. package/dist/providers/openrouter-manual-fallback.d.ts +25 -0
  222. package/dist/providers/openrouter-manual-fallback.js +161 -0
  223. package/dist/providers/openrouter.d.ts +15 -0
  224. package/dist/providers/openrouter.js +107 -0
  225. package/dist/providers/plan-limits.d.ts +6 -0
  226. package/dist/providers/plan-limits.js +175 -0
  227. package/dist/providers/provider-reset.d.ts +4 -0
  228. package/dist/providers/provider-reset.js +83 -0
  229. package/dist/providers/quota-limits.d.ts +4 -0
  230. package/dist/providers/quota-limits.js +33 -0
  231. package/dist/providers/registry.d.ts +33 -0
  232. package/dist/providers/registry.js +213 -0
  233. package/dist/providers/retry.d.ts +37 -0
  234. package/dist/providers/retry.js +347 -0
  235. package/dist/rpc/server.d.ts +23 -0
  236. package/dist/rpc/server.js +222 -0
  237. package/dist/sdk.d.ts +67 -0
  238. package/dist/sdk.js +64 -0
  239. package/dist/skills/skills.d.ts +17 -0
  240. package/dist/skills/skills.js +155 -0
  241. package/dist/text-sanitize.js +28 -0
  242. package/dist/tools/ask-user.d.ts +2 -0
  243. package/dist/tools/ask-user.js +60 -0
  244. package/dist/tools/builtin.d.ts +48 -0
  245. package/dist/tools/builtin.js +2494 -0
  246. package/dist/tools/capabilities.d.ts +5 -0
  247. package/dist/tools/capabilities.js +55 -0
  248. package/dist/tools/computer-capture.d.ts +2 -0
  249. package/dist/tools/computer-capture.js +91 -0
  250. package/dist/tools/computer-posix.d.ts +5 -0
  251. package/dist/tools/computer-posix.js +136 -0
  252. package/dist/tools/computer-use.d.ts +14 -0
  253. package/dist/tools/computer-use.js +108 -0
  254. package/dist/tools/discovery.d.ts +12 -0
  255. package/dist/tools/discovery.js +205 -0
  256. package/dist/tools/execute.d.ts +2 -0
  257. package/dist/tools/execute.js +73 -0
  258. package/dist/tools/normalize-input.d.ts +17 -0
  259. package/dist/tools/normalize-input.js +174 -0
  260. package/dist/tools/output-artifact.d.ts +3 -0
  261. package/dist/tools/output-artifact.js +80 -0
  262. package/dist/tools/pagination.d.ts +15 -0
  263. package/dist/tools/pagination.js +93 -0
  264. package/dist/tools/plan-status.d.ts +2 -0
  265. package/dist/tools/plan-status.js +109 -0
  266. package/dist/tools/program-input.d.ts +3 -0
  267. package/dist/tools/program-input.js +19 -0
  268. package/dist/tools/program.d.ts +3 -0
  269. package/dist/tools/program.js +567 -0
  270. package/dist/tools/protocol-catalog.d.ts +12 -0
  271. package/dist/tools/protocol-catalog.js +46 -0
  272. package/dist/tools/ramification-tools.d.ts +12 -0
  273. package/dist/tools/ramification-tools.js +528 -0
  274. package/dist/tools/regex-compat.d.ts +9 -0
  275. package/dist/tools/regex-compat.js +96 -0
  276. package/dist/tools/registry.d.ts +51 -0
  277. package/dist/tools/registry.js +185 -0
  278. package/dist/tools/resolve.d.ts +2 -0
  279. package/dist/tools/resolve.js +107 -0
  280. package/dist/tools/shell-env.d.ts +2 -0
  281. package/dist/tools/shell-env.js +7 -0
  282. package/dist/tools/team-input.d.ts +3 -0
  283. package/dist/tools/team-input.js +129 -0
  284. package/dist/tools/team-tools.d.ts +7 -0
  285. package/dist/tools/team-tools.js +348 -0
  286. package/dist/tools/workspace-sessions.d.ts +2 -0
  287. package/dist/tools/workspace-sessions.js +35 -0
  288. package/dist/tui/account-entry.d.ts +1 -0
  289. package/dist/tui/account-entry.js +93 -0
  290. package/dist/tui/attachment-highlight.d.ts +1 -0
  291. package/dist/tui/attachment-highlight.js +24 -0
  292. package/dist/tui/attachment-refs.d.ts +2 -0
  293. package/dist/tui/attachment-refs.js +3 -0
  294. package/dist/tui/bottom-input.d.ts +219 -0
  295. package/dist/tui/bottom-input.js +1958 -0
  296. package/dist/tui/branding.d.ts +107 -0
  297. package/dist/tui/branding.js +750 -0
  298. package/dist/tui/clipboard-image.d.ts +10 -0
  299. package/dist/tui/clipboard-image.js +163 -0
  300. package/dist/tui/clipboard-text.d.ts +9 -0
  301. package/dist/tui/clipboard-text.js +51 -0
  302. package/dist/tui/clipboard-windows.d.ts +8 -0
  303. package/dist/tui/clipboard-windows.js +84 -0
  304. package/dist/tui/collapsible-command.d.ts +15 -0
  305. package/dist/tui/collapsible-command.js +48 -0
  306. package/dist/tui/command-feedback.d.ts +10 -0
  307. package/dist/tui/command-feedback.js +46 -0
  308. package/dist/tui/command-output-registry.d.ts +28 -0
  309. package/dist/tui/command-output-registry.js +103 -0
  310. package/dist/tui/command-transcript.d.ts +12 -0
  311. package/dist/tui/command-transcript.js +90 -0
  312. package/dist/tui/commands.d.ts +20 -0
  313. package/dist/tui/commands.js +131 -0
  314. package/dist/tui/context-bar.d.ts +4 -0
  315. package/dist/tui/context-bar.js +74 -0
  316. package/dist/tui/context-meter.d.ts +25 -0
  317. package/dist/tui/context-meter.js +81 -0
  318. package/dist/tui/cost-binding.d.ts +8 -0
  319. package/dist/tui/cost-binding.js +43 -0
  320. package/dist/tui/cost-tracker.d.ts +49 -0
  321. package/dist/tui/cost-tracker.js +122 -0
  322. package/dist/tui/diff.d.ts +9 -0
  323. package/dist/tui/diff.js +119 -0
  324. package/dist/tui/i18n-catalog.d.ts +5 -0
  325. package/dist/tui/i18n-catalog.js +184 -0
  326. package/dist/tui/i18n.d.ts +6 -0
  327. package/dist/tui/i18n.js +10 -0
  328. package/dist/tui/image-attachments.d.ts +46 -0
  329. package/dist/tui/image-attachments.js +167 -0
  330. package/dist/tui/images.d.ts +12 -0
  331. package/dist/tui/images.js +87 -0
  332. package/dist/tui/intro.d.ts +13 -0
  333. package/dist/tui/intro.js +109 -0
  334. package/dist/tui/language-preference.d.ts +6 -0
  335. package/dist/tui/language-preference.js +24 -0
  336. package/dist/tui/live-output.d.ts +2 -0
  337. package/dist/tui/live-output.js +18 -0
  338. package/dist/tui/locales.d.ts +19 -0
  339. package/dist/tui/locales.js +22 -0
  340. package/dist/tui/login-menu.d.ts +17 -0
  341. package/dist/tui/login-menu.js +58 -0
  342. package/dist/tui/logo.d.ts +8 -0
  343. package/dist/tui/logo.js +305 -0
  344. package/dist/tui/markdown-inline.d.ts +3 -0
  345. package/dist/tui/markdown-inline.js +99 -0
  346. package/dist/tui/markdown-table.d.ts +4 -0
  347. package/dist/tui/markdown-table.js +63 -0
  348. package/dist/tui/markdown.d.ts +11 -0
  349. package/dist/tui/markdown.js +257 -0
  350. package/dist/tui/mcp-transcript.d.ts +7 -0
  351. package/dist/tui/mcp-transcript.js +40 -0
  352. package/dist/tui/model-details.d.ts +2 -0
  353. package/dist/tui/model-details.js +21 -0
  354. package/dist/tui/model-selection.d.ts +7 -0
  355. package/dist/tui/model-selection.js +41 -0
  356. package/dist/tui/native-scrollback.d.ts +27 -0
  357. package/dist/tui/native-scrollback.js +134 -0
  358. package/dist/tui/onboarding.d.ts +72 -0
  359. package/dist/tui/onboarding.js +207 -0
  360. package/dist/tui/paste-collapse.d.ts +39 -0
  361. package/dist/tui/paste-collapse.js +117 -0
  362. package/dist/tui/perm-mode.d.ts +8 -0
  363. package/dist/tui/perm-mode.js +41 -0
  364. package/dist/tui/prompt-queue-store.d.ts +26 -0
  365. package/dist/tui/prompt-queue-store.js +87 -0
  366. package/dist/tui/prompt-queue.d.ts +47 -0
  367. package/dist/tui/prompt-queue.js +187 -0
  368. package/dist/tui/provider-menu.d.ts +33 -0
  369. package/dist/tui/provider-menu.js +175 -0
  370. package/dist/tui/question.d.ts +24 -0
  371. package/dist/tui/question.js +32 -0
  372. package/dist/tui/queue-screen.d.ts +25 -0
  373. package/dist/tui/queue-screen.js +152 -0
  374. package/dist/tui/queue-team.d.ts +3 -0
  375. package/dist/tui/queue-team.js +28 -0
  376. package/dist/tui/queued-questions.d.ts +20 -0
  377. package/dist/tui/queued-questions.js +163 -0
  378. package/dist/tui/quick-help.d.ts +1 -0
  379. package/dist/tui/quick-help.js +53 -0
  380. package/dist/tui/repl.d.ts +78 -0
  381. package/dist/tui/repl.js +4130 -0
  382. package/dist/tui/restore-queued-input.d.ts +3 -0
  383. package/dist/tui/restore-queued-input.js +20 -0
  384. package/dist/tui/review-detail.d.ts +6 -0
  385. package/dist/tui/review-detail.js +56 -0
  386. package/dist/tui/review-files.d.ts +3 -0
  387. package/dist/tui/review-files.js +22 -0
  388. package/dist/tui/review-render.d.ts +22 -0
  389. package/dist/tui/review-render.js +98 -0
  390. package/dist/tui/review-screen.d.ts +38 -0
  391. package/dist/tui/review-screen.js +358 -0
  392. package/dist/tui/review-store.d.ts +60 -0
  393. package/dist/tui/review-store.js +270 -0
  394. package/dist/tui/review-team.d.ts +2 -0
  395. package/dist/tui/review-team.js +27 -0
  396. package/dist/tui/selector-mouse.d.ts +26 -0
  397. package/dist/tui/selector-mouse.js +65 -0
  398. package/dist/tui/selector.d.ts +51 -0
  399. package/dist/tui/selector.js +299 -0
  400. package/dist/tui/session-replay.d.ts +10 -0
  401. package/dist/tui/session-replay.js +78 -0
  402. package/dist/tui/session-usage.d.ts +22 -0
  403. package/dist/tui/session-usage.js +55 -0
  404. package/dist/tui/skill-transcript.d.ts +5 -0
  405. package/dist/tui/skill-transcript.js +31 -0
  406. package/dist/tui/startup.d.ts +5 -0
  407. package/dist/tui/startup.js +61 -0
  408. package/dist/tui/status-line-loader.d.ts +8 -0
  409. package/dist/tui/status-line-loader.js +33 -0
  410. package/dist/tui/status-line.d.ts +38 -0
  411. package/dist/tui/status-line.js +213 -0
  412. package/dist/tui/system-language.d.ts +8 -0
  413. package/dist/tui/system-language.js +41 -0
  414. package/dist/tui/task-labels.d.ts +2 -0
  415. package/dist/tui/task-labels.js +7 -0
  416. package/dist/tui/term-bg.d.ts +4 -0
  417. package/dist/tui/term-bg.js +67 -0
  418. package/dist/tui/terminal-screen.d.ts +12 -0
  419. package/dist/tui/terminal-screen.js +1 -0
  420. package/dist/tui/terminal-text.d.ts +7 -0
  421. package/dist/tui/terminal-text.js +123 -0
  422. package/dist/tui/theme.d.ts +25 -0
  423. package/dist/tui/theme.js +383 -0
  424. package/dist/tui/tool-style.d.ts +7 -0
  425. package/dist/tui/tool-style.js +119 -0
  426. package/dist/tui/update-prompt.d.ts +14 -0
  427. package/dist/tui/update-prompt.js +104 -0
  428. package/dist/tui/usage-footer.d.ts +9 -0
  429. package/dist/tui/usage-footer.js +123 -0
  430. package/dist/tui/vascend-login.d.ts +68 -0
  431. package/dist/tui/vascend-login.js +355 -0
  432. package/dist/tui/web-login.d.ts +28 -0
  433. package/dist/tui/web-login.js +115 -0
  434. package/dist/vascend/activity-status.d.ts +12 -0
  435. package/dist/vascend/activity-status.js +68 -0
  436. package/dist/vascend/architecture-map.d.ts +73 -0
  437. package/dist/vascend/architecture-map.js +424 -0
  438. package/dist/vascend/attention-layout.d.ts +19 -0
  439. package/dist/vascend/attention-layout.js +141 -0
  440. package/dist/vascend/budget-allocator.d.ts +31 -0
  441. package/dist/vascend/budget-allocator.js +79 -0
  442. package/dist/vascend/candidate-collector.d.ts +20 -0
  443. package/dist/vascend/candidate-collector.js +220 -0
  444. package/dist/vascend/code-events.d.ts +24 -0
  445. package/dist/vascend/code-events.js +304 -0
  446. package/dist/vascend/code-graph-memory.d.ts +38 -0
  447. package/dist/vascend/code-graph-memory.js +334 -0
  448. package/dist/vascend/code-graph-runtime.d.ts +19 -0
  449. package/dist/vascend/code-graph-runtime.js +190 -0
  450. package/dist/vascend/code-graph-types.d.ts +58 -0
  451. package/dist/vascend/code-graph-types.js +50 -0
  452. package/dist/vascend/code-parser.d.ts +4 -0
  453. package/dist/vascend/code-parser.js +257 -0
  454. package/dist/vascend/competitor-knowledge.d.ts +53 -0
  455. package/dist/vascend/competitor-knowledge.js +262 -0
  456. package/dist/vascend/completeness.d.ts +16 -0
  457. package/dist/vascend/completeness.js +90 -0
  458. package/dist/vascend/context-autotuner.d.ts +45 -0
  459. package/dist/vascend/context-autotuner.js +231 -0
  460. package/dist/vascend/context-candidate.d.ts +45 -0
  461. package/dist/vascend/context-candidate.js +150 -0
  462. package/dist/vascend/context-capsule.d.ts +51 -0
  463. package/dist/vascend/context-capsule.js +133 -0
  464. package/dist/vascend/context-fabric.d.ts +108 -0
  465. package/dist/vascend/context-fabric.js +480 -0
  466. package/dist/vascend/context-feedback-loop.d.ts +56 -0
  467. package/dist/vascend/context-feedback-loop.js +204 -0
  468. package/dist/vascend/context-learning-identity.d.ts +4 -0
  469. package/dist/vascend/context-learning-identity.js +8 -0
  470. package/dist/vascend/context-links.d.ts +14 -0
  471. package/dist/vascend/context-links.js +83 -0
  472. package/dist/vascend/context-lint.d.ts +22 -0
  473. package/dist/vascend/context-lint.js +91 -0
  474. package/dist/vascend/context-market-selection.d.ts +17 -0
  475. package/dist/vascend/context-market-selection.js +50 -0
  476. package/dist/vascend/context-metrics.d.ts +25 -0
  477. package/dist/vascend/context-metrics.js +57 -0
  478. package/dist/vascend/context-pack-render.d.ts +16 -0
  479. package/dist/vascend/context-pack-render.js +52 -0
  480. package/dist/vascend/context-pareto.d.ts +33 -0
  481. package/dist/vascend/context-pareto.js +165 -0
  482. package/dist/vascend/context-pattern-explorer.d.ts +41 -0
  483. package/dist/vascend/context-pattern-explorer.js +276 -0
  484. package/dist/vascend/context-regret.d.ts +32 -0
  485. package/dist/vascend/context-regret.js +93 -0
  486. package/dist/vascend/context-retriever.d.ts +22 -0
  487. package/dist/vascend/context-retriever.js +243 -0
  488. package/dist/vascend/context-scope-index.d.ts +5 -0
  489. package/dist/vascend/context-scope-index.js +96 -0
  490. package/dist/vascend/context-scorer.d.ts +35 -0
  491. package/dist/vascend/context-scorer.js +162 -0
  492. package/dist/vascend/context-search-index.d.ts +12 -0
  493. package/dist/vascend/context-search-index.js +25 -0
  494. package/dist/vascend/context-snapshot.d.ts +22 -0
  495. package/dist/vascend/context-snapshot.js +135 -0
  496. package/dist/vascend/context-window-compiler.d.ts +42 -0
  497. package/dist/vascend/context-window-compiler.js +237 -0
  498. package/dist/vascend/contract-memory.d.ts +61 -0
  499. package/dist/vascend/contract-memory.js +435 -0
  500. package/dist/vascend/council.d.ts +58 -0
  501. package/dist/vascend/council.js +337 -0
  502. package/dist/vascend/decision-promotion.d.ts +48 -0
  503. package/dist/vascend/decision-promotion.js +410 -0
  504. package/dist/vascend/delta-context.d.ts +12 -0
  505. package/dist/vascend/delta-context.js +36 -0
  506. package/dist/vascend/dev-context-compiler.d.ts +27 -0
  507. package/dist/vascend/dev-context-compiler.js +133 -0
  508. package/dist/vascend/dev-context-layout.d.ts +30 -0
  509. package/dist/vascend/dev-context-layout.js +262 -0
  510. package/dist/vascend/evidence-slots.d.ts +14 -0
  511. package/dist/vascend/evidence-slots.js +55 -0
  512. package/dist/vascend/expansion-levels.d.ts +15 -0
  513. package/dist/vascend/expansion-levels.js +178 -0
  514. package/dist/vascend/file-lock.d.ts +7 -0
  515. package/dist/vascend/file-lock.js +95 -0
  516. package/dist/vascend/granular-kingdom.d.ts +25 -0
  517. package/dist/vascend/granular-kingdom.js +259 -0
  518. package/dist/vascend/granular-launcher.d.ts +48 -0
  519. package/dist/vascend/granular-launcher.js +202 -0
  520. package/dist/vascend/kingdom-bus.d.ts +25 -0
  521. package/dist/vascend/kingdom-bus.js +54 -0
  522. package/dist/vascend/kingdom-channel.d.ts +22 -0
  523. package/dist/vascend/kingdom-channel.js +180 -0
  524. package/dist/vascend/kingdom-kpi.d.ts +48 -0
  525. package/dist/vascend/kingdom-kpi.js +172 -0
  526. package/dist/vascend/kingdom-live.d.ts +28 -0
  527. package/dist/vascend/kingdom-live.js +61 -0
  528. package/dist/vascend/kingdom-run.d.ts +18 -0
  529. package/dist/vascend/kingdom-run.js +506 -0
  530. package/dist/vascend/kingdom-scribe.d.ts +20 -0
  531. package/dist/vascend/kingdom-scribe.js +52 -0
  532. package/dist/vascend/kingdom.d.ts +79 -0
  533. package/dist/vascend/kingdom.js +870 -0
  534. package/dist/vascend/lexical-index.d.ts +10 -0
  535. package/dist/vascend/lexical-index.js +99 -0
  536. package/dist/vascend/lexical-search.d.ts +29 -0
  537. package/dist/vascend/lexical-search.js +262 -0
  538. package/dist/vascend/long-term-context.d.ts +51 -0
  539. package/dist/vascend/long-term-context.js +194 -0
  540. package/dist/vascend/mandate.d.ts +33 -0
  541. package/dist/vascend/mandate.js +107 -0
  542. package/dist/vascend/memory-packet.d.ts +13 -0
  543. package/dist/vascend/memory-packet.js +155 -0
  544. package/dist/vascend/memory-status.d.ts +22 -0
  545. package/dist/vascend/memory-status.js +73 -0
  546. package/dist/vascend/memory-store.d.ts +62 -0
  547. package/dist/vascend/memory-store.js +273 -0
  548. package/dist/vascend/objective-revision.d.ts +69 -0
  549. package/dist/vascend/objective-revision.js +319 -0
  550. package/dist/vascend/patch-plan-store.d.ts +33 -0
  551. package/dist/vascend/patch-plan-store.js +240 -0
  552. package/dist/vascend/patch-plan.d.ts +97 -0
  553. package/dist/vascend/patch-plan.js +345 -0
  554. package/dist/vascend/plan-progress.d.ts +11 -0
  555. package/dist/vascend/plan-progress.js +68 -0
  556. package/dist/vascend/plan-store.d.ts +64 -0
  557. package/dist/vascend/plan-store.js +460 -0
  558. package/dist/vascend/proof-chains.d.ts +19 -0
  559. package/dist/vascend/proof-chains.js +120 -0
  560. package/dist/vascend/ramification-store.d.ts +25 -0
  561. package/dist/vascend/ramification-store.js +94 -0
  562. package/dist/vascend/ramification.d.ts +193 -0
  563. package/dist/vascend/ramification.js +583 -0
  564. package/dist/vascend/recall-planner.d.ts +17 -0
  565. package/dist/vascend/recall-planner.js +109 -0
  566. package/dist/vascend/requirements.d.ts +31 -0
  567. package/dist/vascend/requirements.js +151 -0
  568. package/dist/vascend/reviewer.d.ts +9 -0
  569. package/dist/vascend/reviewer.js +93 -0
  570. package/dist/vascend/room-graph.d.ts +29 -0
  571. package/dist/vascend/room-graph.js +242 -0
  572. package/dist/vascend/room-memory.d.ts +57 -0
  573. package/dist/vascend/room-memory.js +631 -0
  574. package/dist/vascend/runtime.d.ts +84 -0
  575. package/dist/vascend/runtime.js +286 -0
  576. package/dist/vascend/self-improve.d.ts +104 -0
  577. package/dist/vascend/self-improve.js +396 -0
  578. package/dist/vascend/session-search.d.ts +52 -0
  579. package/dist/vascend/session-search.js +349 -0
  580. package/dist/vascend/supersession-graph.d.ts +14 -0
  581. package/dist/vascend/supersession-graph.js +70 -0
  582. package/dist/vascend/team-areas.d.ts +9 -0
  583. package/dist/vascend/team-areas.js +28 -0
  584. package/dist/vascend/team-handoff.d.ts +24 -0
  585. package/dist/vascend/team-handoff.js +37 -0
  586. package/dist/vascend/team-scheduler.d.ts +13 -0
  587. package/dist/vascend/team-scheduler.js +74 -0
  588. package/dist/vascend/team-session.d.ts +42 -0
  589. package/dist/vascend/team-session.js +142 -0
  590. package/dist/vascend/test-failure-memory.d.ts +76 -0
  591. package/dist/vascend/test-failure-memory.js +354 -0
  592. package/dist/vascend/token-market.d.ts +42 -0
  593. package/dist/vascend/token-market.js +144 -0
  594. package/dist/vascend/tuner-baseline-store.d.ts +52 -0
  595. package/dist/vascend/tuner-baseline-store.js +446 -0
  596. package/dist/vascend/user-profile.d.ts +44 -0
  597. package/dist/vascend/user-profile.js +131 -0
  598. package/dist/vascend/vision.d.ts +94 -0
  599. package/dist/vascend/vision.js +375 -0
  600. package/dist/vascend/work-agent.d.ts +12 -0
  601. package/dist/vascend/work-agent.js +83 -0
  602. package/dist/vascend/work-queue.d.ts +62 -0
  603. package/dist/vascend/work-queue.js +255 -0
  604. package/dist/vascend/work-runner.d.ts +64 -0
  605. package/dist/vascend/work-runner.js +137 -0
  606. package/dist/vascend/worktree.d.ts +38 -0
  607. package/dist/vascend/worktree.js +206 -0
  608. package/dist/vendor/computer-use/README.md +7 -0
  609. package/dist/vendor/computer-use/computer-desktop.js +211 -0
  610. package/dist/vendor/computer-use/computer-observe.js +315 -0
  611. package/dist/vendor/computer-use/computer-scripts.js +1230 -0
  612. package/dist/vendor/computer-use/computer-tools.js +2136 -0
  613. package/dist/vendor/computer-use/package.json +1 -0
  614. package/dist/vendor/computer-use/rac/atomic-input.js +91 -0
  615. package/dist/vendor/computer-use/rac/grammatica.js +101 -0
  616. package/dist/vendor/computer-use/rac/parser.js +185 -0
  617. package/dist/vendor/computer-use/rac/protocol.js +136 -0
  618. package/install.ps1 +30 -0
  619. package/install.sh +50 -0
  620. package/package.json +56 -0
@@ -0,0 +1,1958 @@
1
+ import { tr } from "./i18n.js";
2
+ import { stdin, stdout } from "node:process";
3
+ import { pulseGlyph, pulseToolName } from "./tool-style.js";
4
+ import { fitLine, plainText, terminalWidth, wrapText } from "./terminal-text.js";
5
+ import { PromptQueue } from "./prompt-queue.js";
6
+ import { QueueScreen, queuePreview } from "./queue-screen.js";
7
+ import { createAttachmentHighlighter } from "./attachment-highlight.js";
8
+ import { NativeScrollback } from "./native-scrollback.js";
9
+ import { QueuedQuestions } from "./queued-questions.js";
10
+ const SYNC_ON = "\x1b[?2026h";
11
+ const SYNC_OFF = "\x1b[?2026l";
12
+ const ALT_ON = "\x1b[?1049h";
13
+ const ALT_OFF = "\x1b[?1049l";
14
+ // Lascia click e trascinamento al terminale per la selezione nativa.
15
+ // Alternate scroll converte la rotella in frecce, gestite da onArrow.
16
+ // Disabilita anche eventuali modalita' ereditate da altre applicazioni.
17
+ export const NATIVE_SELECTION_MODE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1007h";
18
+ export const NATIVE_SCROLL_MODE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1007l";
19
+ // Bracketed paste (DEC 2004): il terminale racchiude il testo incollato tra
20
+ // ESC[200~ ed ESC[201~. Cosi' distinguiamo un INCOLLO da una digitazione: un
21
+ // path/data-URI incollato puo' diventare un allegato "img #N" invece di finire
22
+ // crudo nel buffer (parita' Claude Code).
23
+ const PASTE_ON = "\x1b[?2004h";
24
+ const PASTE_OFF = "\x1b[?2004l";
25
+ const SPINNER = ["✶", "✸", "✺", "✹", "✷", "✦"];
26
+ const MAX_LOG = 5000;
27
+ const SCROLL_FRAME_MS = 16;
28
+ const RESIZE_SETTLE_MS = 80;
29
+ const WHEEL_ARROW_LINES = 2;
30
+ const WHEEL_MOUSE_LINES = 4;
31
+ // Chiusura riga: reset SGR (colori/bold/underline) + chiusura hyperlink OSC 8.
32
+ // Va appesa a OGNI riga renderizzata: se una riga apre un link (fileLink) e
33
+ // viene troncata prima del terminatore, senza questo il sottolineato del link
34
+ // "cola" su tutte le righe successive (bug delle linee orizzontali).
35
+ const LINE_END = "\x1b[0m\x1b]8;;\x1b\\";
36
+ // Helper puro: mantiene il confronto dei frame separato dall'I/O del terminale
37
+ // e rende verificabile che uno spinner non ridisegni l'intera chat.
38
+ export function changedFrameRows(previous, next, force = false) {
39
+ const changed = [];
40
+ for (let i = 0; i < next.length; i += 1) {
41
+ if (force || previous[i] !== next[i])
42
+ changed.push(i);
43
+ }
44
+ return changed;
45
+ }
46
+ export class BottomInput {
47
+ h;
48
+ buffer = "";
49
+ cursor = 0;
50
+ history = [];
51
+ histIdx = -1;
52
+ draft = "";
53
+ active = false;
54
+ paused = false;
55
+ ctrlCConfirmation = null;
56
+ ctrlCTimer;
57
+ running = false;
58
+ questions = new QueuedQuestions(() => this.refresh());
59
+ runAbort;
60
+ queue;
61
+ queueEditor = null;
62
+ // Log della chat (righe gia' formattate) + scroll del viewport (0 = in fondo).
63
+ log = [];
64
+ logIds = [];
65
+ logSeq = 0;
66
+ nativeOutput = new NativeScrollback();
67
+ conversationRenderer;
68
+ conversationColumns = 0;
69
+ nativeScroll;
70
+ alternateScreen = false;
71
+ renderedEntries = [];
72
+ scrollOffset = 0;
73
+ logRevision = 0;
74
+ renderedLogRevision = -1;
75
+ renderedLogCache = [];
76
+ // Fold: intervalli del log collassati in una riga di sintesi (ondate di tool,
77
+ // stile Claude Code "ran N shell commands"). Espandibili con /expand.
78
+ folds = [];
79
+ foldSeq = 0;
80
+ // Viewer dei vassalli (Kingdoms Mode): stream live per-castello, navigabile con
81
+ // Ctrl+G (entra/cicla) ed Esc (esci). vassals=null quando non c'e' un regno.
82
+ vassals = null;
83
+ vassalIdx = 0;
84
+ vassalOpen = false;
85
+ // Board del regno (Kingdoms Mode): righe gia' stilate dal repl, mostrate nella
86
+ // regione viva (sopra l'input) e aggiornate in place — niente spam nella chat.
87
+ kingdomBoard = null;
88
+ overlay = null;
89
+ screen = null;
90
+ // Domanda a risposta libera (tool ask_user): quando attiva, l'editing va alla
91
+ // risposta e Invio/Esc risolvono la promise invece di inviare un prompt.
92
+ modal = null;
93
+ statusLabel = null;
94
+ // Nome GREZZO del tool attualmente in corso (es. "read_file"): pilota colore e
95
+ // glyph animati nella status-line (pulse abbinato alla categoria). null quando
96
+ // l'attivita' non e' un singolo tool (es. "elaboro", ondata mista).
97
+ statusTool = null;
98
+ // Stato VIVO della stanza Danilov corrente (metodo Vascend): invece di
99
+ // sputare ogni mark/avvio in chat (rumoroso), la status-line mostra la stanza
100
+ // attiva e il suo esito, animata. running = punto che pulsa; l'esito
101
+ // (illuminata/fallita) resta per qualche secondo poi si autospegne.
102
+ roomStatus = null;
103
+ roomTimer;
104
+ startedAt = 0;
105
+ tokensNew = 0;
106
+ tokensCache = 0;
107
+ tokensOut = 0;
108
+ frame = 0;
109
+ spinnerTimer;
110
+ lastLayout;
111
+ liveStartRow = 1;
112
+ paintedRows = [];
113
+ paintedColumns = 0;
114
+ paintedHeight = 0;
115
+ paintedCursor = "";
116
+ lastPaintAt = 0;
117
+ scrollRenderTimer;
118
+ quitResolve;
119
+ dataListener;
120
+ resizeListener;
121
+ resizeTimer;
122
+ // Disambiguazione frecce vs scroll (rotella/touchpad): le frecce ravvicinate
123
+ // sono uno scroll, una singola e' una pressione vera (history/cursore).
124
+ pendingArrows = [];
125
+ arrowTimer;
126
+ // Dopo aver riconosciuto una raffica come wheel, le eventuali sequenze
127
+ // residue appartengono allo stesso gesto e non devono raggiungere la history.
128
+ arrowScrollUntil = 0;
129
+ // Accumulatore di un incollo (bracketed paste) arrivato su piu' frame: si
130
+ // riempie finche' non si incontra il terminatore ESC[201~.
131
+ pasteBuffer = null;
132
+ capturingImage = false;
133
+ deferredImageSubmit = null;
134
+ inputGeneration = 0;
135
+ escapePending = "";
136
+ escapeTimer;
137
+ constructor(h) {
138
+ this.h = h;
139
+ this.nativeScroll = h.nativeScroll !== false;
140
+ this.queue = this.createQueue();
141
+ }
142
+ createQueue() {
143
+ return new PromptQueue(this.h.prepareQueuedInput, () => this.render(), this.h.queuePersistence?.(), this.h.restoreQueuedInput);
144
+ }
145
+ restoreQueue() {
146
+ this.inputGeneration++;
147
+ this.queue = this.createQueue();
148
+ this.queueEditor = null;
149
+ if (this.screen instanceof QueueScreen)
150
+ this.screen = null;
151
+ this.render();
152
+ }
153
+ get review() { return this.h.review; }
154
+ get userInbox() { return this.queue.inbox; }
155
+ acceptQueuedMessage(id) {
156
+ const text = this.queue.acknowledge(id);
157
+ if (text)
158
+ this.history.push(text);
159
+ return text;
160
+ }
161
+ pauseQueuedMessages() { if (this.queue.length) {
162
+ this.queue.paused = true;
163
+ this.render();
164
+ } }
165
+ showQueue() {
166
+ if (this.modal || this.overlay || this.queueEditor)
167
+ return;
168
+ this.inputGeneration++;
169
+ if (this.arrowTimer) {
170
+ clearTimeout(this.arrowTimer);
171
+ this.arrowTimer = undefined;
172
+ }
173
+ this.pendingArrows = [];
174
+ this.queue.held = true;
175
+ this.screen = new QueueScreen(this.queue, id => this.editQueuedMessage(id), this.h.queueStyle);
176
+ this.render();
177
+ }
178
+ closeScreen() {
179
+ if (this.screen instanceof QueueScreen)
180
+ this.queue.held = false;
181
+ this.screen = null;
182
+ this.startNextQueued();
183
+ this.render();
184
+ }
185
+ editQueuedMessage(id) {
186
+ const entry = this.queue.get(id);
187
+ if (!entry || !(this.screen instanceof QueueScreen))
188
+ return;
189
+ this.inputGeneration++;
190
+ this.queueEditor = { id, screen: this.screen, draft: { buffer: this.buffer, cursor: this.cursor, histIdx: this.histIdx } };
191
+ this.screen = null;
192
+ this.buffer = entry.text;
193
+ this.cursor = this.buffer.length;
194
+ this.histIdx = -1;
195
+ this.render();
196
+ }
197
+ finishQueueEdit(save, immediate = false) {
198
+ const editor = this.queueEditor;
199
+ if (!editor)
200
+ return;
201
+ if (save) {
202
+ try {
203
+ if (!this.queue.update(editor.id, this.buffer))
204
+ throw new Error("Il messaggio non è più in attesa.");
205
+ }
206
+ catch (error) {
207
+ this.reportFailure(error instanceof Error ? error.message : String(error));
208
+ return;
209
+ }
210
+ }
211
+ this.buffer = editor.draft.buffer;
212
+ this.cursor = editor.draft.cursor;
213
+ this.histIdx = editor.draft.histIdx;
214
+ this.queueEditor = null;
215
+ this.screen = editor.screen;
216
+ if (immediate && this.queue.sendNow(editor.id)) {
217
+ this.closeScreen();
218
+ return;
219
+ }
220
+ this.render();
221
+ }
222
+ showReview(section) {
223
+ if (!this.h.createReviewScreen || this.modal || this.overlay || this.queueEditor || this.queue.held)
224
+ return;
225
+ this.inputGeneration++;
226
+ if (this.arrowTimer) {
227
+ clearTimeout(this.arrowTimer);
228
+ this.arrowTimer = undefined;
229
+ }
230
+ this.pendingArrows = [];
231
+ this.screen = this.h.createReviewScreen(section);
232
+ this.render();
233
+ }
234
+ get screenVisible() { return this.screen !== null && !this.modal && !this.overlay; }
235
+ static supported() {
236
+ return Boolean(stdout.isTTY && stdin.isTTY && typeof stdin.setRawMode === "function");
237
+ }
238
+ start() {
239
+ if (!BottomInput.supported())
240
+ return Promise.resolve();
241
+ this.active = true;
242
+ this.alternateScreen = !this.nativeScroll;
243
+ stdout.write(this.nativeScroll ? "\x1b[2J\x1b[H" + NATIVE_SCROLL_MODE + PASTE_ON
244
+ : ALT_ON + "\x1b[2J\x1b[H" + NATIVE_SELECTION_MODE + PASTE_ON);
245
+ stdin.setRawMode(true);
246
+ stdin.resume();
247
+ // Parser RAW dello stdin: gestiamo noi mouse e tasti (readline spezzava le
248
+ // sequenze del mouse, facendone digitare i frammenti nell'input).
249
+ this.dataListener = (chunk) => this.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
250
+ stdin.on("data", this.dataListener);
251
+ this.resizeListener = () => this.queueResizeRender();
252
+ stdout.on("resize", this.resizeListener);
253
+ this.h.onReady?.();
254
+ this.render(true);
255
+ return new Promise((resolve) => { this.quitResolve = resolve; });
256
+ }
257
+ // Aggiunge contenuto alla CHAT (log) e mantiene il viewport in fondo.
258
+ write(text) {
259
+ this.conversationRenderer = undefined;
260
+ if (!this.active) {
261
+ stdout.write(text);
262
+ return;
263
+ }
264
+ const lines = text.replace(/\n+$/, "").split("\n");
265
+ for (const l of lines) {
266
+ this.log.push(l);
267
+ this.logIds.push(++this.logSeq);
268
+ }
269
+ this.touchLog();
270
+ // Se stai leggendo piu' in alto (scrollato), MANTIENI la posizione invece di
271
+ // strapparti in fondo; se eri in fondo, resti in fondo.
272
+ if (this.scrollOffset > 0)
273
+ this.scrollOffset += lines.length;
274
+ this.render();
275
+ this.trimLog();
276
+ }
277
+ // Forza un re-render del footer/status-line senza scrivere nel log.
278
+ // Usato per le viste LIVE (es. task in background che cambiano stato).
279
+ refresh() { if (this.active)
280
+ this.render(); }
281
+ // Rebuild a restored conversation from its source on terminal resize. A
282
+ // subsequent live write takes ownership of the transcript again.
283
+ replayConversation(renderer) {
284
+ const columns = this.cols();
285
+ this.write(renderer(columns));
286
+ this.conversationRenderer = renderer;
287
+ this.conversationColumns = columns;
288
+ }
289
+ resetConversationLog() {
290
+ this.log.length = 0;
291
+ this.logIds.length = 0;
292
+ this.logSeq = 0;
293
+ this.folds.length = 0;
294
+ this.renderedEntries = [];
295
+ this.renderedLogCache = [];
296
+ this.touchLog();
297
+ this.paintedRows = [];
298
+ this.paintedCursor = "";
299
+ return this.nativeOutput.reset();
300
+ }
301
+ // Forget the previous session's display and editor state before any replay.
302
+ clearConversation() {
303
+ this.questions.clear();
304
+ this.inputGeneration++;
305
+ this.buffer = "";
306
+ this.cursor = 0;
307
+ this.history.length = 0;
308
+ this.histIdx = -1;
309
+ this.draft = "";
310
+ this.conversationRenderer = undefined;
311
+ this.scrollOffset = 0;
312
+ this.lastLayout = undefined;
313
+ this.screen = null;
314
+ this.overlay = null;
315
+ this.vassals = null;
316
+ this.vassalIdx = 0;
317
+ this.vassalOpen = false;
318
+ this.kingdomBoard = null;
319
+ this.statusLabel = null;
320
+ this.statusTool = null;
321
+ this.roomStatus = null;
322
+ this.tokensNew = this.tokensCache = this.tokensOut = 0;
323
+ if (this.spinnerTimer) {
324
+ clearInterval(this.spinnerTimer);
325
+ this.spinnerTimer = undefined;
326
+ }
327
+ if (this.roomTimer) {
328
+ clearTimeout(this.roomTimer);
329
+ this.roomTimer = undefined;
330
+ }
331
+ if (this.arrowTimer) {
332
+ clearTimeout(this.arrowTimer);
333
+ this.arrowTimer = undefined;
334
+ }
335
+ if (this.scrollRenderTimer) {
336
+ clearTimeout(this.scrollRenderTimer);
337
+ this.scrollRenderTimer = undefined;
338
+ }
339
+ this.pendingArrows = [];
340
+ this.arrowScrollUntil = 0;
341
+ const reset = this.resetConversationLog();
342
+ if (this.active) {
343
+ if (this.nativeScroll && this.alternateScreen) {
344
+ stdout.write(ALT_OFF + NATIVE_SCROLL_MODE);
345
+ this.alternateScreen = false;
346
+ }
347
+ stdout.write(reset);
348
+ this.render(true);
349
+ }
350
+ }
351
+ // Sostituisce le ULTIME `count` righe del log con `lines`, poi re-renderizza
352
+ // in place. Pensato per le animazioni dello scrollback (es. intro): si scrive
353
+ // una volta il blocco con write(), poi si aggiorna lo stesso blocco frame per
354
+ // frame senza accodare nuove righe. Se non e' attivo, scrive su stdout grezzo.
355
+ replaceTail(lines, count) {
356
+ if (!this.active) {
357
+ stdout.write(lines.join("\n") + "\n");
358
+ return;
359
+ }
360
+ const n = Math.min(count, this.log.length);
361
+ const ids = this.logIds.splice(this.log.length - n, n);
362
+ if (n > 0)
363
+ this.log.splice(this.log.length - n, n);
364
+ for (const [i, line] of lines.entries()) {
365
+ this.log.push(line);
366
+ this.logIds.push(ids[i] ?? ++this.logSeq);
367
+ }
368
+ this.touchLog();
369
+ this.render();
370
+ this.trimLog();
371
+ }
372
+ // Riscrive in place una SINGOLA riga del log (per indice assoluto), poi
373
+ // re-renderizza. Pensato per l'animazione del bullet ● del tool in corso nello
374
+ // scrollback: si conosce l'indice della riga "● Nome(arg)" e si aggiorna solo
375
+ // quella frame per frame, senza accodare nulla. No-op se l'indice e' fuori range.
376
+ setLogLine(index, text) {
377
+ if (index < 0 || index >= this.log.length)
378
+ return;
379
+ if (this.log[index] === text)
380
+ return;
381
+ this.log[index] = text;
382
+ this.touchLog();
383
+ if (this.active)
384
+ this.render();
385
+ }
386
+ setTokens(newInput, cache, outTok) {
387
+ this.tokensNew = newInput;
388
+ this.tokensCache = cache;
389
+ this.tokensOut = outTok;
390
+ if (this.active && this.statusLabel !== null)
391
+ this.render();
392
+ }
393
+ setStatus(label) {
394
+ this.statusLabel = label;
395
+ if (label !== null) {
396
+ if (!this.spinnerTimer)
397
+ this.spinnerTimer = setInterval(() => { this.frame += 1; this.render(); }, 140);
398
+ }
399
+ else if (this.spinnerTimer) {
400
+ clearInterval(this.spinnerTimer);
401
+ this.spinnerTimer = undefined;
402
+ }
403
+ if (this.active)
404
+ this.render();
405
+ }
406
+ getLogLine(index) { return this.log[index]; }
407
+ // Stato leggibile dall'host per evitare una seconda telemetria statica mentre
408
+ // la riga animata del run sta gia' mostrando token, tempo e coda.
409
+ isBusy() {
410
+ return this.running || this.statusLabel !== null;
411
+ }
412
+ // Nome del tool in corso per l'animazione della status-line. null = attivita'
413
+ // generica (spinner neutro). Non forza un timer: lo gestisce gia' setStatus.
414
+ setStatusTool(name) {
415
+ this.statusTool = name;
416
+ if (this.active && this.statusLabel !== null)
417
+ this.render();
418
+ }
419
+ // Stato della stanza Danilov (Vascend) per la status-line: niente piu' righe
420
+ // in chat per avvio/illuminazione. `running` resta finche' la stanza lavora;
421
+ // `done` si spegne dopo 3s; `failed` resta fino al prossimo invio o avviso.
422
+ // null lo rimuove subito.
423
+ setRoomStatus(label, state = "running") {
424
+ if (this.roomTimer) {
425
+ clearTimeout(this.roomTimer);
426
+ this.roomTimer = undefined;
427
+ }
428
+ if (!label) {
429
+ this.roomStatus = null;
430
+ }
431
+ else if (state === "running" || state === "failed") {
432
+ this.roomStatus = { label, state };
433
+ }
434
+ else {
435
+ this.roomStatus = { label, state, until: Date.now() + 3000 };
436
+ this.roomTimer = setTimeout(() => {
437
+ // Spegni solo se non e' arrivata nel frattempo una nuova stanza running.
438
+ if (this.roomStatus?.state === "done") {
439
+ this.roomStatus = null;
440
+ if (this.active)
441
+ this.render();
442
+ }
443
+ this.roomTimer = undefined;
444
+ }, 3000);
445
+ }
446
+ // La status-line anima solo col timer dello spinner: se non c'e' un run in
447
+ // corso (statusLabel null) accendiamo un refresh leggero per il pulse della
448
+ // stanza running.
449
+ if (this.active)
450
+ this.render();
451
+ }
452
+ reportFailure(message) {
453
+ const detail = plainText(message).trim();
454
+ this.h.review?.record({ type: "error", message: detail });
455
+ this.setRoomStatus(detail, "failed");
456
+ }
457
+ setOverlay(lines) {
458
+ this.overlay = lines && lines.length
459
+ ? { lines, cursorRow: lines.length - 1, cursorCol: 1 }
460
+ : null;
461
+ if (!this.overlay)
462
+ this.startNextQueued();
463
+ if (this.active)
464
+ this.render();
465
+ }
466
+ // Lunghezza attuale del log (indice di inserimento): per delimitare un'ondata.
467
+ logLength() { return this.log.length; }
468
+ // Collassa [start, end) del log in una riga di sintesi. Ritorna l'id del fold.
469
+ fold(start, end, summary) {
470
+ const id = (this.foldSeq += 1);
471
+ this.folds.push({ id, start, end, summary, expanded: false, open: false });
472
+ this.touchLog();
473
+ if (this.active)
474
+ this.render();
475
+ return id;
476
+ }
477
+ // Fold APERTO: copre da `start` fino alla fine corrente del log e si estende da
478
+ // solo man mano che arrivano nuove righe (cosi' durante un'ondata di tool la
479
+ // chat mostra UNA riga di sintesi che si aggiorna, niente duplicati).
480
+ openFold(start, summary) {
481
+ const id = (this.foldSeq += 1);
482
+ this.folds.push({ id, start, end: start, summary, expanded: false, open: true });
483
+ this.touchLog();
484
+ if (this.active)
485
+ this.render();
486
+ return id;
487
+ }
488
+ updateFoldSummary(id, summary) {
489
+ const f = this.folds.find((x) => x.id === id);
490
+ if (!f)
491
+ return;
492
+ if (f.summary === summary)
493
+ return;
494
+ f.summary = summary;
495
+ this.touchLog();
496
+ if (this.active)
497
+ this.render();
498
+ }
499
+ // Aggiorna fine + sintesi di un fold (chiuso). Usato per collassare i tool
500
+ // GIA' finiti di un'ondata lasciando visibile quello in corso.
501
+ updateFold(id, end, summary) {
502
+ const f = this.folds.find((x) => x.id === id);
503
+ if (!f)
504
+ return;
505
+ f.end = end;
506
+ f.summary = summary;
507
+ f.open = false;
508
+ this.touchLog();
509
+ if (this.active)
510
+ this.render();
511
+ }
512
+ // Apre/chiude (toggle) un fold: per id, o il piu' recente. Ritorna true se
513
+ // c'era un fold su cui agire.
514
+ toggleFold(id) {
515
+ const target = id !== undefined ? this.folds.find((f) => f.id === id) : this.folds[this.folds.length - 1];
516
+ if (!target)
517
+ return false;
518
+ if (!target.expanded && this.reprintArchivedFold(target))
519
+ return true;
520
+ target.expanded = !target.expanded;
521
+ this.touchLog();
522
+ if (this.active)
523
+ this.render();
524
+ return true;
525
+ }
526
+ // Chiude un fold aperto: ne fissa la fine (cosi' il contenuto successivo, es.
527
+ // il testo dell'agente, non viene piu' nascosto).
528
+ closeFold(id, summary) {
529
+ const f = this.folds.find((x) => x.id === id);
530
+ if (!f)
531
+ return;
532
+ f.open = false;
533
+ f.end = this.log.length;
534
+ if (summary !== undefined)
535
+ f.summary = summary;
536
+ this.touchLog();
537
+ if (this.active)
538
+ this.render();
539
+ }
540
+ // Espande un fold (per id, o il piu' recente non espanso). Ritorna true se uno
541
+ // e' stato espanso.
542
+ expandFold(id) {
543
+ const target = id !== undefined
544
+ ? this.folds.find((f) => f.id === id && !f.expanded)
545
+ : [...this.folds].reverse().find((f) => !f.expanded);
546
+ if (!target)
547
+ return false;
548
+ if (this.reprintArchivedFold(target))
549
+ return true;
550
+ target.expanded = true;
551
+ this.touchLog();
552
+ if (this.active)
553
+ this.render();
554
+ return true;
555
+ }
556
+ // Rimuove DAVVERO un fold e le righe di log che copriva: il blocco sparisce
557
+ // dalla chat (niente segnaposto), come si aspetta l'utente quando chiude un
558
+ // output che non vuole piu' vedere. Gli start/end dei fold successivi vengono
559
+ // ricompattati. Ritorna true se il fold esisteva.
560
+ removeFold(id) {
561
+ const idx = this.folds.findIndex((f) => f.id === id);
562
+ if (idx < 0)
563
+ return false;
564
+ const f = this.folds[idx];
565
+ const start = f.start;
566
+ const end = f.open ? this.log.length : f.end;
567
+ const removed = Math.max(0, end - start);
568
+ this.folds.splice(idx, 1);
569
+ if (removed > 0) {
570
+ this.log.splice(start, removed);
571
+ this.logIds.splice(start, removed);
572
+ // Ricompatta gli intervalli dei fold rimasti che stavano dopo la regione.
573
+ for (const o of this.folds) {
574
+ if (o.start >= end)
575
+ o.start -= removed;
576
+ if (o.end >= end)
577
+ o.end -= removed;
578
+ else if (o.end > start)
579
+ o.end = start;
580
+ }
581
+ }
582
+ this.touchLog();
583
+ if (this.active)
584
+ this.render();
585
+ return true;
586
+ }
587
+ // Espande tutti i fold. Ritorna quanti ne ha espansi.
588
+ expandAllFolds() {
589
+ let n = 0;
590
+ for (const f of [...this.folds])
591
+ if (!f.expanded) {
592
+ if (!this.reprintArchivedFold(f))
593
+ f.expanded = true;
594
+ n += 1;
595
+ }
596
+ if (n)
597
+ this.touchLog();
598
+ if (n && this.active)
599
+ this.render();
600
+ return n;
601
+ }
602
+ reprintArchivedFold(fold) {
603
+ if (!this.nativeScroll || fold.open || !this.nativeOutput.isCommitted(this.logIds[fold.start] ?? Infinity))
604
+ return false;
605
+ const details = this.log.slice(fold.start, fold.end);
606
+ fold.expanded = true;
607
+ fold.start = this.log.length;
608
+ fold.end = fold.start + 1 + details.length;
609
+ this.write([fold.summary, ...details].join("\n"));
610
+ return true;
611
+ }
612
+ trimLog() {
613
+ // Native scrollback owns committed rows. The alternate viewport needs the
614
+ // complete session in memory so a resumed chat remains scrollable in full.
615
+ if (!this.nativeScroll)
616
+ return;
617
+ const count = this.log.length - MAX_LOG;
618
+ if (count <= 0 || (this.nativeScroll && (this.alternateScreen || this.paused)))
619
+ return;
620
+ this.log.splice(0, count);
621
+ this.logIds.splice(0, count);
622
+ for (let i = this.folds.length - 1; i >= 0; i--) {
623
+ const fold = this.folds[i];
624
+ fold.start = Math.max(0, fold.start - count);
625
+ fold.end = Math.max(0, fold.end - count);
626
+ if (!fold.open && fold.end <= fold.start)
627
+ this.folds.splice(i, 1);
628
+ }
629
+ this.touchLog();
630
+ }
631
+ // Log con i fold applicati: sostituisce gli intervalli collassati con la sintesi.
632
+ renderedLog() {
633
+ if (this.renderedLogRevision === this.logRevision)
634
+ return this.renderedLogCache;
635
+ const active = this.folds.filter((f) => !f.expanded).sort((a, b) => a.start - b.start);
636
+ if (!active.length) {
637
+ this.renderedEntries = this.log.map((text, index) => ({ text, id: this.logIds[index] }));
638
+ this.renderedLogRevision = this.logRevision;
639
+ this.renderedLogCache = this.log;
640
+ return this.renderedLogCache;
641
+ }
642
+ const out = [];
643
+ const entries = [];
644
+ let i = 0;
645
+ let fi = 0;
646
+ while (i < this.log.length) {
647
+ let f = active[fi];
648
+ while (f && f.start < i) {
649
+ fi += 1;
650
+ f = active[fi];
651
+ }
652
+ if (f && i === f.start) {
653
+ const end = f.open ? this.log.length : f.end; // fold aperto: fino alla fine
654
+ if (end > f.start) {
655
+ out.push(f.summary);
656
+ entries.push({ text: f.summary, id: this.logIds[i], through: this.logIds[Math.min(end, this.log.length) - 1] });
657
+ i = end;
658
+ fi += 1;
659
+ continue;
660
+ }
661
+ }
662
+ out.push(this.log[i] ?? "");
663
+ entries.push({ text: this.log[i] ?? "", id: this.logIds[i] });
664
+ i += 1;
665
+ }
666
+ this.renderedLogRevision = this.logRevision;
667
+ this.renderedLogCache = out;
668
+ this.renderedEntries = entries;
669
+ return this.renderedLogCache;
670
+ }
671
+ touchLog() {
672
+ this.logRevision += 1;
673
+ }
674
+ pause() {
675
+ if (this.paused)
676
+ return;
677
+ this.paused = true;
678
+ this.inputGeneration++;
679
+ this.clearPendingEscape();
680
+ if (this.nativeScroll) {
681
+ if (this.alternateScreen) {
682
+ stdout.write(ALT_OFF);
683
+ this.alternateScreen = false;
684
+ }
685
+ stdout.write(this.nativeOutput.release());
686
+ }
687
+ this.cancelCtrlC();
688
+ if (this.dataListener)
689
+ stdin.off("data", this.dataListener);
690
+ if (this.spinnerTimer) {
691
+ clearInterval(this.spinnerTimer);
692
+ this.spinnerTimer = undefined;
693
+ }
694
+ if (this.scrollRenderTimer) {
695
+ clearTimeout(this.scrollRenderTimer);
696
+ this.scrollRenderTimer = undefined;
697
+ }
698
+ this.paintedRows = [];
699
+ this.paintedCursor = "";
700
+ }
701
+ resume() {
702
+ if (!this.active)
703
+ return;
704
+ this.paused = false;
705
+ stdout.write(this.nativeScroll ? NATIVE_SCROLL_MODE : NATIVE_SELECTION_MODE);
706
+ if (this.dataListener)
707
+ stdin.on("data", this.dataListener);
708
+ if (typeof stdin.setRawMode === "function")
709
+ stdin.setRawMode(true);
710
+ stdin.resume();
711
+ if (this.statusLabel !== null && !this.spinnerTimer) {
712
+ this.spinnerTimer = setInterval(() => { this.frame += 1; this.render(); }, 140);
713
+ }
714
+ this.render(true);
715
+ }
716
+ // Dati dei vassalli per il viewer (Kingdoms Mode). null = nessun regno: chiude.
717
+ setKingdomVassals(data) {
718
+ if (!data || !data.length) {
719
+ this.vassals = null;
720
+ this.vassalOpen = false;
721
+ }
722
+ else {
723
+ this.vassals = data.map((d) => ({ slug: d.slug, lines: d.lines }));
724
+ if (this.vassalIdx >= this.vassals.length)
725
+ this.vassalIdx = 0;
726
+ }
727
+ if (this.active && (this.vassalOpen || !data))
728
+ this.render();
729
+ }
730
+ // Board live del regno: panoramica di tutti i vassalli, aggiornata in place
731
+ // nella regione viva. null = nessun regno (la rimuove). Le righe arrivano gia'
732
+ // stilate dal repl. Non si mostra mentre si e' dentro il viewer di un vassallo.
733
+ setKingdomBoard(lines) {
734
+ this.kingdomBoard = lines && lines.length ? lines.slice() : null;
735
+ if (this.active && !this.vassalOpen)
736
+ this.render();
737
+ }
738
+ // true se la board live del regno e' pinned ora: il chiamante puo' evitare di
739
+ // scrivere ANCHE il riquadro inline del piano, che duplicherebbe la vista.
740
+ hasKingdomBoard() {
741
+ return this.kingdomBoard !== null;
742
+ }
743
+ requestQuit() { this.quit(); }
744
+ cols() { return stdout.columns || 80; }
745
+ rows() { return stdout.rows || 24; }
746
+ queueResizeRender() {
747
+ this.nativeOutput.invalidate();
748
+ if (this.resizeTimer)
749
+ clearTimeout(this.resizeTimer);
750
+ this.resizeTimer = setTimeout(() => {
751
+ this.resizeTimer = undefined;
752
+ this.render(true);
753
+ }, RESIZE_SETTLE_MS);
754
+ }
755
+ // ── rendering full-frame ────────────────────────────────────────────────────
756
+ liveContent() {
757
+ if (this.overlay) {
758
+ return {
759
+ lines: this.overlay.lines, cursorRow: this.overlay.cursorRow, cursorCol: this.overlay.cursorCol,
760
+ inputStart: 0, inputRows: 0, textWidth: 0, prefixCols: 0, rowStarts: [], rowLengths: [],
761
+ };
762
+ }
763
+ // Viewer vassallo: occupa la regione viva con il tail dello stream del castello.
764
+ if (this.vassalOpen && this.vassals && this.vassals.length) {
765
+ const v = this.vassals[this.vassalIdx] ?? this.vassals[0];
766
+ const style = this.h.statusStyle ?? ((s) => s);
767
+ const maxLines = Math.max(3, this.rows() - 3);
768
+ const tail = v.lines.slice(-maxLines);
769
+ const header = ` ◆ collaboratore ${this.vassalIdx + 1}/${this.vassals.length} · ${v.slug} · Ctrl+G prossimo · Esc esci`;
770
+ const lines = [style(header), ...(tail.length ? tail : [style(" (in attesa di attività…)")])];
771
+ return { lines, cursorRow: 0, cursorCol: 1, inputStart: 0, inputRows: 0, textWidth: 0, prefixCols: 0, rowStarts: [], rowLengths: [] };
772
+ }
773
+ return this.layout();
774
+ }
775
+ render(force = false) {
776
+ if (!this.active || this.resizeTimer || (this.paused && !this.overlay))
777
+ return;
778
+ if (this.scrollRenderTimer) {
779
+ clearTimeout(this.scrollRenderTimer);
780
+ this.scrollRenderTimer = undefined;
781
+ }
782
+ const H = this.rows();
783
+ const W = this.cols();
784
+ const alternate = !this.nativeScroll || this.screenVisible || this.overlay !== null || this.vassalOpen || this.scrollOffset > 0;
785
+ if (alternate !== this.alternateScreen) {
786
+ stdout.write(alternate ? ALT_ON + "\x1b[2J\x1b[H" + NATIVE_SELECTION_MODE : ALT_OFF + NATIVE_SCROLL_MODE);
787
+ this.alternateScreen = alternate;
788
+ this.paintedRows = [];
789
+ this.paintedCursor = "";
790
+ force = true;
791
+ }
792
+ if (this.screenVisible) {
793
+ const content = this.screen.render(W, H);
794
+ const rows = Array.from({ length: H }, (_, i) => fitLine(content[i] ?? "", Math.max(1, W - 1)));
795
+ this.paint(rows, W, H, 1, 1, force, true);
796
+ return;
797
+ }
798
+ if (this.conversationRenderer && this.conversationColumns !== W && (!this.nativeScroll || !alternate)) {
799
+ const text = this.conversationRenderer(W);
800
+ const reset = this.resetConversationLog();
801
+ for (const line of text.replace(/\n+$/, "").split("\n")) {
802
+ this.log.push(line);
803
+ this.logIds.push(++this.logSeq);
804
+ }
805
+ this.conversationColumns = W;
806
+ if (this.nativeScroll)
807
+ stdout.write(reset);
808
+ force = true;
809
+ }
810
+ const lay = this.liveContent();
811
+ this.lastLayout = lay;
812
+ if (this.nativeScroll && !alternate) {
813
+ this.renderedLog();
814
+ const output = this.nativeOutput.render(this.renderedEntries, lay, W, H, false, this.h.startupLines?.() ?? this.h.headerLines?.() ?? []);
815
+ if (output)
816
+ stdout.write(output);
817
+ this.lastPaintAt = Date.now();
818
+ this.trimLog();
819
+ return;
820
+ }
821
+ const liveH = Math.min(lay.lines.length, H - 1);
822
+ const requestedHeader = this.h.headerLines?.() ?? [];
823
+ const headerH = Math.min(requestedHeader.length, Math.max(0, H - liveH - 1));
824
+ const header = requestedHeader.slice(0, headerH);
825
+ const chatRows = Math.max(1, H - liveH - headerH);
826
+ const rendered = this.renderedLog();
827
+ const total = rendered.length;
828
+ const maxOffset = Math.max(0, total - chatRows);
829
+ if (this.scrollOffset > maxOffset)
830
+ this.scrollOffset = maxOffset;
831
+ const startIdx = Math.max(0, total - chatRows - this.scrollOffset);
832
+ const visible = rendered.slice(startIdx, startIdx + chatRows);
833
+ const frameRows = Array.from({ length: H }, () => "");
834
+ for (let i = 0; i < headerH; i += 1)
835
+ frameRows[i] = clipVisible(header[i] ?? "", W);
836
+ for (let i = 0; i < chatRows; i += 1) {
837
+ let line = visible[i] ?? "";
838
+ // Indicatore "altro sopra/sotto" sulla prima/ultima riga del viewport.
839
+ if (i === 0 && this.scrollOffset < maxOffset)
840
+ line = scrollHint(line, "↑ altro sopra", W);
841
+ frameRows[headerH + i] = clipVisible(line, W);
842
+ }
843
+ this.liveStartRow = headerH + chatRows + 1;
844
+ for (let i = 0; i < liveH; i += 1) {
845
+ frameRows[headerH + chatRows + i] = clipVisible(lay.lines[i] ?? "", W);
846
+ }
847
+ const cursorRow = Math.max(1, Math.min(H, this.liveStartRow + Math.min(lay.cursorRow, Math.max(0, liveH - 1))));
848
+ const cursorCol = Math.max(1, Math.min(W, lay.cursorCol));
849
+ this.paint(frameRows, W, H, cursorRow, cursorCol, force, this.overlay !== null);
850
+ }
851
+ paint(frameRows, W, H, cursorRow, cursorCol, force, hideCursor = false) {
852
+ const dimensionsChanged = W !== this.paintedColumns || H !== this.paintedHeight;
853
+ const full = force || dimensionsChanged || this.paintedRows.length !== frameRows.length;
854
+ const changed = changedFrameRows(this.paintedRows, frameRows, full);
855
+ const cursor = `${cursorRow}:${cursorCol}:${hideCursor}`;
856
+ if (!changed.length && cursor === this.paintedCursor)
857
+ return;
858
+ let out = SYNC_ON;
859
+ for (const index of changed) {
860
+ out += `\x1b[${index + 1};1H\x1b[2K` + (frameRows[index] ?? "") + LINE_END;
861
+ }
862
+ out += `\x1b[${cursorRow};${cursorCol}H` + (hideCursor ? "\x1b[?25l" : "\x1b[?25h");
863
+ out += SYNC_OFF;
864
+ stdout.write(out);
865
+ this.paintedRows = frameRows;
866
+ this.paintedColumns = W;
867
+ this.paintedHeight = H;
868
+ this.paintedCursor = cursor;
869
+ this.lastPaintAt = Date.now();
870
+ }
871
+ scrollBy(delta) {
872
+ const next = Math.max(0, this.scrollOffset + delta);
873
+ if (next === this.scrollOffset)
874
+ return;
875
+ this.scrollOffset = next;
876
+ this.queueScrollRender();
877
+ }
878
+ // Touchpad e rotelle ad alta risoluzione possono inviare decine di eventi in
879
+ // pochi millisecondi. Lo stato avanza subito, ma il terminale riceve al massimo
880
+ // un frame per intervallo visivo: niente coda ANSI che continua a smaltirsi
881
+ // quando il gesto e' gia' terminato.
882
+ queueScrollRender() {
883
+ if (!this.active || this.scrollRenderTimer)
884
+ return;
885
+ const elapsed = Date.now() - this.lastPaintAt;
886
+ const delay = Math.max(0, SCROLL_FRAME_MS - elapsed);
887
+ this.scrollRenderTimer = setTimeout(() => {
888
+ this.scrollRenderTimer = undefined;
889
+ this.render();
890
+ }, delay);
891
+ }
892
+ layout() {
893
+ const cols = this.cols();
894
+ const lines = [];
895
+ if (this.statusLabel !== null)
896
+ lines.push(this.statusLine());
897
+ if (this.statusLabel === null && this.roomStatus?.state === "failed") {
898
+ const notice = this.roomSegment().replace(/\s+/g, " ").trim();
899
+ lines.push(...wrapText(notice, Math.max(8, cols - 3)).slice(0, 3).map(line => ` ${line}`));
900
+ lines.push(fitLine(tr(" Ctrl+R dettagli", " Ctrl+R details"), cols - 1));
901
+ }
902
+ // La riga live descrive gia' il team; la board completa si consulta a parte.
903
+ if (this.kingdomBoard && this.statusLabel === null)
904
+ lines.push(fitLine(this.kingdomBoard[0] ?? "", cols - 1));
905
+ for (const f of this.h.footerLines())
906
+ lines.push(f);
907
+ if (!this.modal && !this.queueEditor)
908
+ lines.push(...queuePreview(this.queue, cols, this.h.queueStyle, !this.running));
909
+ const questionStart = lines.length;
910
+ const questions = !this.modal && !this.queueEditor
911
+ ? this.questions.render(cols, Math.max(5, this.rows() - lines.length - 6), this.h.queueStyle)
912
+ : { lines: [], cursorRow: 0, cursorCol: 1 };
913
+ lines.push(...questions.lines);
914
+ const box = this.h.boxStyle ?? ((s) => s);
915
+ // Etichetta della domanda a risposta libera (modal) sopra il box.
916
+ if (this.modal) {
917
+ const style = this.h.statusStyle ?? ((s) => s);
918
+ lines.push(...wrapText(this.modal.label, Math.max(2, cols - 3), true).map(line => style(` ${line}`)));
919
+ }
920
+ else if (this.queueEditor) {
921
+ const label = tr(" Modifica in coda · Invio salva · Alt+S invia subito · Esc annulla", " Edit queued message · Enter save · Alt+S send now · Esc cancel");
922
+ lines.push(fitLine(this.h.queueStyle?.heading(label) ?? label, cols - 1));
923
+ }
924
+ const prompt = this.h.promptText();
925
+ const pcols = visibleLength(prompt);
926
+ const cell = Math.max(8, cols - 3);
927
+ const badge = !this.modal ? (this.h.composerBadge?.() ?? "") : "";
928
+ const badgeSuffix = badge ? box(" │ ") + badge : "";
929
+ const badgeCols = visibleLength(badgeSuffix);
930
+ const tw = Math.max(4, cell - pcols - badgeCols);
931
+ // Badge artifact degli allegati immagine citati nel buffer: appaiono DENTRO
932
+ // la regione input (sopra il box), cosi' l'immagine incollata si vede come
933
+ // oggetto del prompt e NON come messaggio in chat.
934
+ if (!this.modal) {
935
+ const badges = this.h.imageBadges?.(this.buffer) ?? [];
936
+ if (badges.length)
937
+ lines.push(fitLine(badges.join(" ").replace(/\n/g, " "), cols - 1));
938
+ }
939
+ const { rows, crow, ccol, starts } = wrapInput(this.buffer, this.cursor, tw);
940
+ const inputCap = Math.max(1, Math.min(3, Math.floor(this.rows() / 4)));
941
+ const rowOffset = Math.max(0, Math.min(crow - Math.floor(inputCap / 2), rows.length - inputCap));
942
+ const visibleRows = rows.slice(rowOffset, rowOffset + inputCap);
943
+ const padding = !this.modal && !this.queueEditor && this.rows() >= 16 && this.h.composerPadding?.();
944
+ const blank = " " + (this.h.composerStyle?.(" ".repeat(cell)) ?? " ".repeat(cell));
945
+ if (padding)
946
+ lines.push(blank);
947
+ const inputStart = lines.length;
948
+ const highlight = createAttachmentHighlighter(this.buffer, this.modal ? undefined : this.h.attachmentStyle);
949
+ visibleRows.forEach((row, k) => {
950
+ const pre = k === 0 && rowOffset === 0 ? prompt : " ".repeat(pcols);
951
+ const placeholder = !this.buffer && !this.modal && !this.queueEditor ? this.h.placeholder?.() : undefined;
952
+ const highlighted = placeholder ? fitLine(placeholder, tw) : highlight(row, starts[rowOffset + k]);
953
+ const content = pre + padEnd(highlighted, tw) + (k === 0 ? badgeSuffix : " ".repeat(badgeCols));
954
+ lines.push(" " + (this.h.composerStyle?.(content) ?? content));
955
+ });
956
+ if (padding)
957
+ lines.push(blank);
958
+ if (this.roomStatus?.state === "done") {
959
+ const notice = ` ${this.roomStatus.label}`;
960
+ lines.push(fitLine(this.h.statusStyle?.(notice) ?? notice, cols - 1));
961
+ }
962
+ if (!this.modal) {
963
+ for (const row of this.h.belowInputLines?.() ?? [])
964
+ lines.push(fitLine(row, cols - 1));
965
+ }
966
+ const action = this.ctrlCConfirmation?.action;
967
+ const warning = action === "stop" ? tr("fermare il lavoro e mettere in pausa la coda", "stop work and pause the queue") : action === "clear" ? tr("svuotare la bozza", "clear the draft") : tr("uscire", "exit");
968
+ const hint = action
969
+ ? tr(" Ctrl+C ancora entro 2s per {warning}. Altro tasto annulla.", " Ctrl+C again within 2s to {warning}. Any other key cancels.", { warning })
970
+ : this.capturingImage ? tr(" Lettura immagine dagli appunti...", " Reading clipboard image...") : this.h.hint(this.buffer);
971
+ if (hint) {
972
+ const hints = action ? wrapText(hint, Math.max(8, cols - 1)) : [hint];
973
+ for (const row of hints)
974
+ lines.push(fitLine(this.h.statusStyle ? this.h.statusStyle(row) : row, cols - 1));
975
+ }
976
+ const prefixCols = 1 + pcols;
977
+ return {
978
+ lines,
979
+ cursorRow: this.questions.focused && questions.lines.length ? questionStart + questions.cursorRow : inputStart + crow - rowOffset,
980
+ cursorCol: this.questions.focused && questions.lines.length ? questions.cursorCol : prefixCols + ccol + 1,
981
+ inputStart, inputRows: this.questions.focused ? 0 : visibleRows.length, textWidth: tw, prefixCols,
982
+ rowStarts: starts.slice(rowOffset, rowOffset + inputCap),
983
+ rowLengths: visibleRows.map((r) => r.length),
984
+ };
985
+ }
986
+ // Segmento VIVO della stanza Danilov per la status-line: stato animato
987
+ // (running pulsa col frame, done/failed con glyph fisso). Vuoto se nessuna.
988
+ roomSegment() {
989
+ if (!this.roomStatus || this.roomStatus.state === "done")
990
+ return "";
991
+ const style = this.h.statusStyle ?? ((s) => s);
992
+ const { label, state } = this.roomStatus;
993
+ if (state === "running") {
994
+ const g = SPINNER[this.frame % SPINNER.length];
995
+ return `${g} ${label}`;
996
+ }
997
+ const glyph = state === "failed" ? "✗" : "✦";
998
+ return style(`${glyph} ${label}`);
999
+ }
1000
+ statusLine() {
1001
+ const elapsed = Math.round((Date.now() - this.startedAt) / 1000);
1002
+ const style = this.h.statusStyle ?? ((s) => s);
1003
+ const label = this.statusLabel ?? tr("Elaborazione", "Working");
1004
+ const glyph = this.statusTool ? pulseGlyph(this.statusTool, this.frame) : SPINNER[this.frame % SPINNER.length];
1005
+ const name = this.statusTool ? pulseToolName(this.statusTool, label, this.frame) : style(label);
1006
+ const meta = [elapsed >= 1 ? fmtElapsed(elapsed) : "", this.h.runningHint?.() ?? "", this.roomSegment()].filter(Boolean);
1007
+ const right = style(this.cols() >= 64 && (!this.queue.length || this.modal || this.queueEditor)
1008
+ ? tr("Alt+S subito · Esc ferma", "Alt+S send now · Esc stop") : tr("Esc ferma", "Esc stop"));
1009
+ const budget = Math.max(0, this.cols() - terminalWidth(right) - 5);
1010
+ let left = ` ${glyph} ${name}`;
1011
+ for (const segment of meta) {
1012
+ const candidate = left + style(` · ${segment}`);
1013
+ if (terminalWidth(candidate) > budget)
1014
+ break;
1015
+ left = candidate;
1016
+ }
1017
+ left = fitLine(left, budget);
1018
+ return fitLine(left + " ".repeat(Math.max(2, this.cols() - 2 - terminalWidth(left) - terminalWidth(right))) + right, this.cols() - 1);
1019
+ }
1020
+ // ── input ─────────────────────────────────────────────────────────────────
1021
+ // Parser RAW dello stdin: mouse (SGR), sequenze escape (frecce/Home/End/Page/
1022
+ // Delete con modificatori), Alt+lettera, control char, caratteri stampabili.
1023
+ clearPendingEscape() {
1024
+ if (this.escapeTimer)
1025
+ clearTimeout(this.escapeTimer);
1026
+ this.escapeTimer = undefined;
1027
+ this.escapePending = "";
1028
+ }
1029
+ feed(s, flushEscape = false) {
1030
+ if (!this.active)
1031
+ return;
1032
+ s = this.escapePending + s;
1033
+ this.clearPendingEscape();
1034
+ if (s !== "\x03")
1035
+ this.cancelCtrlC();
1036
+ // Continuazione di un incollo (bracketed paste) iniziato in un frame precedente.
1037
+ if (this.pasteBuffer !== null) {
1038
+ const combined = this.pasteBuffer + s;
1039
+ const endIdx = combined.indexOf("\x1b[201~");
1040
+ if (endIdx >= 0) {
1041
+ this.pasteBuffer = null;
1042
+ this.handlePaste(combined.slice(0, endIdx));
1043
+ s = combined.slice(endIdx + 6);
1044
+ }
1045
+ else {
1046
+ this.pasteBuffer = combined;
1047
+ return;
1048
+ }
1049
+ }
1050
+ let i = 0;
1051
+ while (i < s.length) {
1052
+ const ch = s[i];
1053
+ if (ch === "\x1b") {
1054
+ const rest = s.slice(i);
1055
+ // A terminal can split even the paste-start marker between stdin
1056
+ // chunks. Preserve the escape prefix instead of typing "200~" and
1057
+ // losing the first image paste. A lone Esc still acts after 30 ms.
1058
+ if (rest === "\x1b" || /^\x1b(?:\[[0-9;:<=>?]*|O)$/.test(rest)) {
1059
+ if (!flushEscape && rest.length < 64) {
1060
+ this.escapePending = rest;
1061
+ this.escapeTimer = setTimeout(() => this.feed("", true), 30);
1062
+ return;
1063
+ }
1064
+ if (rest !== "\x1b")
1065
+ return;
1066
+ }
1067
+ // Bracketed paste: ESC[200~ <testo> ESC[201~. Catturiamo il blocco intero
1068
+ // (puo' essere su piu' frame: bufferizziamo finche' non arriva 201~).
1069
+ if (rest.startsWith("\x1b[200~")) {
1070
+ const body = rest.slice(6);
1071
+ const endIdx = body.indexOf("\x1b[201~");
1072
+ if (endIdx >= 0) {
1073
+ this.handlePaste(body.slice(0, endIdx));
1074
+ i += 6 + endIdx + 6;
1075
+ }
1076
+ else {
1077
+ // Fine non ancora arrivata: bufferizza il resto, attende il prossimo frame.
1078
+ this.pasteBuffer = body;
1079
+ i = s.length;
1080
+ }
1081
+ continue;
1082
+ }
1083
+ const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(rest);
1084
+ if (mouse) {
1085
+ this.handleMouse(+mouse[1], +mouse[2], +mouse[3], mouse[4] === "M");
1086
+ i += mouse[0].length;
1087
+ continue;
1088
+ }
1089
+ const csi = /^\x1b[[O]([0-9;]*)([A-Za-z~])/.exec(rest);
1090
+ if (csi) {
1091
+ this.handleEscape(csi[1] ?? "", csi[2]);
1092
+ i += csi[0].length;
1093
+ continue;
1094
+ }
1095
+ const alt = s[i + 1];
1096
+ if (alt === "\r" || alt === "\n") {
1097
+ this.dispatchKey("", { meta: true, name: "return" });
1098
+ i += 2;
1099
+ continue;
1100
+ }
1101
+ if (alt && alt >= " " && alt !== "\x7f") {
1102
+ this.dispatchKey(alt, { meta: true, name: alt.toLowerCase() });
1103
+ i += 2;
1104
+ continue;
1105
+ }
1106
+ // Esc isolato (\x1b non seguito da una sequenza riconosciuta): risolve in
1107
+ // ordine di priorita'. Mentre un run e' in corso, FERMA la generazione
1108
+ // (come promette la status-line "esc ferma"); prima era gestito solo
1109
+ // Ctrl+C, da cui il bug "Esc non interrompe".
1110
+ if (this.screenVisible) {
1111
+ if (this.screen.key("", { name: "escape" }) === "close")
1112
+ this.closeScreen();
1113
+ this.render();
1114
+ }
1115
+ else if (this.questions.focused && !this.modal)
1116
+ this.questions.hide();
1117
+ else if (this.modal)
1118
+ this.resolveModal(null); // annulla la domanda
1119
+ else if (this.queueEditor)
1120
+ this.finishQueueEdit(false);
1121
+ else if (this.vassalOpen) {
1122
+ this.vassalOpen = false;
1123
+ this.render();
1124
+ } // esci dal viewer
1125
+ else if (this.nativeScroll && this.scrollOffset > 0) {
1126
+ this.scrollOffset = 0;
1127
+ this.render();
1128
+ }
1129
+ else if (this.running) {
1130
+ this.stopRun();
1131
+ }
1132
+ i += 1; // ESC isolato
1133
+ continue;
1134
+ }
1135
+ const code = ch.charCodeAt(0);
1136
+ if (code === 13 || code === 10) {
1137
+ this.dispatchKey("", { name: "return" });
1138
+ i += 1;
1139
+ continue;
1140
+ }
1141
+ if (code === 9) {
1142
+ this.dispatchKey("\t", { name: "tab" });
1143
+ i += 1;
1144
+ continue;
1145
+ }
1146
+ if (code === 127) {
1147
+ this.dispatchKey("", { name: "backspace" });
1148
+ i += 1;
1149
+ continue;
1150
+ }
1151
+ if (code === 8) {
1152
+ this.dispatchKey("\x08", { name: "backspace", ctrl: true });
1153
+ i += 1;
1154
+ continue;
1155
+ }
1156
+ if (code < 32) {
1157
+ this.dispatchKey(ch, { ctrl: true, name: String.fromCharCode(code + 96) });
1158
+ i += 1;
1159
+ continue;
1160
+ }
1161
+ this.dispatchKey(ch, {});
1162
+ i += 1;
1163
+ }
1164
+ }
1165
+ handleEscape(params, final) {
1166
+ const parts = params.split(";");
1167
+ const mod = parts.length > 1 ? Math.max(0, Number(parts[1]) - 1) : 0;
1168
+ const shift = Boolean(mod & 1), meta = Boolean(mod & 2), ctrl = Boolean(mod & 4);
1169
+ // Shift+Tab arriva come CSI Z (CBT): cicla la modalita' permessi.
1170
+ if (final === "Z") {
1171
+ this.dispatchKey("", { name: "tab", shift: true });
1172
+ return;
1173
+ }
1174
+ if ((final === "u" && parts[0] === "13") || (final === "~" && parts[0] === "27" && parts[2] === "13")) {
1175
+ this.dispatchKey("", { name: "return", shift, meta, ctrl });
1176
+ return;
1177
+ }
1178
+ const codepoint = final === "u" ? parts[0] : final === "~" && parts[0] === "27" ? parts[2] : undefined;
1179
+ if ((codepoint === "118" || codepoint === "86") && (ctrl || meta)) {
1180
+ this.dispatchKey("", { name: "v", shift, meta, ctrl });
1181
+ return;
1182
+ }
1183
+ const arrows = { A: "up", B: "down", C: "right", D: "left", H: "home", F: "end" };
1184
+ let name = arrows[final];
1185
+ if (final === "~") {
1186
+ const tilde = { "1": "home", "7": "home", "4": "end", "8": "end", "5": "pageup", "6": "pagedown", "3": "delete" };
1187
+ name = tilde[parts[0] ?? ""];
1188
+ }
1189
+ // Freccia su/giu' NUDA: disambigua scroll(touchpad) vs pressione (timer).
1190
+ if ((name === "up" || name === "down") && !ctrl && !meta && !shift && !this.screenVisible
1191
+ && !this.questions.focused && (!this.nativeScroll || this.scrollOffset > 0)) {
1192
+ this.onArrow(name);
1193
+ return;
1194
+ }
1195
+ if (name)
1196
+ this.dispatchKey("", { name, ctrl, meta, shift });
1197
+ }
1198
+ // Una freccia isolata mantiene history/cursore; la seconda riconosce subito
1199
+ // una raffica wheel. La finestra arrowScrollUntil include gli eventi residui,
1200
+ // evitando sia il debounce dell'intero gesto sia richiami spurii alla history.
1201
+ onArrow(dir) {
1202
+ const now = Date.now();
1203
+ if (now <= this.arrowScrollUntil || this.scrollOffset > 0) {
1204
+ const step = now <= this.arrowScrollUntil ? WHEEL_ARROW_LINES : 1;
1205
+ this.scrollBy(dir === "up" ? step : -step);
1206
+ this.arrowScrollUntil = now + 140;
1207
+ return;
1208
+ }
1209
+ this.pendingArrows.push(dir);
1210
+ // Due eventi bastano a riconoscere il gesto. Non attendere il silenzio:
1211
+ // un debounce continuamente rinnovato accumula tutta la corsa del touchpad.
1212
+ // flushArrows apre la finestra wheel anche per il terzo evento e i residui.
1213
+ if (this.pendingArrows.length >= 2) {
1214
+ this.flushArrows();
1215
+ return;
1216
+ }
1217
+ this.arrowTimer = setTimeout(() => this.flushArrows(), 80);
1218
+ }
1219
+ flushArrows() {
1220
+ if (this.arrowTimer) {
1221
+ clearTimeout(this.arrowTimer);
1222
+ this.arrowTimer = undefined;
1223
+ }
1224
+ const arr = this.pendingArrows;
1225
+ this.pendingArrows = [];
1226
+ if (!arr.length)
1227
+ return;
1228
+ if (arr.length >= 2) {
1229
+ this.arrowScrollUntil = Date.now() + 140;
1230
+ const up = arr.filter((d) => d === "up").length;
1231
+ this.scrollBy((up - (arr.length - up)) * WHEEL_ARROW_LINES);
1232
+ }
1233
+ else if (this.scrollOffset > 0) {
1234
+ // Se la chat e' gia' scrollata, una freccia nuda continua a navigare il
1235
+ // viewport. La history torna disponibile quando si e' di nuovo in fondo.
1236
+ this.scrollBy(arr[0] === "up" ? 1 : -1);
1237
+ }
1238
+ else if (arr[0] === "up") {
1239
+ this.onUp();
1240
+ }
1241
+ else {
1242
+ this.onDown();
1243
+ }
1244
+ }
1245
+ dispatchKey(s, k) {
1246
+ if (!this.active)
1247
+ return;
1248
+ const seq = s;
1249
+ if (!(k.ctrl && k.name === "c"))
1250
+ this.cancelCtrlC();
1251
+ if (this.screenVisible) {
1252
+ if (this.screen.key(s, k) === "close")
1253
+ this.closeScreen();
1254
+ this.render();
1255
+ return;
1256
+ }
1257
+ if (!this.modal && !this.overlay && !this.queueEditor) {
1258
+ if (k.shift && k.name === "left" && this.questions.count) {
1259
+ this.questions.open();
1260
+ return;
1261
+ }
1262
+ if (this.questions.focused && !(k.ctrl && k.name === "c")) {
1263
+ this.questions.key(s, k);
1264
+ return;
1265
+ }
1266
+ }
1267
+ if (k.meta && k.name === "q" && !this.modal) {
1268
+ this.showQueue();
1269
+ return;
1270
+ }
1271
+ if (k.meta && k.name === "t" && !this.modal && !this.overlay && !this.queueEditor) {
1272
+ if (this.queue.sendToTeam())
1273
+ this.startNextQueued();
1274
+ else
1275
+ this.showQueue();
1276
+ this.render();
1277
+ return;
1278
+ }
1279
+ if (k.ctrl && k.name === "c" && this.queueEditor && !this.modal) {
1280
+ this.finishQueueEdit(false);
1281
+ return;
1282
+ }
1283
+ if (k.ctrl && k.name === "r" && !this.modal) {
1284
+ this.showReview();
1285
+ return;
1286
+ }
1287
+ if (k.ctrl && k.name === "c")
1288
+ return this.onCtrlC();
1289
+ if (k.ctrl && k.name === "g") {
1290
+ // Con un regno attivo: Ctrl+G entra nel viewer e cicla i vassalli (poi esce).
1291
+ if (this.vassals && this.vassals.length) {
1292
+ if (!this.vassalOpen) {
1293
+ this.vassalOpen = true;
1294
+ this.vassalIdx = 0;
1295
+ }
1296
+ else {
1297
+ this.vassalIdx += 1;
1298
+ if (this.vassalIdx >= this.vassals.length)
1299
+ this.vassalOpen = false;
1300
+ }
1301
+ this.render();
1302
+ return;
1303
+ }
1304
+ this.h.onShowPlan?.();
1305
+ return;
1306
+ }
1307
+ if (k.ctrl && k.name === "b") {
1308
+ this.h.onBackground?.();
1309
+ return;
1310
+ }
1311
+ // Ctrl+V (e Alt+V come alias): incolla un'immagine dalla clipboard di sistema
1312
+ // (i terminali non mandano i byte binari via stdin, quindi li chiediamo
1313
+ // direttamente all'OS). Nota: se il terminale gestisce Ctrl+V come incolla
1314
+ // nativo, arriva invece come bracketed paste (handlePaste se ne occupa);
1315
+ // questo ramo scatta quando Ctrl+V raggiunge l'app come control char (0x16).
1316
+ if ((k.ctrl || k.meta) && k.name === "v") {
1317
+ void this.captureImage();
1318
+ return;
1319
+ }
1320
+ if (k.ctrl && k.name === "o" && !this.modal) {
1321
+ this.showReview("modifiche");
1322
+ return;
1323
+ }
1324
+ // Scroll della CHAT (input resta fisso): PageUp/Down, Shift+frecce.
1325
+ if (k.name === "pageup")
1326
+ return this.scrollBy(this.pageStep());
1327
+ if (k.name === "pagedown")
1328
+ return this.scrollBy(-this.pageStep());
1329
+ if (k.shift && k.name === "up")
1330
+ return this.scrollBy(1);
1331
+ if (k.shift && k.name === "down")
1332
+ return this.scrollBy(-1);
1333
+ if (k.meta && k.name === "s")
1334
+ return this.submit(true);
1335
+ if (k.name === "return" || k.name === "enter")
1336
+ return this.submit(Boolean(k.meta));
1337
+ if (k.shift && k.name === "tab") {
1338
+ this.h.onCycleMode?.();
1339
+ this.render();
1340
+ return;
1341
+ }
1342
+ if (k.name === "tab")
1343
+ return this.complete();
1344
+ if ((k.ctrl || k.meta) && k.name === "backspace")
1345
+ return this.edit(() => this.deleteWord());
1346
+ if ((k.ctrl || k.meta) && k.name === "delete")
1347
+ return this.edit(() => this.deleteWordForward());
1348
+ if (seq === "\x08" || seq === "\x17")
1349
+ return this.edit(() => this.deleteWord());
1350
+ if (k.name === "backspace")
1351
+ return this.edit(() => {
1352
+ if (this.cursor <= 0)
1353
+ return;
1354
+ // Cancellazione ATOMICA del token "img #N": se il cursore tocca un
1355
+ // riferimento immagine, lo rimuove per intero (l'allegato si scollega).
1356
+ const span = this.h.onBackspaceToken?.(this.buffer, this.cursor);
1357
+ if (span) {
1358
+ this.buffer = this.buffer.slice(0, span.start) + this.buffer.slice(span.end);
1359
+ this.cursor = span.start;
1360
+ return;
1361
+ }
1362
+ this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
1363
+ this.cursor -= 1;
1364
+ });
1365
+ if (k.name === "delete")
1366
+ return this.edit(() => {
1367
+ this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
1368
+ });
1369
+ if ((k.ctrl || k.meta) && k.name === "left")
1370
+ return this.edit(() => { this.cursor = wordLeft(this.buffer, this.cursor); });
1371
+ if ((k.ctrl || k.meta) && k.name === "right")
1372
+ return this.edit(() => { this.cursor = wordRight(this.buffer, this.cursor); });
1373
+ if (k.name === "left")
1374
+ return this.edit(() => { this.cursor = Math.max(0, this.cursor - 1); });
1375
+ if (k.name === "right")
1376
+ return this.edit(() => { this.cursor = Math.min(this.buffer.length, this.cursor + 1); });
1377
+ if (k.name === "home" || (k.ctrl && k.name === "a"))
1378
+ return this.edit(() => { this.cursor = 0; });
1379
+ if (k.name === "end" || (k.ctrl && k.name === "e"))
1380
+ return this.edit(() => { this.cursor = this.buffer.length; });
1381
+ if (k.ctrl && k.name === "u")
1382
+ return this.edit(() => { this.buffer = this.buffer.slice(this.cursor); this.cursor = 0; });
1383
+ if (k.ctrl && k.name === "k")
1384
+ return this.edit(() => { this.buffer = this.buffer.slice(0, this.cursor); });
1385
+ if (k.ctrl && k.name === "w")
1386
+ return this.edit(() => this.deleteWord());
1387
+ if (k.name === "up")
1388
+ return this.onUp();
1389
+ if (k.name === "down")
1390
+ return this.onDown();
1391
+ if (s && !k.ctrl && !k.meta && !isControl(s)) {
1392
+ return this.edit(() => {
1393
+ const clean = s.replace(/[\r\n]+/g, " ");
1394
+ this.buffer = this.buffer.slice(0, this.cursor) + clean + this.buffer.slice(this.cursor);
1395
+ this.cursor += clean.length;
1396
+ });
1397
+ }
1398
+ }
1399
+ // Testo incollato (bracketed paste): l'hook onPaste puo' trasformarlo (es.
1400
+ // un path/data-URI immagine -> "img #N"); altrimenti si inserisce com'e'.
1401
+ // I newline si normalizzano a spazio: l'input e' una riga sola (Invio invia).
1402
+ handlePaste(text) {
1403
+ if (this.screenVisible) {
1404
+ this.screen.paste?.(text);
1405
+ this.render();
1406
+ return;
1407
+ }
1408
+ if (this.questions.focused && !this.modal && !this.overlay) {
1409
+ this.questions.paste(text);
1410
+ return;
1411
+ }
1412
+ // Incollo di un'IMMAGINE con Ctrl+V: i terminali intercettano Ctrl+V come
1413
+ // paste nativo, ma un'immagine non ha rappresentazione testuale, quindi il
1414
+ // bracketed paste arriva vuoto (o con soli spazi/whitespace). In quel caso
1415
+ // proviamo a leggere i byte dalla clipboard di sistema (stesso flusso di
1416
+ // Alt+V). Solo fuori dai modali e con la sessione attiva.
1417
+ if ((!text || !text.trim()) && !this.modal && this.active) {
1418
+ void this.captureImage();
1419
+ return;
1420
+ }
1421
+ if (!text)
1422
+ return;
1423
+ if (this.modal) {
1424
+ // Nelle domande a risposta libera l'incollo va comunque nel buffer testuale.
1425
+ const clean = text.replace(/[\r\n]+/g, " ");
1426
+ this.edit(() => {
1427
+ this.buffer = this.buffer.slice(0, this.cursor) + clean + this.buffer.slice(this.cursor);
1428
+ this.cursor += clean.length;
1429
+ });
1430
+ return;
1431
+ }
1432
+ const transformed = this.h.onPaste?.(text);
1433
+ const insert = (transformed ?? text).replace(/[\r\n]+/g, " ");
1434
+ this.edit(() => {
1435
+ this.buffer = this.buffer.slice(0, this.cursor) + insert + this.buffer.slice(this.cursor);
1436
+ this.cursor += insert.length;
1437
+ });
1438
+ }
1439
+ // Ctrl+V: chiede all'host i byte dell'immagine nella clipboard di sistema. Se
1440
+ // c'e' un'immagine, l'host ritorna il riferimento "img #N" da inserire nel
1441
+ // buffer (l'host stesso mostra eventuale feedback/anteprima). Async perche'
1442
+ // la lettura della clipboard passa da un sottoprocesso di sistema.
1443
+ async captureImage() {
1444
+ if (!this.active || this.modal || this.screenVisible || this.capturingImage || !this.h.onCaptureImage)
1445
+ return;
1446
+ this.capturingImage = true;
1447
+ const generation = this.inputGeneration;
1448
+ const editor = this.queueEditor;
1449
+ let inserted = false;
1450
+ this.render();
1451
+ try {
1452
+ const ref = await this.h.onCaptureImage();
1453
+ // Do not attach a late result to another draft, a modal or a closed UI.
1454
+ if (!ref || !this.active || this.modal || this.screenVisible
1455
+ || this.inputGeneration !== generation || this.queueEditor !== editor)
1456
+ return;
1457
+ const insert = ref.replace(/[\r\n]+/g, " ");
1458
+ this.edit(() => {
1459
+ this.buffer = this.buffer.slice(0, this.cursor) + insert + this.buffer.slice(this.cursor);
1460
+ this.cursor += insert.length;
1461
+ });
1462
+ inserted = true;
1463
+ }
1464
+ catch (error) {
1465
+ if (this.active && !this.modal && !this.screenVisible && this.inputGeneration === generation && this.queueEditor === editor) {
1466
+ this.reportFailure(`${tr("Errore clipboard", "Clipboard error")}: ${error instanceof Error ? error.message : String(error)}`);
1467
+ }
1468
+ }
1469
+ finally {
1470
+ this.capturingImage = false;
1471
+ const submit = this.deferredImageSubmit;
1472
+ this.deferredImageSubmit = null;
1473
+ this.render();
1474
+ if (inserted && submit !== null)
1475
+ this.submit(submit);
1476
+ }
1477
+ }
1478
+ pageStep() {
1479
+ const header = this.h.headerLines?.().length ?? 0;
1480
+ return Math.max(1, this.rows() - this.liveContent().lines.length - header - 1);
1481
+ }
1482
+ handleMouse(button, x, y, press) {
1483
+ if (this.screenVisible) {
1484
+ if (press && (button & 0b1000000)) {
1485
+ this.screen.scroll(button & 1 ? WHEEL_MOUSE_LINES : -WHEEL_MOUSE_LINES);
1486
+ this.queueScrollRender();
1487
+ }
1488
+ return;
1489
+ }
1490
+ // Rotella (bit 6): SEMPRE scroll della CHAT, anche sul rilascio e ovunque sia
1491
+ // il puntatore. L'input resta fisso: lo scroll NON "ruba" mai la posizione
1492
+ // del cursore ne' digita nell'input. 64 = su, 65 = giu'.
1493
+ if (button & 0b1000000) {
1494
+ if (press)
1495
+ this.scrollBy(button & 1 ? -WHEEL_MOUSE_LINES : WHEEL_MOUSE_LINES);
1496
+ return;
1497
+ }
1498
+ // Da qui in giu' solo i CLICK veri: il rilascio (press=false) e i movimenti
1499
+ // (bit 5, drag) non toccano l'input.
1500
+ if (!press)
1501
+ return;
1502
+ if (button & 0b0100000)
1503
+ return; // movimento/drag
1504
+ if ((button & 0b11) !== 0)
1505
+ return; // solo click sinistro (no middle/right)
1506
+ const lay = this.lastLayout;
1507
+ if (!lay || this.overlay || lay.inputRows === 0)
1508
+ return;
1509
+ // Il click sposta il cursore SOLO se cade DENTRO le righe del box input.
1510
+ const within = (y - this.liveStartRow) - lay.inputStart;
1511
+ if (within < 0 || within >= lay.inputRows)
1512
+ return;
1513
+ // Mappa colonna->offset usando lo start REALE della riga (word-wrap: righe
1514
+ // di lunghezza variabile, non multipli fissi di textWidth).
1515
+ const rowStart = lay.rowStarts[within] ?? this.buffer.length;
1516
+ const rowLen = lay.rowLengths[within] ?? 0;
1517
+ const col = Math.max(0, Math.min((x - 1) - lay.prefixCols, rowLen));
1518
+ this.cursor = Math.max(0, Math.min(this.buffer.length, rowStart + col));
1519
+ this.render();
1520
+ }
1521
+ onUp() {
1522
+ const tw = this.lastLayout?.textWidth ?? this.buffer.length + 1;
1523
+ if (this.cursor >= tw) {
1524
+ this.edit(() => { this.cursor = Math.max(0, this.cursor - tw); });
1525
+ return;
1526
+ }
1527
+ this.historyPrev();
1528
+ }
1529
+ onDown() {
1530
+ const tw = this.lastLayout?.textWidth ?? this.buffer.length + 1;
1531
+ if (this.buffer.length - this.cursor >= tw) {
1532
+ this.edit(() => { this.cursor = Math.min(this.buffer.length, this.cursor + tw); });
1533
+ return;
1534
+ }
1535
+ this.historyNext();
1536
+ }
1537
+ edit(mutate) { mutate(); this.render(); }
1538
+ deleteWord() {
1539
+ const left = this.buffer.slice(0, this.cursor).replace(/\s*\S+\s*$/, "");
1540
+ this.buffer = left + this.buffer.slice(this.cursor);
1541
+ this.cursor = left.length;
1542
+ }
1543
+ deleteWordForward() {
1544
+ const right = this.buffer.slice(this.cursor).replace(/^\s*\S+\s*/, "");
1545
+ this.buffer = this.buffer.slice(0, this.cursor) + right;
1546
+ }
1547
+ complete() {
1548
+ const next = this.h.completion(this.buffer);
1549
+ if (next === null || next === this.buffer)
1550
+ return;
1551
+ this.buffer = next;
1552
+ this.cursor = next.length;
1553
+ this.render();
1554
+ }
1555
+ historyPrev() {
1556
+ if (!this.history.length)
1557
+ return;
1558
+ if (this.histIdx === -1)
1559
+ this.draft = this.buffer;
1560
+ this.histIdx = this.histIdx === -1 ? this.history.length - 1 : Math.max(0, this.histIdx - 1);
1561
+ this.buffer = this.history[this.histIdx] ?? "";
1562
+ this.cursor = this.buffer.length;
1563
+ this.render();
1564
+ }
1565
+ historyNext() {
1566
+ if (this.histIdx === -1)
1567
+ return;
1568
+ this.histIdx += 1;
1569
+ if (this.histIdx >= this.history.length) {
1570
+ this.histIdx = -1;
1571
+ this.buffer = this.draft;
1572
+ }
1573
+ else
1574
+ this.buffer = this.history[this.histIdx] ?? "";
1575
+ this.cursor = this.buffer.length;
1576
+ this.render();
1577
+ }
1578
+ onCtrlC() {
1579
+ if (this.modal) {
1580
+ this.resolveModal(null);
1581
+ return;
1582
+ }
1583
+ const action = this.running ? "stop" : this.buffer ? "clear" : "quit";
1584
+ if (this.ctrlCConfirmation?.action === action && Date.now() < this.ctrlCConfirmation.until) {
1585
+ this.cancelCtrlC();
1586
+ if (action === "stop") {
1587
+ this.stopRun();
1588
+ this.render();
1589
+ return;
1590
+ }
1591
+ if (action === "clear") {
1592
+ this.inputGeneration++;
1593
+ this.buffer = "";
1594
+ this.cursor = 0;
1595
+ this.histIdx = -1;
1596
+ this.render();
1597
+ return;
1598
+ }
1599
+ this.quit();
1600
+ return;
1601
+ }
1602
+ this.cancelCtrlC();
1603
+ this.ctrlCConfirmation = { action, until: Date.now() + 2000 };
1604
+ this.ctrlCTimer = setTimeout(() => { this.cancelCtrlC(); if (this.active)
1605
+ this.render(); }, 2000);
1606
+ this.render();
1607
+ }
1608
+ cancelCtrlC() {
1609
+ if (this.ctrlCTimer)
1610
+ clearTimeout(this.ctrlCTimer);
1611
+ this.ctrlCTimer = undefined;
1612
+ this.ctrlCConfirmation = null;
1613
+ }
1614
+ stopRun() {
1615
+ this.cancelCtrlC();
1616
+ const queued = this.queue.length;
1617
+ this.queue.paused = true;
1618
+ this.runAbort?.abort();
1619
+ if (queued)
1620
+ this.setRoomStatus(tr("Lavoro interrotto. {queued} messaggi conservati in pausa · Alt+Q o /queue per gestirli.", "Work interrupted. {queued} messages paused · Alt+Q or /queue to manage.", { queued }), "failed");
1621
+ }
1622
+ // Domanda a risposta libera: l'editing finisce nel buffer, Invio risolve.
1623
+ queueQuestion(title, options, signal) {
1624
+ if (!this.active)
1625
+ return Promise.resolve(null);
1626
+ return this.questions.enqueue(title, options, signal);
1627
+ }
1628
+ promptInput(label) {
1629
+ if (!this.active)
1630
+ return Promise.resolve(null);
1631
+ this.inputGeneration++;
1632
+ this.cancelCtrlC();
1633
+ const labelRows = wrapText(label, Math.max(2, this.cols() - 3), true);
1634
+ if (labelRows.length > Math.max(2, Math.floor(this.rows() / 3))) {
1635
+ const style = this.h.statusStyle ?? ((s) => s);
1636
+ this.write("\n" + labelRows.map(line => style(" " + line)).join("\n") + "\n");
1637
+ label = tr("Rispondi qui · Invio conferma · Esc annulla", "Reply here · Enter confirm · Esc cancel");
1638
+ }
1639
+ const draft = { buffer: this.buffer, cursor: this.cursor, histIdx: this.histIdx };
1640
+ this.buffer = "";
1641
+ this.cursor = 0;
1642
+ this.histIdx = -1;
1643
+ return new Promise((resolve) => {
1644
+ this.modal = { label, resolve, draft };
1645
+ this.render();
1646
+ });
1647
+ }
1648
+ resolveModal(value) {
1649
+ const m = this.modal;
1650
+ if (!m)
1651
+ return;
1652
+ this.modal = null;
1653
+ this.buffer = m.draft.buffer;
1654
+ this.cursor = m.draft.cursor;
1655
+ this.histIdx = m.draft.histIdx;
1656
+ this.render();
1657
+ m.resolve(value);
1658
+ this.startNextQueued();
1659
+ }
1660
+ submit(immediate = false) {
1661
+ if (this.modal) {
1662
+ this.resolveModal(this.buffer);
1663
+ return;
1664
+ }
1665
+ // Enter during a clipboard read waits for the attachment. On failure the
1666
+ // draft stays editable instead of sending a prompt with a missing image.
1667
+ if (this.capturingImage) {
1668
+ this.deferredImageSubmit = Boolean(this.deferredImageSubmit || immediate);
1669
+ return;
1670
+ }
1671
+ if (this.queueEditor) {
1672
+ this.finishQueueEdit(true, immediate);
1673
+ return;
1674
+ }
1675
+ const line = this.buffer;
1676
+ this.buffer = "";
1677
+ this.cursor = 0;
1678
+ this.histIdx = -1;
1679
+ if (!line.trim()) {
1680
+ if (immediate) {
1681
+ const next = this.queue.items().find(entry => !entry.text.trimStart().startsWith("/"));
1682
+ if (next)
1683
+ this.queue.sendNow(next.id);
1684
+ }
1685
+ else if (!this.running && this.queue.paused)
1686
+ this.queue.paused = false;
1687
+ this.startNextQueued();
1688
+ this.render();
1689
+ return;
1690
+ }
1691
+ if (/^\/(?:queue|coda)\s*$/i.test(line.trim())) {
1692
+ this.showQueue();
1693
+ return;
1694
+ }
1695
+ const review = /^\/(?:review|revisione)(?:\s+(\S+))?\s*$/i.exec(line.trim());
1696
+ if (review && this.h.createReviewScreen) {
1697
+ this.showReview(review[1]);
1698
+ return;
1699
+ }
1700
+ this.dispatch(line, immediate);
1701
+ }
1702
+ dispatch(line, immediate = false) {
1703
+ // Una coda ripristinata in pausa non deve bloccare /new, /resume, /help
1704
+ // o /quit. A riposo i comandi controllano la UI senza riprendere la coda.
1705
+ if (!this.running && line.trimStart().startsWith("/")) {
1706
+ this.runLine(line);
1707
+ return;
1708
+ }
1709
+ if (this.running || this.queue.length) {
1710
+ try {
1711
+ this.queue.add(line, immediate ? "immediate" : "after-turn");
1712
+ // Aprire una chat non avvia lavoro da solo. Un nuovo Invio a riposo
1713
+ // riprende invece la coda: il prompt appena scritto non resta bloccato
1714
+ // dietro la pausa applicata durante il ripristino.
1715
+ if (!this.running && !immediate)
1716
+ this.queue.paused = false;
1717
+ this.h.onQueued?.(line);
1718
+ }
1719
+ catch (error) {
1720
+ this.buffer = line;
1721
+ this.cursor = line.length;
1722
+ this.reportFailure(`${tr("Messaggio non accodato", "Message not queued")}: ${error instanceof Error ? error.message : String(error)}`);
1723
+ }
1724
+ this.startNextQueued();
1725
+ this.render();
1726
+ return;
1727
+ }
1728
+ this.queue.paused = false;
1729
+ this.runLine(line);
1730
+ }
1731
+ startNextQueued() {
1732
+ if (!this.active || this.running || this.modal || this.overlay || this.queueEditor)
1733
+ return;
1734
+ const next = this.queue.takeNext();
1735
+ if (next) {
1736
+ this.h.onDequeue?.(next.text);
1737
+ this.runLine(next.text, true, next);
1738
+ }
1739
+ }
1740
+ runLine(line, fromQueue = false, queued) {
1741
+ this.setRoomStatus(null);
1742
+ this.history.push(line);
1743
+ this.running = true;
1744
+ this.startedAt = Date.now();
1745
+ this.tokensNew = 0;
1746
+ this.tokensCache = 0;
1747
+ this.tokensOut = 0;
1748
+ const ac = new AbortController();
1749
+ this.runAbort = ac;
1750
+ this.render();
1751
+ Promise.resolve().then(() => this.h.onSubmit(line, ac.signal, fromQueue, queued))
1752
+ .catch(error => {
1753
+ this.queue.paused = true;
1754
+ this.reportFailure(`${tr("Errore", "Error")}: ${error instanceof Error ? error.message : String(error)}. ${tr("Coda in pausa.", "Queue paused.")}`);
1755
+ })
1756
+ .finally(() => {
1757
+ this.cancelCtrlC();
1758
+ this.runAbort = undefined;
1759
+ this.running = false;
1760
+ this.setStatus(null);
1761
+ if (!this.active)
1762
+ return;
1763
+ this.startNextQueued();
1764
+ this.render();
1765
+ });
1766
+ }
1767
+ quit() {
1768
+ if (!this.active)
1769
+ return;
1770
+ this.active = false;
1771
+ this.questions.clear();
1772
+ this.clearPendingEscape();
1773
+ this.cancelCtrlC();
1774
+ if (this.spinnerTimer) {
1775
+ clearInterval(this.spinnerTimer);
1776
+ this.spinnerTimer = undefined;
1777
+ }
1778
+ if (this.arrowTimer) {
1779
+ clearTimeout(this.arrowTimer);
1780
+ this.arrowTimer = undefined;
1781
+ }
1782
+ if (this.roomTimer) {
1783
+ clearTimeout(this.roomTimer);
1784
+ this.roomTimer = undefined;
1785
+ }
1786
+ if (this.scrollRenderTimer) {
1787
+ clearTimeout(this.scrollRenderTimer);
1788
+ this.scrollRenderTimer = undefined;
1789
+ }
1790
+ if (this.dataListener)
1791
+ stdin.off("data", this.dataListener);
1792
+ if (this.resizeListener)
1793
+ stdout.off("resize", this.resizeListener);
1794
+ if (this.resizeTimer) {
1795
+ clearTimeout(this.resizeTimer);
1796
+ this.resizeTimer = undefined;
1797
+ }
1798
+ if (this.alternateScreen) {
1799
+ stdout.write(ALT_OFF);
1800
+ this.alternateScreen = false;
1801
+ }
1802
+ if (this.nativeScroll)
1803
+ stdout.write(this.nativeOutput.release());
1804
+ stdout.write(PASTE_OFF + NATIVE_SCROLL_MODE + "\x1b[?25h");
1805
+ if (typeof stdin.setRawMode === "function")
1806
+ stdin.setRawMode(false);
1807
+ this.h.onQuit();
1808
+ this.quitResolve?.();
1809
+ }
1810
+ }
1811
+ // Word-wrap dell'input: manda a capo le PAROLE INTERE invece di spezzarle a
1812
+ // meta'. Ogni riga porta il suo offset di partenza nel buffer, cosi' la
1813
+ // posizione del cursore (crow/ccol) resta esatta. Una parola piu' lunga della
1814
+ // larghezza disponibile viene comunque spezzata (hard-cut), altrimenti non
1815
+ // entrerebbe mai.
1816
+ export function wrapInput(buffer, cursor, tw) {
1817
+ const width = Math.max(1, tw);
1818
+ // Ogni riga: { text, start } dove start = indice nel buffer del primo char.
1819
+ const segs = wrapSegments(buffer, width);
1820
+ const rows = segs.map((s) => s.text);
1821
+ const starts = segs.map((s) => s.start);
1822
+ // Mappa il cursore alla riga giusta: l'ultima riga il cui start <= cursor.
1823
+ let crow = 0;
1824
+ for (let r = 0; r < segs.length; r += 1) {
1825
+ if (segs[r].start <= cursor)
1826
+ crow = r;
1827
+ else
1828
+ break;
1829
+ }
1830
+ const seg = segs[crow];
1831
+ let ccol = cursor - seg.start;
1832
+ // Se il cursore cade oltre la fine del testo visibile di questa riga (lo
1833
+ // spazio "mangiato" a fine riga dal wrap), tienilo a fine riga.
1834
+ if (ccol > seg.text.length)
1835
+ ccol = seg.text.length;
1836
+ // Cursore in coda al buffer e l'ultima riga e' piena: apri una riga nuova
1837
+ // cosi' il cursore non si sovrappone all'ultimo carattere.
1838
+ const last = segs[segs.length - 1];
1839
+ if (cursor === buffer.length && cursor > 0 && last.start + last.text.length === cursor && last.text.length >= width) {
1840
+ rows.push("");
1841
+ starts.push(cursor);
1842
+ crow = rows.length - 1;
1843
+ ccol = 0;
1844
+ }
1845
+ return { rows, crow, ccol, starts };
1846
+ }
1847
+ // Suddivide il buffer in righe larghe al massimo `width`, preferendo i confini
1848
+ // di parola (spazi). Ritorna per ogni riga il testo e l'offset di partenza.
1849
+ function wrapSegments(buffer, width) {
1850
+ if (buffer.length === 0)
1851
+ return [{ text: "", start: 0 }];
1852
+ const segs = [];
1853
+ let start = 0;
1854
+ const n = buffer.length;
1855
+ while (start < n) {
1856
+ let end = Math.min(start + width, n);
1857
+ if (end < n) {
1858
+ // C'e' ancora testo dopo: cerca l'ultimo spazio entro [start, end] per
1859
+ // spezzare su confine di parola.
1860
+ let brk = -1;
1861
+ for (let i = end; i > start; i -= 1) {
1862
+ if (buffer[i] === " ") {
1863
+ brk = i;
1864
+ break;
1865
+ }
1866
+ }
1867
+ if (brk > start) {
1868
+ // Spezza dopo lo spazio: lo spazio resta in coda alla riga corrente,
1869
+ // la riga successiva parte dalla parola.
1870
+ end = brk + 1;
1871
+ }
1872
+ // Se nessuno spazio (brk<=start): parola piu' lunga di width -> hard-cut.
1873
+ }
1874
+ segs.push({ text: buffer.slice(start, end), start });
1875
+ start = end;
1876
+ }
1877
+ if (segs.length === 0)
1878
+ segs.push({ text: "", start: 0 });
1879
+ return segs;
1880
+ }
1881
+ function wordLeft(buffer, cursor) {
1882
+ let i = cursor;
1883
+ while (i > 0 && /\s/.test(buffer[i - 1] ?? ""))
1884
+ i -= 1;
1885
+ while (i > 0 && !/\s/.test(buffer[i - 1] ?? ""))
1886
+ i -= 1;
1887
+ return i;
1888
+ }
1889
+ function wordRight(buffer, cursor) {
1890
+ let i = cursor;
1891
+ const n = buffer.length;
1892
+ while (i < n && /\s/.test(buffer[i] ?? ""))
1893
+ i += 1;
1894
+ while (i < n && !/\s/.test(buffer[i] ?? ""))
1895
+ i += 1;
1896
+ return i;
1897
+ }
1898
+ function fmtElapsed(seconds) {
1899
+ if (seconds < 60)
1900
+ return `${seconds}s`;
1901
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
1902
+ }
1903
+ function isControl(s) {
1904
+ return s.length === 1 && s.charCodeAt(0) < 0x20;
1905
+ }
1906
+ function padEnd(value, width) {
1907
+ return value + " ".repeat(Math.max(0, width - visibleLength(value)));
1908
+ }
1909
+ function visibleLength(value) {
1910
+ return value.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "").length;
1911
+ }
1912
+ // Tronca alla larghezza VISIBILE preservando le sequenze ANSI (colori, OSC 8).
1913
+ // Sul troncamento chiude SGR e hyperlink OSC 8 aperti (anti-bleed).
1914
+ export function clipVisible(s, max) {
1915
+ if (max <= 0)
1916
+ return "";
1917
+ let out = "";
1918
+ let vis = 0;
1919
+ let i = 0;
1920
+ while (i < s.length) {
1921
+ if (s[i] === "\x1b") {
1922
+ const rest = s.slice(i);
1923
+ const sgr = /^\x1b\[[0-9;]*m/.exec(rest);
1924
+ const osc = /^\x1b\][\s\S]*?(?:\x07|\x1b\\)/.exec(rest);
1925
+ const csi = /^\x1b\[[0-9;?]*[A-Za-z]/.exec(rest);
1926
+ const seq = sgr?.[0] ?? osc?.[0] ?? csi?.[0];
1927
+ if (seq) {
1928
+ out += seq;
1929
+ i += seq.length;
1930
+ continue;
1931
+ }
1932
+ i += 1; // ESC isolato: scartalo
1933
+ continue;
1934
+ }
1935
+ if (vis >= max) {
1936
+ out += "\x1b[0m\x1b]8;;\x1b\\";
1937
+ return out;
1938
+ }
1939
+ out += s[i];
1940
+ vis += 1;
1941
+ i += 1;
1942
+ }
1943
+ return out;
1944
+ }
1945
+ function truncate(value, max) {
1946
+ if (max <= 0)
1947
+ return "";
1948
+ if (visibleLength(value) <= max)
1949
+ return value;
1950
+ return value.slice(0, max - 1) + "…";
1951
+ }
1952
+ // Sovrappone un suggerimento a destra di una riga (per "altro sopra/sotto").
1953
+ function scrollHint(line, hint, width) {
1954
+ const room = width - visibleLength(line) - hint.length - 2;
1955
+ if (room < 1)
1956
+ return line;
1957
+ return line + " ".repeat(room) + hint;
1958
+ }