talos-code 0.0.1 → 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 (401) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +111 -3
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/dist/archive/zip.js +73 -0
  5. package/dist/args.js +97 -0
  6. package/dist/automations/history-store.js +34 -0
  7. package/dist/automations/policy-store.js +43 -0
  8. package/dist/automations/runner.js +107 -0
  9. package/dist/automations/schedule.js +85 -0
  10. package/dist/automations/windows-task.js +53 -0
  11. package/dist/commands/advanced-cli.js +233 -0
  12. package/dist/commands/checkpoint-cli.js +45 -0
  13. package/dist/commands/config-cli.js +107 -0
  14. package/dist/commands/context.js +17 -0
  15. package/dist/commands/extensions-cli.js +207 -0
  16. package/dist/commands/project-cli.js +74 -0
  17. package/dist/commands/project-commands.js +143 -0
  18. package/dist/commands/provider-cli.js +185 -0
  19. package/dist/commands/services-cli.js +248 -0
  20. package/dist/commands/session-cli.js +171 -0
  21. package/dist/commands/system-cli.js +405 -0
  22. package/dist/config/commands.js +60 -0
  23. package/dist/config/load.js +382 -0
  24. package/dist/config/migrations.js +48 -0
  25. package/dist/config/types.js +11 -0
  26. package/dist/diagnostics/development-log.js +154 -0
  27. package/dist/diagnostics/doctor.js +104 -0
  28. package/dist/diagnostics/redact.js +80 -0
  29. package/dist/diagnostics/zip.js +52 -0
  30. package/dist/errors.js +156 -0
  31. package/dist/events/bridge.js +130 -0
  32. package/dist/extensions/installer.js +178 -0
  33. package/dist/extensions/package-schema.js +24 -0
  34. package/dist/headless/result.js +84 -0
  35. package/dist/headless/run.js +230 -0
  36. package/dist/i18n/en/approval.js +43 -0
  37. package/dist/i18n/en/common.js +9 -0
  38. package/dist/i18n/en/credentials.js +82 -0
  39. package/dist/i18n/en/errors.js +246 -0
  40. package/dist/i18n/en/firstrun.js +89 -0
  41. package/dist/i18n/en/providers.js +37 -0
  42. package/dist/i18n/en/screen.js +736 -0
  43. package/dist/i18n/en/tools.js +127 -0
  44. package/dist/i18n/en.js +14 -0
  45. package/dist/i18n/error-view.js +307 -0
  46. package/dist/i18n/index.js +19 -0
  47. package/dist/i18n/kernel-map.js +139 -0
  48. package/dist/io.js +45 -0
  49. package/dist/main.js +330 -0
  50. package/dist/paths.js +41 -0
  51. package/dist/protocol/v2/codec.js +9 -0
  52. package/dist/protocol/v2/events.js +555 -0
  53. package/dist/protocol/v2/index.js +4 -0
  54. package/dist/protocol/v2/replay.js +28 -0
  55. package/dist/protocol/v2/types.js +1 -0
  56. package/dist/provider/control-plane.js +135 -0
  57. package/dist/provider/environment-keys.js +275 -0
  58. package/dist/provider/health.js +110 -0
  59. package/dist/provider/missing-key.js +48 -0
  60. package/dist/provider/model-catalog.js +168 -0
  61. package/dist/provider/provider-text.js +13 -0
  62. package/dist/provider/store.js +95 -0
  63. package/dist/provider/system-keyring.js +214 -0
  64. package/dist/runtime/active-run.js +63 -0
  65. package/dist/runtime/agent-tree.js +74 -0
  66. package/dist/runtime/attachments.js +84 -0
  67. package/dist/runtime/brokered-executor.js +395 -0
  68. package/dist/runtime/context-status.js +76 -0
  69. package/dist/runtime/create-runtime.js +111 -0
  70. package/dist/runtime/model-profile.js +361 -0
  71. package/dist/runtime/output-store.js +137 -0
  72. package/dist/runtime/provider-attempts.js +185 -0
  73. package/dist/runtime/replay-buffer.js +153 -0
  74. package/dist/runtime/repo.js +95 -0
  75. package/dist/runtime/session-facade.js +135 -0
  76. package/dist/runtime/session-summary.js +125 -0
  77. package/dist/runtime/supervisor.js +262 -0
  78. package/dist/runtime/talos-composition.js +1006 -0
  79. package/dist/runtime/types.js +5 -0
  80. package/dist/runtime/usage-snapshot.js +82 -0
  81. package/dist/security/auto-classifier.js +38 -0
  82. package/dist/security/credential-free-environment.js +54 -0
  83. package/dist/security/evaluate.js +243 -0
  84. package/dist/security/execution-backends.js +236 -0
  85. package/dist/security/execution-broker.js +102 -0
  86. package/dist/security/forge-scan.js +74 -0
  87. package/dist/security/from-approval.js +39 -0
  88. package/dist/security/mxc-execution-backend.js +281 -0
  89. package/dist/security/permission-engine.js +138 -0
  90. package/dist/security/permission-explanation.js +54 -0
  91. package/dist/security/persist.js +179 -0
  92. package/dist/security/plugin-guard.js +230 -0
  93. package/dist/security/process-tree-evidence-store.js +74 -0
  94. package/dist/security/process-tree-probe.js +554 -0
  95. package/dist/security/project-resource-inventory.js +186 -0
  96. package/dist/security/project-trust-gate.js +38 -0
  97. package/dist/security/project-trust.js +367 -0
  98. package/dist/security/rule-parser.js +201 -0
  99. package/dist/security/shell-segmentation.js +68 -0
  100. package/dist/security/trust-authority.js +324 -0
  101. package/dist/security/types.js +1 -0
  102. package/dist/security/workspace-identity.js +102 -0
  103. package/dist/services/automation-facade.js +59 -0
  104. package/dist/services/forge-facade.js +77 -0
  105. package/dist/services/hook-facade.js +240 -0
  106. package/dist/services/index.js +16 -0
  107. package/dist/services/library-facade.js +174 -0
  108. package/dist/services/mcp-facade.js +348 -0
  109. package/dist/services/memory-facade.js +126 -0
  110. package/dist/services/notes-facade.js +157 -0
  111. package/dist/services/plugin-facade.js +308 -0
  112. package/dist/services/research-facade.js +110 -0
  113. package/dist/services/task-board-facade.js +236 -0
  114. package/dist/services/workflow-catalog.js +246 -0
  115. package/dist/sessions/export-format.js +98 -0
  116. package/dist/sessions/metadata-store.js +160 -0
  117. package/dist/sessions/share.js +122 -0
  118. package/dist/sessions/transfer.js +16 -0
  119. package/dist/subcommands.js +62 -0
  120. package/dist/tui/agent-roster.js +36 -0
  121. package/dist/tui/app.js +3761 -0
  122. package/dist/tui/approval.js +36 -0
  123. package/dist/tui/boot/boot-sequence.js +76 -0
  124. package/dist/tui/boot/cinematic.js +243 -0
  125. package/dist/tui/boot/desktop-logo.js +82 -0
  126. package/dist/tui/boot.js +3 -0
  127. package/dist/tui/busy-input.js +58 -0
  128. package/dist/tui/catalog-service.js +559 -0
  129. package/dist/tui/components/assistant-stream.js +1 -0
  130. package/dist/tui/components/command-menu.js +68 -0
  131. package/dist/tui/components/composer.js +219 -0
  132. package/dist/tui/components/diff.js +35 -0
  133. package/dist/tui/components/footer.js +48 -0
  134. package/dist/tui/components/header.js +6 -0
  135. package/dist/tui/components/markdown.js +305 -0
  136. package/dist/tui/components/status-indicator.js +32 -0
  137. package/dist/tui/components/terminal-shell.js +205 -0
  138. package/dist/tui/components/thinking-row.js +19 -0
  139. package/dist/tui/components/tool-row.js +97 -0
  140. package/dist/tui/components/transcript-virtualizer.js +165 -0
  141. package/dist/tui/components/transcript.js +43 -0
  142. package/dist/tui/diff-model.js +77 -0
  143. package/dist/tui/editor-history.js +21 -0
  144. package/dist/tui/editor.js +210 -0
  145. package/dist/tui/event-adapter.js +436 -0
  146. package/dist/tui/exit-output.js +58 -0
  147. package/dist/tui/external-editor.js +53 -0
  148. package/dist/tui/file-completion.js +89 -0
  149. package/dist/tui/focus-manager.js +5 -0
  150. package/dist/tui/highlight.js +30 -0
  151. package/dist/tui/input-router.js +18 -0
  152. package/dist/tui/interrupt.js +99 -0
  153. package/dist/tui/keybindings.js +194 -0
  154. package/dist/tui/keymap-resolver.js +91 -0
  155. package/dist/tui/launch-state.js +139 -0
  156. package/dist/tui/line-diff.js +57 -0
  157. package/dist/tui/live-activity.js +45 -0
  158. package/dist/tui/metrics.js +123 -0
  159. package/dist/tui/onboarding.js +36 -0
  160. package/dist/tui/overlays/agent-tree.js +39 -0
  161. package/dist/tui/overlays/approval-dialog.js +640 -0
  162. package/dist/tui/overlays/automation-center.js +51 -0
  163. package/dist/tui/overlays/checkpoint-picker.js +47 -0
  164. package/dist/tui/overlays/context-inspector.js +36 -0
  165. package/dist/tui/overlays/effort-line.js +110 -0
  166. package/dist/tui/overlays/forge-center.js +46 -0
  167. package/dist/tui/overlays/help-dialog.js +16 -0
  168. package/dist/tui/overlays/history-picker.js +37 -0
  169. package/dist/tui/overlays/hook-center.js +70 -0
  170. package/dist/tui/overlays/library-center.js +55 -0
  171. package/dist/tui/overlays/mcp-center.js +60 -0
  172. package/dist/tui/overlays/memory-center.js +63 -0
  173. package/dist/tui/overlays/model-picker.js +72 -0
  174. package/dist/tui/overlays/notes-center.js +53 -0
  175. package/dist/tui/overlays/overlay-host.js +8 -0
  176. package/dist/tui/overlays/plugin-center.js +76 -0
  177. package/dist/tui/overlays/provider-picker.js +145 -0
  178. package/dist/tui/overlays/queue-editor.js +32 -0
  179. package/dist/tui/overlays/research-center.js +59 -0
  180. package/dist/tui/overlays/scroll-window.js +234 -0
  181. package/dist/tui/overlays/session-picker.js +115 -0
  182. package/dist/tui/overlays/tasks-center.js +83 -0
  183. package/dist/tui/overlays/theme-picker.js +21 -0
  184. package/dist/tui/overlays/transcript-search.js +58 -0
  185. package/dist/tui/overlays/trust-center.js +29 -0
  186. package/dist/tui/overlays/workflow-center.js +46 -0
  187. package/dist/tui/project-file-index.js +214 -0
  188. package/dist/tui/project-references.js +85 -0
  189. package/dist/tui/project-trust-prompt.js +287 -0
  190. package/dist/tui/prompt-history-store.js +116 -0
  191. package/dist/tui/queue-store.js +110 -0
  192. package/dist/tui/regions/budget.js +59 -0
  193. package/dist/tui/regions/views.js +96 -0
  194. package/dist/tui/render-coordinator.js +148 -0
  195. package/dist/tui/render-scheduler.js +4 -0
  196. package/dist/tui/run.js +69 -0
  197. package/dist/tui/selection-list.js +29 -0
  198. package/dist/tui/session-controller.js +778 -0
  199. package/dist/tui/session-export.js +54 -0
  200. package/dist/tui/shell-input.js +35 -0
  201. package/dist/tui/shell-layout.js +48 -0
  202. package/dist/tui/shell-model.js +98 -0
  203. package/dist/tui/slash-commands.js +80 -0
  204. package/dist/tui/state.js +151 -0
  205. package/dist/tui/status-bar.js +125 -0
  206. package/dist/tui/status-view.js +40 -0
  207. package/dist/tui/terminal-capabilities.js +9 -0
  208. package/dist/tui/terminal-session.js +11 -0
  209. package/dist/tui/text-width.js +66 -0
  210. package/dist/tui/theme-catalog.js +25 -0
  211. package/dist/tui/theme-store.js +23 -0
  212. package/dist/tui/theme.js +23 -0
  213. package/dist/tui/tool-display.js +135 -0
  214. package/dist/tui/tool-facts.js +1 -0
  215. package/dist/tui/tools/bash-renderer.js +32 -0
  216. package/dist/tui/tools/change.js +61 -0
  217. package/dist/tui/tools/edit-renderer.js +17 -0
  218. package/dist/tui/tools/generic-renderer.js +13 -0
  219. package/dist/tui/tools/list-renderer.js +14 -0
  220. package/dist/tui/tools/read-renderer.js +13 -0
  221. package/dist/tui/tools/registry.js +20 -0
  222. package/dist/tui/tools/row-format.js +187 -0
  223. package/dist/tui/tools/search-renderer.js +38 -0
  224. package/dist/tui/tools/shared.js +97 -0
  225. package/dist/tui/tools/write-renderer.js +15 -0
  226. package/dist/tui/transcript-model.js +441 -0
  227. package/dist/tui/ui-preferences.js +79 -0
  228. package/dist/tui/usage-view.js +77 -0
  229. package/dist/tui/vim-mode.js +99 -0
  230. package/dist/update/check.js +15 -0
  231. package/dist/update/npm.js +51 -0
  232. package/dist/update/run.js +129 -0
  233. package/dist/version.js +2 -0
  234. package/dist/workspace/checkpoint-store.js +210 -0
  235. package/dist/workspace/checkpoint.js +413 -0
  236. package/dist/workspace/restore.js +232 -0
  237. package/package.json +63 -5
  238. package/vendor/context-engine/package.json +11 -0
  239. package/vendor/context-engine/src/compaction-planner.mjs +57 -0
  240. package/vendor/context-engine/src/contracts.mjs +48 -0
  241. package/vendor/context-engine/src/engine.mjs +445 -0
  242. package/vendor/context-engine/src/node/context-export.mjs +164 -0
  243. package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
  244. package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
  245. package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
  246. package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
  247. package/vendor/context-engine/src/profiles.mjs +10 -0
  248. package/vendor/context-engine/src/retrieval.mjs +104 -0
  249. package/vendor/context-engine/src/summary.mjs +97 -0
  250. package/vendor/context-engine/src/usage.mjs +34 -0
  251. package/vendor/harness-ui/package.json +38 -0
  252. package/vendor/harness-ui/src/acp-agent.mjs +350 -0
  253. package/vendor/harness-ui/src/agent-service.mjs +2188 -0
  254. package/vendor/harness-ui/src/agui-events.mjs +452 -0
  255. package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
  256. package/vendor/harness-ui/src/artifact-store.mjs +39 -0
  257. package/vendor/harness-ui/src/assistenza.mjs +123 -0
  258. package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
  259. package/vendor/harness-ui/src/automation-store.mjs +145 -0
  260. package/vendor/harness-ui/src/browser-annota.mjs +639 -0
  261. package/vendor/harness-ui/src/browser-frame.mjs +211 -0
  262. package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
  263. package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
  264. package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
  265. package/vendor/harness-ui/src/browser-stream.mjs +445 -0
  266. package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
  267. package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
  268. package/vendor/harness-ui/src/config.mjs +697 -0
  269. package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
  270. package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
  271. package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
  272. package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
  273. package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
  274. package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
  275. package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
  276. package/vendor/harness-ui/src/context-runtime.mjs +118 -0
  277. package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
  278. package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
  279. package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
  280. package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
  281. package/vendor/harness-ui/src/custom-task.mjs +171 -0
  282. package/vendor/harness-ui/src/doctor.mjs +142 -0
  283. package/vendor/harness-ui/src/document-filename.mjs +97 -0
  284. package/vendor/harness-ui/src/document-generator.mjs +493 -0
  285. package/vendor/harness-ui/src/document-report.mjs +331 -0
  286. package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
  287. package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
  288. package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
  289. package/vendor/harness-ui/src/forge-contract.mjs +221 -0
  290. package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
  291. package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
  292. package/vendor/harness-ui/src/generation-idle.mjs +332 -0
  293. package/vendor/harness-ui/src/gguf-header.mjs +207 -0
  294. package/vendor/harness-ui/src/git-service.mjs +626 -0
  295. package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
  296. package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
  297. package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
  298. package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
  299. package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
  300. package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
  301. package/vendor/harness-ui/src/hook-registry.mjs +186 -0
  302. package/vendor/harness-ui/src/http-app.mjs +6259 -0
  303. package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
  304. package/vendor/harness-ui/src/id-archivio.mjs +27 -0
  305. package/vendor/harness-ui/src/image-generator.mjs +143 -0
  306. package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
  307. package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
  308. package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
  309. package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
  310. package/vendor/harness-ui/src/library-store.mjs +652 -0
  311. package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
  312. package/vendor/harness-ui/src/local-model-store.mjs +339 -0
  313. package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
  314. package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
  315. package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
  316. package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
  317. package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
  318. package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
  319. package/vendor/harness-ui/src/mcp-client.mjs +98 -0
  320. package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
  321. package/vendor/harness-ui/src/mcp-session.mjs +177 -0
  322. package/vendor/harness-ui/src/memory-store.mjs +227 -0
  323. package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
  324. package/vendor/harness-ui/src/model-catalog.mjs +129 -0
  325. package/vendor/harness-ui/src/model-destination.mjs +189 -0
  326. package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
  327. package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
  328. package/vendor/harness-ui/src/notes-store.mjs +250 -0
  329. package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
  330. package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
  331. package/vendor/harness-ui/src/path-policy.mjs +442 -0
  332. package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
  333. package/vendor/harness-ui/src/plugin-session.mjs +180 -0
  334. package/vendor/harness-ui/src/process-policy.mjs +345 -0
  335. package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
  336. package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
  337. package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
  338. package/vendor/harness-ui/src/provider-probe.mjs +582 -0
  339. package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
  340. package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
  341. package/vendor/harness-ui/src/public-problem.mjs +109 -0
  342. package/vendor/harness-ui/src/research/card.mjs +235 -0
  343. package/vendor/harness-ui/src/research/citations.mjs +142 -0
  344. package/vendor/harness-ui/src/research/collector.mjs +275 -0
  345. package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
  346. package/vendor/harness-ui/src/research/dossier.mjs +114 -0
  347. package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
  348. package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
  349. package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
  350. package/vendor/harness-ui/src/research/independence.mjs +159 -0
  351. package/vendor/harness-ui/src/research/ledger.mjs +166 -0
  352. package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
  353. package/vendor/harness-ui/src/research/narration.mjs +181 -0
  354. package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
  355. package/vendor/harness-ui/src/research/opposing.mjs +305 -0
  356. package/vendor/harness-ui/src/research/outline.mjs +111 -0
  357. package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
  358. package/vendor/harness-ui/src/research/pdf.mjs +291 -0
  359. package/vendor/harness-ui/src/research/plan.mjs +301 -0
  360. package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
  361. package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
  362. package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
  363. package/vendor/harness-ui/src/research/recheck.mjs +194 -0
  364. package/vendor/harness-ui/src/research/report.mjs +203 -0
  365. package/vendor/harness-ui/src/research/run.mjs +527 -0
  366. package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
  367. package/vendor/harness-ui/src/research/verification.mjs +572 -0
  368. package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
  369. package/vendor/harness-ui/src/research-store.mjs +1133 -0
  370. package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
  371. package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
  372. package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
  373. package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
  374. package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
  375. package/vendor/harness-ui/src/search-source-store.mjs +172 -0
  376. package/vendor/harness-ui/src/session-registry.mjs +6095 -0
  377. package/vendor/harness-ui/src/session-store.mjs +220 -0
  378. package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
  379. package/vendor/harness-ui/src/setup-stato.mjs +31 -0
  380. package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
  381. package/vendor/harness-ui/src/skill-registry.mjs +120 -0
  382. package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
  383. package/vendor/harness-ui/src/static-files.mjs +96 -0
  384. package/vendor/harness-ui/src/stream-partition.mjs +123 -0
  385. package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
  386. package/vendor/harness-ui/src/task-catalog.mjs +65 -0
  387. package/vendor/harness-ui/src/tasks-store.mjs +220 -0
  388. package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
  389. package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
  390. package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
  391. package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
  392. package/vendor/harness-ui/src/usage-cache.mjs +315 -0
  393. package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
  394. package/vendor/harness-ui/src/workspace-context.mjs +124 -0
  395. package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
  396. package/vendor/harness-ui/src/workspace-files.mjs +589 -0
  397. package/vendor/harness-ui/src/workspace-info.mjs +189 -0
  398. package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
  399. package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
  400. package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
  401. package/vendor/manifest.json +170 -0
@@ -0,0 +1,640 @@
1
+ import { compileRules, PATH_BOUNDARY_RULE, patternNamesOnlyAVerb } from "../../security/evaluate.js";
2
+ import { explainPermission, simulatePermission } from "../../security/permission-explanation.js";
3
+ import { permissionActionToRule } from "../../security/persist.js";
4
+ import { splitGraphemes } from "../text-width.js";
5
+ import { sliceByHeights } from "./scroll-window.js";
6
+ import { t } from "../../i18n/index.js";
7
+ import { lineDiff } from "../line-diff.js";
8
+ import { isKernelTool, permissionToolFor, toolDisplay } from "../tool-display.js";
9
+ /*
10
+ * R3 gate E11, owner decisions Q-R3-5/6/14. Four choices, Deny highlighted by default, so a reflexive Enter never runs
11
+ * anything; ↑/↓ move the highlight, Enter confirms it, a digit picks at once. "Deny always" is offered in the full review
12
+ * only (or /permissions). The highlighted Deny denies and lets the turn continue; Esc denies and stops the turn from the
13
+ * summary, and goes back from the full review. Every choice row starts at the left edge with the highlight marker or a
14
+ * space and then `[digit]`, where model text (always indented by TEXT_INDENT) can never start, so a command cannot forge
15
+ * a choice.
16
+ */
17
+ export const APPROVAL_CHOICES = Object.freeze([
18
+ { action: 'approval-once', digit: '1', label: 'approval.choice.once' },
19
+ { action: 'approval-session', digit: '2', label: 'approval.choice.session' },
20
+ { action: 'approval-always', digit: '3', label: 'approval.choice.always' },
21
+ { action: 'approval-deny-once', digit: '4', label: 'approval.choice.deny' },
22
+ ]);
23
+ export const APPROVAL_DEFAULT_SELECTED = 3;
24
+ const SELECTED_MARKER = '›';
25
+ function choiceText(index, selected) { const row = APPROVAL_CHOICES[index]; return `${index === selected ? SELECTED_MARKER : ' '}[${row.digit}] ${t(row.label)}`; }
26
+ /*
27
+ * R3 approval key race, owner decision (binding): for APPROVAL_GRACE_MS after the dialog is first shown for a request,
28
+ * keys keep going to the composer (type-ahead is preserved, and a key typed in advance never answers the approval);
29
+ * after that the dialog takes every key, whatever the render has caught up with (app.ts reads a ref set synchronously).
30
+ */
31
+ export const APPROVAL_GRACE_MS = 300;
32
+ export function approvalInputGate(shown, now) {
33
+ if (!shown)
34
+ return 'none';
35
+ return now - shown.shownAt < APPROVAL_GRACE_MS ? 'grace' : 'dialog';
36
+ }
37
+ /* One key in the dialog → what it does. `approvalChoiceForAction` stays the one mapping from a key action to a choice. */
38
+ export function approvalKeyOutcome(action, { expanded, selected, escape }) {
39
+ if (!action)
40
+ return { kind: 'ignore' };
41
+ if (action === 'approval-view')
42
+ return { kind: 'toggle-review' };
43
+ if (action === 'approval-up')
44
+ return { kind: 'select', selected: Math.max(0, selected - 1) };
45
+ if (action === 'approval-down')
46
+ return { kind: 'select', selected: Math.min(APPROVAL_CHOICES.length - 1, selected + 1) };
47
+ if (action === 'approval-confirm') {
48
+ const row = APPROVAL_CHOICES[selected] ?? APPROVAL_CHOICES[APPROVAL_DEFAULT_SELECTED];
49
+ return { kind: 'answer', choice: approvalChoiceForAction(row.action), stop: false };
50
+ }
51
+ if (action === 'approval-cancel' && escape && expanded)
52
+ return { kind: 'back' };
53
+ if (action === 'approval-cancel' || action === 'interrupt')
54
+ return { kind: 'answer', choice: approvalChoiceForAction(action), stop: true };
55
+ if (action === 'approval-deny-always' && !expanded)
56
+ return { kind: 'ignore' };
57
+ const choice = approvalChoiceForAction(action);
58
+ return choice ? { kind: 'answer', choice, stop: false } : { kind: 'ignore' };
59
+ }
60
+ // The verdict a pending approval answers. The coordinator only surfaces an engine `ask`.
61
+ const VERDICT_BEING_ANSWERED = 'ask';
62
+ // Mirrors `bashRuleNarrowsNothing` in security/persist.ts, which is not exported. The test
63
+ // "the refusal shown in the preview is the persistence layer's own refusal" runs both against the
64
+ // same commands, so a drift between the two turns that test red instead of lying on screen.
65
+ function bashRuleNarrowsNothing(rule) { const match = /^Bash\((.*)\)$/u.exec(rule); if (!match)
66
+ return false; const pattern = match[1]; return pattern === '' || patternNamesOnlyAVerb(pattern); }
67
+ function errorText(error) { return error instanceof Error ? error.message : String(error); }
68
+ /*
69
+ * R2 (Q-R2-4, coordinator note to lane T): a refusal names its code ONCE. Lane E moves the code of the 8 `CODE: prose`
70
+ * sites from the message to `error.code`, so both shapes are read: the code from `error.code` or from the message's
71
+ * `CODE:` prefix, the sentence without that prefix. The code comes first, beside "refused", so it stays on the first row.
72
+ */
73
+ const CODE_SHAPE = /^[A-Z][A-Z0-9_]*$/u;
74
+ function errorCode(error) {
75
+ const own = error?.code;
76
+ if (typeof own === 'string' && CODE_SHAPE.test(own))
77
+ return own;
78
+ return /^([A-Z][A-Z0-9_]*)(?::|$)/u.exec(errorText(error))?.[1] ?? null;
79
+ }
80
+ export function refusalText(error) {
81
+ const code = errorCode(error);
82
+ let sentence = errorText(error).trim();
83
+ if (code && (sentence === code || sentence.startsWith(`${code}:`)))
84
+ sentence = sentence.slice(code.length + 1).trim();
85
+ return `would be refused${code ? ` (${code})` : ''}${sentence ? `: ${sentence}` : ''}`;
86
+ }
87
+ function simulateAllowAlways({ rules, action, projectRoot, persists }) {
88
+ if (!persists)
89
+ return { writes: null, refused: 'writes no rule: no configuration path is attached to this session' };
90
+ let rule;
91
+ try {
92
+ rule = permissionActionToRule(action, projectRoot);
93
+ }
94
+ catch (error) {
95
+ return { writes: null, refused: refusalText(error) };
96
+ }
97
+ if (action.tool === 'Bash' && bashRuleNarrowsNothing(rule))
98
+ return { writes: null, refused: `would be refused: ${rule} would narrow nothing` };
99
+ try {
100
+ return { writes: rule, simulation: simulatePermission({ rules: { ...rules, allow: [...rules.allow, rule] }, action, projectRoot }) };
101
+ }
102
+ // The rule is already printed on the line above: report the code, not the rule a second time.
103
+ catch (error) {
104
+ return { writes: rule, failure: errorCode(error) ?? errorText(error) };
105
+ }
106
+ }
107
+ // Computed ONCE, when the approval arrives, from the same rules the engine was built with. Never in
108
+ // render: the path boundary resolves real paths, and a render runs on every keystroke. And it never
109
+ // throws: a failure here must not stop the approval from reaching the person, it is reported instead.
110
+ // ⛔ The account and the simulation fail SEPARATELY. Round 1 wrapped both in one try, so a command
111
+ // with a quoted line break (whose persisted rule cannot be compiled) lost every segment row too.
112
+ export function explainApproval({ rules, action, projectRoot, persists }) {
113
+ let explanation;
114
+ try {
115
+ explanation = explainPermission(compileRules(rules), action, { projectRoot });
116
+ }
117
+ catch (error) {
118
+ return { explanationError: errorText(error) };
119
+ }
120
+ try {
121
+ return { explanation, alwaysSimulation: simulateAllowAlways({ rules, action, projectRoot, persists }) };
122
+ }
123
+ catch (error) {
124
+ return { explanation, alwaysSimulation: { writes: null, refused: `cannot be previewed: ${errorText(error)}` } };
125
+ }
126
+ }
127
+ function segmentRows(explanation) {
128
+ // A single command is exactly what the summary line already says: no row adds anything to it.
129
+ if (explanation.segments.length < 2)
130
+ return [];
131
+ return explanation.segments.map((segment, index) => {
132
+ const boundary = segment.rule === PATH_BOUNDARY_RULE;
133
+ const decidedBy = boundary ? 'path-boundary' : segment.rule ? 'rule' : segment.effect === 'ask' ? 'no-rule' : 'built-in';
134
+ return { position: index + 1, operatorBefore: segment.operatorBefore, command: segment.command, effect: segment.effect, decidedBy, ...(segment.rule && !boundary ? { rule: segment.rule } : {}), reason: segment.reason, decisive: index === explanation.decisiveSegment };
135
+ });
136
+ }
137
+ // The detail is shown only while it tells the same story as the verdict being answered: same
138
+ // effect, same rule, same decisive segment. Anything else is withheld, never shown beside it.
139
+ function accountAgrees(pending, explanation) {
140
+ if (explanation.effect !== VERDICT_BEING_ANSWERED)
141
+ return false;
142
+ if ((explanation.rule ?? null) !== (pending.rule ?? null))
143
+ return false;
144
+ const segment = /^segment (\d+): /u.exec(pending.reason);
145
+ if (segment && Number(segment[1]) - 1 !== explanation.decisiveSegment)
146
+ return false;
147
+ return true;
148
+ }
149
+ function alwaysPreview(pending, simulation) {
150
+ if (simulation.writes === null)
151
+ return { writes: null, refused: simulation.refused, decidedByNewRule: false, wildcard: false, compound: false, changedSegments: [] };
152
+ if ('failure' in simulation)
153
+ return { writes: simulation.writes, failure: simulation.failure, decidedByNewRule: false, wildcard: false, compound: false, changedSegments: [] };
154
+ const after = simulation.simulation.explanation;
155
+ // The persisted rule is an ALLOW rule. An ask or deny rule can carry the very same text, and
156
+ // matching on text alone would credit the new rule with a decision the old one made.
157
+ const byNewRule = (decision) => decision.rule === simulation.writes && decision.effect === 'allow';
158
+ const decidedByNewRule = byNewRule(after) || after.segments.some(byNewRule);
159
+ // In a Bash rule `*` is literal text unless it is the whole pattern or a trailing `:*`, and the
160
+ // persistence layer refuses both. Every other tool matches its rule as a glob.
161
+ const pattern = simulation.writes.slice(simulation.writes.indexOf('(') + 1, -1);
162
+ const wildcard = pending.action.tool !== 'Bash' && pattern.includes('*');
163
+ const before = pending.explanation?.segments ?? [];
164
+ const changedSegments = segmentRows(after).filter((row, index) => before[index]?.effect !== row.effect || before[index]?.rule !== after.segments[index]?.rule);
165
+ return { writes: simulation.writes, effect: simulation.simulation.effect, decidedByNewRule, wildcard, compound: after.segments.length > 1, changedSegments };
166
+ }
167
+ /*
168
+ * R2 lane T (Q-R2-3, gate E3): the title and summary name the tool by its on-screen label ("Permission: Shell"); rule text
169
+ * keeps its grammar (`Bash(…)`). The TITLE is drawn from a closed set only — a kernel tool's label from the table, and
170
+ * only when the table judges that tool as the action being answered, otherwise the permission tool's own label: a name
171
+ * chosen by a model, an MCP server or a plugin author never reaches the one fixed line that starts at the left edge.
172
+ * The summary is model text (indented, bounded) and may carry the humanised name of a tool the kernel did not declare.
173
+ */
174
+ function approvalLabel(action, name) {
175
+ if (isKernelTool(name) && permissionToolFor(name) === action.tool)
176
+ return toolDisplay(name).label;
177
+ return t(`tool.permission.${action.tool}`);
178
+ }
179
+ /*
180
+ * R3 gate E11 (§0.10): the full review is written for a person. It was `JSON.stringify` of the kernel payload, which put
181
+ * kernel keys and tool names on screen (`"tool": "scrivi"`, `"contenutoPrima": null`). Every field the kernel sends is
182
+ * named in English; the before/after texts become the diff below; a field this table does not know yet is still shown,
183
+ * under "Other details", so nothing the approval covers is hidden from the person who answers it.
184
+ */
185
+ const REVIEW_SKIPPED = new Set(['tool', 'tipo', 'toolCallId', 'displayName', 'contenutoPrima', 'contenutoProposto', 'trifecta', 'path', 'percorso', 'command', 'comando', 'format', 'formato', 'url', 'serverId', 'operation', 'sensitive', 'segreto']);
186
+ function field(payload, ...keys) { for (const key of keys) {
187
+ const value = payload[key];
188
+ if (typeof value === 'string' && value.length > 0)
189
+ return value;
190
+ } return undefined; }
191
+ function reviewText(pending, label) {
192
+ const payload = pending.rawPayload ?? {};
193
+ const lines = [`${t('approval.review.tool')}: ${label}`];
194
+ const add = (id, value) => { if (value !== undefined)
195
+ lines.push(`${t(id)}: ${value}`); };
196
+ add('approval.review.path', field(payload, 'path', 'percorso'));
197
+ add('approval.review.command', field(payload, 'command', 'comando'));
198
+ add('approval.review.format', field(payload, 'format', 'formato'));
199
+ add('approval.review.address', field(payload, 'url'));
200
+ add('approval.review.server', field(payload, 'serverId'));
201
+ add('approval.review.operation', field(payload, 'operation'));
202
+ const sensitive = payload.sensitive ?? payload.segreto;
203
+ if (sensitive && typeof sensitive === 'object') {
204
+ const where = field(sensitive, 'path', 'percorso');
205
+ lines.push(where ? t('approval.review.sensitive', { path: where }) : t('approval.review.sensitiveHidden'));
206
+ }
207
+ if (payload.trifecta === true || pending.runtimeHint === 'trifecta')
208
+ lines.push(t('approval.review.trifecta'));
209
+ const other = Object.entries(payload).filter(([key]) => !REVIEW_SKIPPED.has(key));
210
+ if (other.length) {
211
+ lines.push(`${t('approval.review.other')}:`);
212
+ for (const [key, value] of other)
213
+ lines.push(`${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+ /* Q-R3-4: a write or an edit carries the text before and the text proposed (talosHarness.mjs:8252-8256, 8449-8451). */
218
+ function approvalChange(pending) {
219
+ const payload = pending.rawPayload ?? {};
220
+ const proposed = payload.contenutoProposto;
221
+ if (typeof proposed !== 'string' || (pending.action.tool !== 'Write' && pending.action.tool !== 'Edit'))
222
+ return undefined;
223
+ const before = typeof payload.contenutoPrima === 'string' ? payload.contenutoPrima : null;
224
+ const diff = lineDiff(before, proposed, { path: field(payload, 'path', 'percorso') ?? 'file' });
225
+ const counts = before === null ? t('approval.change.newFile', { lines: diff.added }) : diff.added === 0 && diff.removed === 0 ? t('approval.change.none') : t('approval.change.counts', { added: diff.added, removed: diff.removed });
226
+ const rows = [...diff.model.plain, ...diff.model.files.flatMap(file => file.rows)].map(row => ({ kind: row.kind === 'add' || row.kind === 'remove' || row.kind === 'context' ? row.kind : 'meta', text: row.text }));
227
+ return { counts, rows };
228
+ }
229
+ export function approvalDialogModel(pending, input) {
230
+ const action = pending.action;
231
+ const name = pending.rawPayload?.tipo ?? pending.rawPayload?.tool;
232
+ const label = approvalLabel(action, name);
233
+ // A kernel tool's operation IS its raw name, which stays in the full review (Q-R2-3); another tool's is humanised.
234
+ const operation = typeof name === 'string' && action.operation === name ? (isKernelTool(name) ? undefined : toolDisplay(name).label) : action.operation;
235
+ const target = action.command ?? action.resource ?? [action.serverId, operation].filter(Boolean).join(':');
236
+ const summary = [label, target].filter(Boolean).join(' · ');
237
+ const fullPayload = input.expanded ? reviewText(pending, label) : summary;
238
+ const selected = Math.max(0, Math.min(APPROVAL_CHOICES.length - 1, Math.floor(input.selected ?? APPROVAL_DEFAULT_SELECTED)));
239
+ const questionId = `approval.question.${action.tool}`;
240
+ const question = t(questionId in QUESTION_IDS ? questionId : 'approval.question');
241
+ /* Q-R3-13 (R0 decision 4): a command approval on a host with no verified sandbox says so in its title. */
242
+ const title = t('tool.approval.title', { label }) + (input.unsandboxed === true && action.tool === 'Bash' ? ` ${t('approval.unsandboxed')}` : '');
243
+ const change = approvalChange(pending);
244
+ const ruleLine = pending.rule === PATH_BOUNDARY_RULE ? 'decided by the path boundary' : pending.rule ? `rule ${pending.rule}` : undefined;
245
+ const explanation = pending.explanation;
246
+ let segments = [];
247
+ let segmentsWithheld;
248
+ if (explanation) {
249
+ const rows = segmentRows(explanation);
250
+ if (rows.length && !accountAgrees(pending, explanation))
251
+ segmentsWithheld = 'explanation unavailable: the account disagreed with the decision (possible race)';
252
+ else
253
+ segments = rows;
254
+ }
255
+ return {
256
+ title, summary, fullPayload, expanded: input.expanded, reason: pending.reason, ...(pending.rule ? { rule: pending.rule } : {}), ...(pending.runtimeHint ? { runtimeHint: pending.runtimeHint } : {}),
257
+ ...(ruleLine ? { ruleLine } : {}), segments, ...(segmentsWithheld ? { segmentsWithheld } : {}), ...(pending.explanationError ? { explanationError: pending.explanationError } : {}),
258
+ ...(input.expanded && pending.alwaysSimulation ? { alwaysPreview: alwaysPreview(pending, pending.alwaysSimulation) } : {}),
259
+ question, selected, ...(change ? { change } : {}),
260
+ };
261
+ }
262
+ const QUESTION_IDS = { 'approval.question.Bash': true, 'approval.question.Write': true, 'approval.question.Edit': true, 'approval.question.Read': true, 'approval.question.WebFetch': true, 'approval.question.Mcp': true, 'approval.question.Hook': true, 'approval.question.Plugin': true, 'approval.question.TalosService': true };
263
+ /*
264
+ * Every character that moves the cursor, ends a line, drives the terminal or renders as nothing is
265
+ * shown as its code point instead of being written. Same convention as slice 14's human error line
266
+ * (`\uXXXX` over C0, DEL, C1 — U+0085 NEL and U+009B CSI included — U+2028 and U+2029), extended
267
+ * with the whole Unicode Bidi_Control set and every Default_Ignorable_Code_Point (zero-width
268
+ * characters, word joiner, tag characters, variation selectors): a dialog that asks for consent
269
+ * must not contain text the person cannot see. ESC is C0, so no SGR, OSC or CSI sequence survives.
270
+ * Nothing is dropped: a hidden character becomes a visible one.
271
+ */
272
+ const TERMINAL_UNSAFE = /[\x5c\x00-\x1f\x7f-\x9f\u2028\u2029\p{Bidi_Control}\p{Default_Ignorable_Code_Point}]/gu;
273
+ // Round 3: the backslash is escaped too. Without it, a real ESC made inert and a command that
274
+ // literally contains the six characters of that escape rendered byte-identically, which teaches a
275
+ // person to read escapes as noise. The full review is JSON, whose own escaping already doubles every
276
+ // backslash, so there the backslash is left exactly as JSON wrote it.
277
+ const BACKSLASH = String.fromCharCode(92);
278
+ export function inertText(value, { json = false } = {}) {
279
+ return value.replace(TERMINAL_UNSAFE, character => {
280
+ if (character === BACKSLASH)
281
+ return json ? BACKSLASH : BACKSLASH + BACKSLASH;
282
+ const code = character.codePointAt(0);
283
+ return code > 0xffff ? `${BACKSLASH}u{${code.toString(16)}}` : `${BACKSLASH}u${code.toString(16).padStart(4, '0')}`;
284
+ });
285
+ }
286
+ const EFFECT_WIDTH = 5;
287
+ const COMMAND_WIDTH_CAP = 32;
288
+ const COLUMN_GAP = 2;
289
+ // Free text derived from the model is indented by this much, so none of its lines, wrapped or not,
290
+ // can start at the left edge where a row starts.
291
+ const TEXT_INDENT = 2;
292
+ const PRECEDENCE_HEADER = 'Segments in execution order; the strictest decides (deny > ask > allow):';
293
+ // `summaryReason` is the engine's reason printed right above the rows. The decisive row already IS
294
+ // that sentence ("segment N: …"), so it names its decider without printing the sentence twice.
295
+ function deciderText(row, summaryReason) {
296
+ const said = row.decisive && summaryReason === `segment ${row.position}: ${row.reason}`;
297
+ const detail = (label) => said ? label : `${label}: ${row.reason}`;
298
+ if (row.decidedBy === 'path-boundary')
299
+ return detail('path boundary');
300
+ if (row.decidedBy === 'rule')
301
+ return row.reason === `matched ${row.effect} rule` ? `rule ${row.rule}` : detail(`rule ${row.rule}`);
302
+ if (row.decidedBy === 'built-in')
303
+ return detail('built-in');
304
+ return row.reason;
305
+ }
306
+ /*
307
+ * Round 3 — MODEL TEXT HAS A HEIGHT LIMIT.
308
+ *
309
+ * Measured by the adversarial review and reproduced through the real TuiApp at 80x24: a single-segment
310
+ * `curl … | sh` whose second argument was NBSP filler sized to the terminal height, followed by a
311
+ * forged dialog, pushed the real title off the visible screen and drew a whole forged
312
+ * `Permission: Bash / Segments… / >2 ask && npm test / [1] once…` block, at the very indent of the
313
+ * real reason and summary. Indentation cannot tell the two apart, and styling disappears with colour
314
+ * off, so the defence is HEIGHT: collapsed, every piece of model-derived text takes at most
315
+ * COLLAPSED_TEXT_LINES rendered lines, the last of which says how much was cut. The title, the
316
+ * precedence header, every row and the key line always render; `v` shows everything.
317
+ *
318
+ * The wrapping is done here and not by Ink, so the number of lines is known before rendering: each
319
+ * wrapped piece is its own Text with `truncate-end`, which can shorten a line but never add one.
320
+ */
321
+ // Width is measured CONSERVATIVELY: printable ASCII is one column, every other grapheme two. Ink
322
+ // measures with string-width, which this CLI does not depend on, and the TUI's own displayWidth reads
323
+ // 8,451 single code points narrower than Ink does (measured: U+2630 and up, keycap sequences, flags).
324
+ // Measuring too wide only makes a line shorter; measuring too narrow would let Ink cut the end of a
325
+ // line without the cut marker saying so.
326
+ const PRINTABLE_ASCII = /^[\x20-\x7e]$/u;
327
+ export function conservativeCells(text) { let total = 0; for (const grapheme of splitGraphemes(text))
328
+ total += PRINTABLE_ASCII.test(grapheme) ? 1 : 2; return total; }
329
+ const COLLAPSED_TEXT_LINES = 3;
330
+ const BORDER_WIDTH = 2;
331
+ const MIN_COMMAND_COLUMN = 8;
332
+ // Word wrap that loses nothing: the lines concatenate back to the text exactly. A word that does not
333
+ // fit the rest of the line moves to the next one; a word longer than a whole line fills the current
334
+ // line and is broken inside, as Ink's own wrap does, so no line is wasted before a hard break.
335
+ export function wrapToWidth(text, width) {
336
+ const limit = Math.max(1, width);
337
+ const lines = [];
338
+ let line = '';
339
+ let used = 0;
340
+ const breakInside = (token) => { for (const grapheme of splitGraphemes(token)) {
341
+ const cells = conservativeCells(grapheme);
342
+ if (line !== '' && used + cells > limit) {
343
+ lines.push(line);
344
+ line = '';
345
+ used = 0;
346
+ }
347
+ line += grapheme;
348
+ used += cells;
349
+ } };
350
+ for (const token of text.split(/( +)/u)) {
351
+ if (token === '')
352
+ continue;
353
+ const cells = conservativeCells(token);
354
+ if (used + cells <= limit) {
355
+ line += token;
356
+ used += cells;
357
+ continue;
358
+ }
359
+ if (token.startsWith(' ') || cells > limit) {
360
+ breakInside(token);
361
+ continue;
362
+ }
363
+ if (line !== '') {
364
+ lines.push(line);
365
+ line = '';
366
+ used = 0;
367
+ }
368
+ line = token;
369
+ used = cells;
370
+ }
371
+ if (line !== '' || !lines.length)
372
+ lines.push(line);
373
+ return lines;
374
+ }
375
+ function cutMarker(hiddenCharacters, width) {
376
+ const count = hiddenCharacters.toLocaleString('en-US');
377
+ for (const marker of [`… ${count} more characters — press v to review in full`, `… ${count} more — press v`, `… ${count} more`])
378
+ if (conservativeCells(marker) <= width)
379
+ return marker;
380
+ return '…';
381
+ }
382
+ // `limit` null means the full review: nothing is cut.
383
+ function boundedLines(text, width, limit) {
384
+ const lines = wrapToWidth(text, width);
385
+ if (limit === null || lines.length <= limit)
386
+ return lines;
387
+ const kept = lines.slice(0, limit - 1);
388
+ return [...kept, cutMarker([...lines.slice(limit - 1).join('')].length, width)];
389
+ }
390
+ function segmentLines(rows, summaryReason, tone, inner, limit) {
391
+ const commands = rows.map(row => inertText(row.operatorBefore && row.operatorBefore !== 'newline' ? `${row.operatorBefore} ${row.command}` : row.command));
392
+ const commandWidth = Math.min(COMMAND_WIDTH_CAP, Math.max(...commands.map(command => conservativeCells(command))));
393
+ // The position is padded on the right, never the left: the digit always sits right after the marker.
394
+ const digits = String(Math.max(...rows.map(row => row.position))).length;
395
+ const gutterWidth = 1 + digits + 1 + EFFECT_WIDTH + COLUMN_GAP;
396
+ // Explicit column widths, computed once for every row: left to flex layout, each row shrank its own
397
+ // columns by its own content and the rule column started somewhere else on every row.
398
+ const available = Math.max(0, inner - gutterWidth);
399
+ const commandColumn = Math.max(MIN_COMMAND_COLUMN, Math.min(commandWidth + COLUMN_GAP, Math.floor(available / 2)));
400
+ const deciderColumn = Math.max(1, available - commandColumn);
401
+ return rows.map((row, index) => {
402
+ const gutter = `${row.decisive ? '>' : ' '}${String(row.position).padEnd(digits)} ${row.effect.padEnd(EFFECT_WIDTH)}`;
403
+ const commandLines = boundedLines(commands[index], commandColumn - COLUMN_GAP, limit);
404
+ const deciderLines = boundedLines(inertText(deciderText(row, summaryReason)), deciderColumn, limit);
405
+ return { kind: 'row', tone, gutter, gutterWidth, commandLines, commandColumn, deciderLines, deciderColumn, text: `${gutter.padEnd(gutterWidth)}${commandLines.join('').padEnd(commandWidth + COLUMN_GAP)}${deciderLines.join('')}` };
406
+ });
407
+ }
408
+ function fixedLine(text, tone, indent) { return { kind: 'text', text: inertText(text), tone, indent, modelText: false }; }
409
+ function modelLines(text, tone, inner, limit, { json = false } = {}) {
410
+ return boundedLines(inertText(text, { json }), inner - TEXT_INDENT, limit).map(piece => ({ kind: 'text', text: piece, tone, indent: TEXT_INDENT, modelText: true }));
411
+ }
412
+ // The exact lines the TUI renders, in order, already made inert. The terminal only maps a tone to a
413
+ // style and a row to its columns.
414
+ export function approvalDialogLines(model, { columns = 80 } = {}) {
415
+ const inner = Math.max(1, columns - BORDER_WIDTH);
416
+ const limit = model.expanded ? null : COLLAPSED_TEXT_LINES;
417
+ const lines = [fixedLine(model.title, 'title', 0), ...modelLines(model.reason, 'plain', inner, limit)];
418
+ if (model.ruleLine)
419
+ lines.push(...modelLines(model.ruleLine, 'dim', inner, limit));
420
+ if (model.segments.length) {
421
+ lines.push(fixedLine(PRECEDENCE_HEADER, 'dim', 0));
422
+ lines.push(...segmentLines(model.segments, model.reason, 'plain', inner, limit));
423
+ }
424
+ if (model.segmentsWithheld)
425
+ lines.push(fixedLine(model.segmentsWithheld, 'dim', TEXT_INDENT));
426
+ if (model.explanationError)
427
+ lines.push(...modelLines(`per-segment account unavailable: ${model.explanationError}`, 'dim', inner, limit));
428
+ // R3 E11: the full review is one field per line (reviewText), and a field's own line breaks stay data: each field is made
429
+ // inert on its own, so a value can never open a line of its own. Collapsed, the payload IS the command.
430
+ if (model.expanded)
431
+ for (const reviewLine of model.fullPayload.split('\n'))
432
+ lines.push(...modelLines(reviewLine, 'plain', inner, limit));
433
+ else
434
+ lines.push(...modelLines(model.fullPayload, 'plain', inner, limit));
435
+ if (model.change)
436
+ lines.push(...changeLines(model.change, inner, model.expanded));
437
+ const preview = model.alwaysPreview;
438
+ if (preview) {
439
+ if (preview.writes === null)
440
+ lines.push(...modelLines(`[3] always ${preview.refused}`, 'dim', inner, limit));
441
+ else {
442
+ lines.push(...modelLines(`[3] always writes ${preview.writes}`, 'dim', inner, limit));
443
+ if (preview.failure !== undefined)
444
+ lines.push(...modelLines(` that rule cannot be evaluated: ${preview.failure}`, 'dim', inner, limit));
445
+ else {
446
+ if (preview.wildcard)
447
+ lines.push(fixedLine(' wildcard: this rule covers more than this request', 'dim', TEXT_INDENT));
448
+ const outcome = preview.decidedByNewRule ? `${preview.effect} by this rule` : `still ${preview.effect}: the rule ${preview.compound ? 'matches no segment' : 'does not decide this request'}`;
449
+ lines.push(fixedLine(` with that rule this command is: ${outcome}`, 'dim', TEXT_INDENT));
450
+ if (preview.changedSegments.length)
451
+ lines.push(...segmentLines(preview.changedSegments, '', 'dim', inner, limit));
452
+ }
453
+ }
454
+ }
455
+ lines.push(...controlLines(model));
456
+ return lines;
457
+ }
458
+ /* Q-R3-4: the diff of a write or an edit, model text (indented, inert). Collapsed it is bounded; `v` shows all of it. */
459
+ const CHANGE_SUMMARY_ROWS = 12;
460
+ function changeLines(change, inner, expanded) {
461
+ const out = [{ kind: 'text', text: inertText(change.counts), tone: 'dim', indent: TEXT_INDENT, modelText: true }];
462
+ const rows = expanded ? change.rows : change.rows.slice(0, CHANGE_SUMMARY_ROWS);
463
+ for (const row of rows) {
464
+ const prefix = row.kind === 'add' ? '+ ' : row.kind === 'remove' ? '- ' : row.kind === 'context' ? ' ' : '';
465
+ const tone = row.kind === 'add' ? 'add' : row.kind === 'remove' ? 'remove' : 'dim';
466
+ for (const piece of wrapToWidth(inertText(prefix + row.text), Math.max(1, inner - TEXT_INDENT)))
467
+ out.push({ kind: 'text', text: piece, tone, indent: TEXT_INDENT, modelText: true });
468
+ }
469
+ if (!expanded && change.rows.length > CHANGE_SUMMARY_ROWS)
470
+ out.push({ kind: 'text', text: t('approval.change.more', { count: change.rows.length - CHANGE_SUMMARY_ROWS }), tone: 'dim', indent: TEXT_INDENT, modelText: true });
471
+ return out;
472
+ }
473
+ /* The question, the four choices (the highlighted one marked) and the keys. Fixed text: every row starts at the left edge. */
474
+ function controlLines(model) {
475
+ return [
476
+ fixedLine(model.question, 'plain', 0),
477
+ ...APPROVAL_CHOICES.map((_, index) => fixedLine(choiceText(index, model.selected), index === model.selected ? 'selected' : 'plain', 0)),
478
+ fixedLine(t(model.expanded ? 'approval.hint.review' : 'approval.hint.summary'), 'dim', 0),
479
+ ];
480
+ }
481
+ /* When the height is short, the same choices on as few rows as the width allows; all of them, and the deny path, stay.
482
+ Items are packed whole (a choice is never split across two rows unless it is wider than a row); the question goes
483
+ first when there is room for it. */
484
+ function compactControlLines(model, inner, { question }) {
485
+ const items = [...(question ? [model.question] : []), ...APPROVAL_CHOICES.map((_, index) => choiceText(index, model.selected).trimStart()), model.expanded ? `[5] ${t('approval.choice.denyAlways')}` : t('approval.hint.compact')].map(item => inertText(item));
486
+ const width = Math.max(1, inner), rows = [];
487
+ let row = '';
488
+ for (const item of items) {
489
+ const joined = row ? `${row} ${item}` : item;
490
+ if (conservativeCells(joined) <= width) {
491
+ row = joined;
492
+ continue;
493
+ }
494
+ if (row)
495
+ rows.push(row);
496
+ if (conservativeCells(item) <= width) {
497
+ row = item;
498
+ continue;
499
+ }
500
+ const pieces = wrapToWidth(item, width);
501
+ rows.push(...pieces.slice(0, -1));
502
+ row = pieces.at(-1) ?? '';
503
+ }
504
+ if (row)
505
+ rows.push(row);
506
+ return rows.map(text => ({ kind: 'text', text, tone: 'plain', indent: 0, modelText: false }));
507
+ }
508
+ const CONTROL_ROWS = APPROVAL_CHOICES.length + 2;
509
+ function approvalLineHeight(line) { return line.kind === 'row' ? Math.max(1, line.commandLines.length, line.deciderLines.length) : 1; }
510
+ function clippedApprovalLine(line, height) {
511
+ if (line.kind === 'text' || approvalLineHeight(line) <= height)
512
+ return line;
513
+ return { ...line, commandLines: line.commandLines.slice(0, height), deciderLines: line.deciderLines.slice(0, height) };
514
+ }
515
+ function oneModelLine(text, tone, inner) {
516
+ return modelLines(text, tone, inner, 1)[0] ?? fixedLine('', tone, TEXT_INDENT);
517
+ }
518
+ export function approvalDialogViewport(model, { columns = 80, rows = 12, scrollOffset = 0 } = {}) {
519
+ const all = approvalDialogLines(model, { columns });
520
+ const budget = Math.max(6, Math.floor(rows));
521
+ const total = all.reduce((sum, line) => sum + approvalLineHeight(line), 0);
522
+ if (total <= budget)
523
+ return { lines: all, offset: 0, maxOffset: 0 };
524
+ const inner = Math.max(1, columns - BORDER_WIDTH);
525
+ const decisive = model.segments.find(row => row.decisive);
526
+ const operation = decisive ? `${decisive.effect}: ${decisive.command}` : model.summary;
527
+ const firstPayloadField = model.expanded ? model.fullPayload.split('\n').find(line => { const value = line.trim(); return value !== '' && value !== '{' && value !== '}'; }) : undefined;
528
+ const header = [fixedLine(model.title, 'title', 0), oneModelLine(model.reason, 'plain', inner), oneModelLine(operation, 'plain', inner), ...(firstPayloadField ? [oneModelLine(firstPayloadField, 'dim', inner)] : [])];
529
+ const controls = compactControlLines(model, inner, { question: false });
530
+ const body = all.slice(1, all.length - CONTROL_ROWS);
531
+ const reserved = header.length + controls.length + (model.expanded ? 1 : 0);
532
+ /* R3 E11: the header already carries the reason and the operation, so a short view may show no body line at all. */
533
+ const bodyBudget = Math.max(0, budget - reserved);
534
+ let used = 0, maxOffset = body.length;
535
+ for (let index = body.length - 1; index >= 0; index -= 1) {
536
+ const height = approvalLineHeight(body[index]);
537
+ if (used + height > bodyBudget)
538
+ break;
539
+ used += height;
540
+ maxOffset = index;
541
+ }
542
+ const offset = Math.max(0, Math.min(maxOffset, Math.floor(Number.isFinite(scrollOffset) ? scrollOffset : 0)));
543
+ const visible = [];
544
+ used = 0;
545
+ for (let index = offset; index < body.length; index += 1) {
546
+ const line = body[index], height = approvalLineHeight(line);
547
+ if (used + height > bodyBudget) {
548
+ if (visible.length === 0 && bodyBudget > 0)
549
+ visible.push(clippedApprovalLine(line, bodyBudget));
550
+ break;
551
+ }
552
+ visible.push(line);
553
+ used += height;
554
+ }
555
+ const scroll = model.expanded ? fixedLine(`Review ${offset + 1}/${maxOffset + 1} · PgUp/PgDn scroll`, 'dim', 0) : null;
556
+ return { lines: [...header, ...(scroll ? [scroll] : []), ...visible, ...controls], offset, maxOffset };
557
+ }
558
+ function wrappedFixed(text, tone, width, indent = 0) {
559
+ return wrapToWidth(inertText(text), Math.max(1, width - indent)).map(piece => ({ kind: 'text', text: piece, tone, indent, modelText: false }));
560
+ }
561
+ // One row, cut with `…` when longer: a header row only names the operation; the preview below is where it is read.
562
+ function oneRow(text, tone, width) {
563
+ const limit = Math.max(1, width - TEXT_INDENT), pieces = wrapToWidth(inertText(text), limit);
564
+ let row = pieces[0] ?? '';
565
+ if (pieces.length > 1) {
566
+ const graphemes = splitGraphemes(row);
567
+ while (graphemes.length && conservativeCells(graphemes.join('') + '…') > limit)
568
+ graphemes.pop();
569
+ row = graphemes.join('') + '…';
570
+ }
571
+ return { kind: 'text', text: row, tone, indent: TEXT_INDENT, modelText: true };
572
+ }
573
+ export function approvalDialogFit(model, { columns = 80, maxRows, scrollOffset = 0 } = {}) {
574
+ const border = maxRows === undefined || maxRows >= 4;
575
+ const inner = Math.max(1, columns - (border ? BORDER_WIDTH : 0));
576
+ const budget = maxRows === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(maxRows) - (border ? 2 : 0));
577
+ const all = approvalDialogLines(model, { columns: inner + BORDER_WIDTH });
578
+ const exact = (line) => line.kind === 'text' && !line.modelText ? wrappedFixed(line.text, line.tone, inner, line.indent) : [line];
579
+ const title = exact(all[0]).slice(0, 1);
580
+ const body = all.slice(1, all.length - CONTROL_ROWS).flatMap(exact);
581
+ /* R3 E11: the question and the four choices on their own rows while at least the title and the operation fit above
582
+ them; below that, the same choices on as few rows as the width allows. Every choice and the deny path stay. */
583
+ const fullControls = all.slice(all.length - CONTROL_ROWS).flatMap(exact);
584
+ const withQuestion = compactControlLines(model, inner, { question: true });
585
+ const controls = budget - fullControls.length >= 2 ? fullControls : budget - withQuestion.length >= 1 ? withQuestion : compactControlLines(model, inner, { question: false });
586
+ const heights = body.map(approvalLineHeight), bodyRows = heights.reduce((sum, value) => sum + value, 0);
587
+ const total = title.length + bodyRows + controls.length;
588
+ const compact = controls !== fullControls;
589
+ if (total <= budget)
590
+ return { lines: [...title, ...body, ...controls], rows: total, offset: 0, maxOffset: 0, below: 0, border, compact };
591
+ let left = budget;
592
+ const keep = (lines) => { const taken = lines.slice(0, Math.max(0, left)); left -= taken.length; return taken; };
593
+ const shownControls = keep(controls), shownTitle = keep(title);
594
+ const decisive = model.segments.find(row => row.decisive);
595
+ const operation = keep([oneRow(decisive ? `${decisive.effect}: ${decisive.command}` : model.summary, 'plain', inner)]);
596
+ let marker = left >= 1 ? 1 : 0;
597
+ left -= marker;
598
+ const reason = left >= 3 ? keep([oneRow(model.reason, 'plain', inner)]) : [];
599
+ const scroll = model.expanded && left >= 3 ? 1 : 0;
600
+ left -= scroll;
601
+ const view = sliceByHeights(heights, { budget: left + marker, offset: scrollOffset });
602
+ // The whole preview fits once the marker row is given back to it: nothing is cut, nothing is announced.
603
+ if (view.above === 0 && view.below === 0 && view.clip === null) {
604
+ left += marker;
605
+ marker = 0;
606
+ }
607
+ const visible = [];
608
+ for (let index = view.start; index < view.end && left > 0; index++) {
609
+ const line = body[index], height = approvalLineHeight(line);
610
+ visible.push(height <= left ? line : clippedApprovalLine(line, left));
611
+ left -= Math.min(height, left);
612
+ }
613
+ const below = heights.slice(view.start + visible.length).reduce((sum, value) => sum + value, 0) + (visible.length ? approvalLineHeight(body[view.start + visible.length - 1]) - approvalLineHeight(visible.at(-1)) : 0);
614
+ const hint = model.expanded ? 'PgDn scrolls' : 'press v to review in full';
615
+ const more = marker && below > 0 ? [fixedLine(`… ${below} more line${below === 1 ? '' : 's'} · ${hint}`, 'dim', 0)] : [];
616
+ const review = scroll ? [fixedLine(`Review ${view.start + 1}/${view.maxOffset + 1} · PgUp/PgDn scroll`, 'dim', 0)] : [];
617
+ const lines = [...shownTitle, ...reason, ...operation, ...review, ...visible, ...more, ...shownControls].map(line => line.kind === 'text' && !line.modelText ? { ...line, text: wrapToWidth(line.text, Math.max(1, inner - line.indent))[0] ?? '' } : line);
618
+ return { lines, rows: lines.reduce((sum, line) => sum + approvalLineHeight(line), 0), offset: view.start, maxOffset: view.maxOffset, below: marker ? below : 0, border, compact };
619
+ }
620
+ // The dialog as Ink elements. Model text arrives already wrapped: one Text per terminal line, cut
621
+ // rather than wrapped by Ink, so its height is exactly what the viewport decided. With `maxRows` every
622
+ // row is pre-wrapped (approvalDialogFit), so every text row is cut, never wrapped.
623
+ export function renderApprovalDialog(h, Ink, model, { accentColor, columns = 80, rows, maxRows, scrollOffset = 0 } = {}) {
624
+ const fitted = maxRows === undefined ? null : approvalDialogFit(model, { columns, maxRows, scrollOffset });
625
+ const lines = fitted ? fitted.lines : rows === undefined ? approvalDialogLines(model, { columns }) : approvalDialogViewport(model, { columns, rows, scrollOffset }).lines;
626
+ return h(Ink.Box, fitted && !fitted.border ? { flexDirection: 'column', flexShrink: 0 } : { borderStyle: 'round', flexDirection: 'column', ...(fitted ? { flexShrink: 0 } : {}) }, ...lines.map((line, index) => {
627
+ const style = line.tone === 'title' ? { bold: true, color: accentColor } : line.tone === 'dim' ? { dimColor: true } : line.tone === 'selected' ? { bold: true, color: accentColor } : line.tone === 'add' ? { color: 'green' } : line.tone === 'remove' ? { color: 'red' } : {};
628
+ const piece = (text, key) => h(Ink.Text, { ...style, key, wrap: 'truncate-end' }, text);
629
+ if (line.kind === 'text')
630
+ return h(Ink.Box, { key: index, paddingLeft: line.indent }, line.modelText || fitted ? piece(line.text, 0) : h(Ink.Text, style, line.text));
631
+ return h(Ink.Box, { key: index, flexDirection: 'row' }, h(Ink.Box, { width: line.gutterWidth, flexShrink: 0 }, h(Ink.Text, style, line.gutter)),
632
+ // The gap is padding, not width: a hard-wrapped command must never touch the rule beside it.
633
+ h(Ink.Box, { width: line.commandColumn, paddingRight: COLUMN_GAP, flexShrink: 0, flexDirection: 'column' }, ...line.commandLines.map(piece)), h(Ink.Box, { width: line.deciderColumn, flexShrink: 0, flexDirection: 'column' }, ...line.deciderLines.map(piece)));
634
+ }));
635
+ }
636
+ const APPROVAL_ACTIONS = {
637
+ 'approval-once': 'allow-once', 'approval-session': 'allow-session', 'approval-always': 'allow-always',
638
+ 'approval-deny-once': 'deny-once', 'approval-deny-always': 'deny-always', 'approval-cancel': 'deny-once', 'interrupt': 'deny-once',
639
+ };
640
+ export function approvalChoiceForAction(action) { return APPROVAL_ACTIONS[action]; }