quoxcli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,677 @@
1
+ /**
2
+ * QUOXON conversation state reducer. Consumes BOTH the legacy /chat/stream
3
+ * events (connected, thinking, chunk, done, error) and the v1 contract
4
+ * (docs/QUOXON_EVENTS_v1.md): turn.*, text.delta, tool.*, approval.*, trace,
5
+ * steer.accepted. Pure: no I/O, no clocks (time comes in on actions).
6
+ *
7
+ * Shape
8
+ * history : settled items, rendered once via <Static>
9
+ * blocks : the live turn's ordered blocks; a settled PREFIX is flushed to
10
+ * history (text followed by something, tools that finished)
11
+ * turn : null | { id, status: running|interrupting, startedAt, stage, activity }
12
+ */
13
+
14
+ import { dividerStep } from './dividerAocl.js';
15
+ import { approvalFacts } from './approvalFacts.js';
16
+ import { isConsentError, consentErrorText } from './consent.js';
17
+ import { splitSettled } from './markdownSettle.js'; // not markdown.js: that loads React + Ink (QUOXON-LATENCY)
18
+ import { nextActivity, actionActivity } from './activity.js';
19
+ import { normUsage, turnTokens, fmtTokens, fmtCost } from './indicator.js';
20
+ import { isLayerTrace, normaliseSummary, summaryFromTraces, verdictNote } from './aocl.js';
21
+ import { toolResultText } from './toolError.js';
22
+
23
+ export const TOOL_STATUSES = ['ok', 'failed', 'denied', 'effect_unknown'];
24
+ export const APPROVAL_ARM_MS = 300;
25
+ export const KEY_REPEAT_MS = 400;
26
+ export const HELD_KEY_MS = 1500;
27
+
28
+ export const initialState = (over = {}) => ({
29
+ history: [],
30
+ blocks: [],
31
+ asks: [], // ASK-SETTLE: open y/N/c ask cards (live, never scrollback) until answered/expired/closed
32
+ turn: null,
33
+ ctx: { agent: null, model: null, provider: null, endpoint: null, hat: null, conversationId: null },
34
+ pending: [], // client-side queue (Tab)
35
+ steering: [], // steer messages not yet accepted
36
+ lastTurnId: null, // the newest turn begun here: a card from an older turn is stale
37
+ lastAocl: null, // QUOXON-AOCL: the last turn's normalised aocl.summary (for /aocl)
38
+ waiting: [], // approvals raised by ANOTHER conversation/session: a notice only, never a focused card (answered via /approvals)
39
+ approvals: [], // approval cards waiting for a human, first is shown; survive the turn that raised them
40
+ running: [], // approved from this terminal, result arrives asynchronously (polled)
41
+ inbox: [], // last /approvals listing
42
+ verbose: 'on',
43
+ doneCalls: {},
44
+ background: 0,
45
+ notify: { approvals: null, error: null },
46
+ seenInbox: {}, // inbox item ids already shown as a card (event or poll), so a poll never re-raises one
47
+ autoDrain: false,
48
+ seq: 0,
49
+ ...over
50
+ });
51
+
52
+ /**
53
+ * The server mirrors every legacy `chunk` as a `text.delta` (chunk first). Per turn, text comes from
54
+ * text.delta once the stream is known to be v1 (turn.started or any text.delta); legacy-only
55
+ * streams keep using `chunk`. Returns { append, st } where st is { v1, lastChunk }.
56
+ */
57
+ export function textGate(st, ev) {
58
+ const g = st || { v1: false, lastChunk: null, acc: '' };
59
+ const acc = g.acc || '';
60
+ const add = (c) => (acc + c).slice(-200000);
61
+ if (ev.type === 'turn.started') return { append: false, st: { ...g, v1: true } };
62
+ if (ev.type === 'chunk') {
63
+ const c = ev.content;
64
+ if (!c) return { append: false, st: g };
65
+ // RESTORE-TUI: on a v1 stream a chunk is skipped only when it mirrors text already shown (the full text so far,
66
+ // or the delta it repeats). A chunk carrying NEW text is still answer text (it was dropped since dae720283).
67
+ if (g.v1 && acc.endsWith(c)) return { append: false, st: g };
68
+ return { append: true, st: { ...g, lastChunk: c, acc: add(c) } };
69
+ }
70
+ if (ev.type === 'text.delta') {
71
+ const c = ev.content;
72
+ const mirrored = g.lastChunk != null && g.lastChunk === c;
73
+ const append = Boolean(c) && !mirrored;
74
+ return { append, st: { v1: true, lastChunk: null, acc: append ? add(c) : acc } };
75
+ }
76
+ return { append: false, st: g };
77
+ }
78
+
79
+ const tms = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return Number.isFinite(t) ? t : null; };
80
+
81
+ const short = (v, n = 120) => { const t = typeof v === 'string' ? v : (v == null ? '' : JSON.stringify(v)); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
82
+ const argText = (a) => a && typeof a === 'object' && !Array.isArray(a) && (a.argv || a.command) ? argText(a.argv ?? a.command) : Array.isArray(a) ? a.join(' ') : (a && typeof a === 'object' ? Object.entries(a).filter(([, v]) => v != null && typeof v !== 'object').slice(0, 4).map(([k, v]) => `${k}=${v}`).join(' ') : (a == null ? '' : String(a)));
83
+
84
+ /**
85
+ * Describe ANY inbox approval from what the item carries: the tool or leaf, the command or key args, the target
86
+ * (explicit target, else the org it acts on), and who asked and when. Never invents; empty fields are null.
87
+ */
88
+ export function describeInboxItem(it) {
89
+ const p = it?.payload || {};
90
+ const c = p.approval_context || {};
91
+ const m = c.method || {};
92
+ const ob = c.on_behalf_of || {};
93
+ const w = p.what && typeof p.what === 'object' ? p.what : {}; // APPROVAL-WHAT: the server's own action/target/summary wins
94
+ const tool = w.action || m.tool || c.tool || c.leaf || p.tool || p.leaf || null;
95
+ const argv = m.argv ?? m.args ?? c.argv ?? c.args ?? p.argv ?? p.args ?? m.command ?? c.command ?? p.command ?? null;
96
+ const cmd = short([m.leaf || c.leaf || p.leaf, argText(argv)].filter(Boolean).join(' ').trim() || null);
97
+ const target = w.target || m.target || c.target || p.target || cmd || w.target_none || c.target_org || c.org_name || p.org_name || c.org_id || p.org_id || null;
98
+ const by = ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null;
99
+ const what = w.summary ? String(w.summary) : (it?.title ? short(String(it.title), 160) : null);
100
+ return { tool, target: target ? short(target, 200) : null, title: what, by: by ? String(by) : null, at: it?.created_at ?? it?.createdAt ?? null };
101
+ }
102
+
103
+ /**
104
+ * A pending inbox approval found by polling -> approval card, or null when it does not qualify.
105
+ * Qualifies when created after `since` (the turn start, else app start), or raised on behalf of this
106
+ * user, or in this conversation. The inbox list is already scoped to the signed-in user and org.
107
+ */
108
+ export function cardFromInboxItem(it, { since = 0, userId = null, conversationId = null, now = 0 } = {}) {
109
+ if (!it || !it.id) return null;
110
+ if (it.payload?.type === 'decision') return null; // quox ask: answered with /answer N key from /approvals, never a y/n key card
111
+ const c = it.payload?.approval_context || {};
112
+ const m = c.method || {};
113
+ const created = tms(it.created_at ?? it.createdAt);
114
+ const mine = Boolean(userId && c.on_behalf_of?.user_id && String(c.on_behalf_of.user_id) === String(userId));
115
+ const here = Boolean(conversationId && c.on_behalf_of?.conversation_id === conversationId);
116
+ if (!(created != null && created >= since) && !mine && !here) return null;
117
+ // QUOXON-REVIEW: collector tool approvals mint the inbox item with aee_correlation_id = the approval id;
118
+ // without it the card had no id, polling was skipped and every terminal approve read "timeout".
119
+ const toolApproval = it.payload?.type === 'native_tool_approval';
120
+ const approvalId = it.payload?.approval_id || it.approval_id || it.approvalId
121
+ || (toolApproval ? (it.aee_correlation_id || it.aeeCorrelationId || null) : null) || null;
122
+ const ob = c.on_behalf_of || {};
123
+ const d = describeInboxItem(it);
124
+ const tool = d.tool || String(it.title || it.id);
125
+ return {
126
+ callId: `inbox:${it.id}`, inboxItemId: it.id, approvalId,
127
+ tool, target: d.target, what: d.title, askedBy: d.by, askedAt: d.at, scope: null, risk: c.risk || null,
128
+ url: it.dashboard_url || null, shownAt: now, undescribable: !d.tool && !d.target,
129
+ facts: approvalFacts(it), // APPROVAL-WWWH: the exact command, hosts, chain, reason, time and read-only class
130
+ // only an item that names THIS conversation is ours; anything else came from another session and must not take the keyboard
131
+ foreign: !here,
132
+ origin: String(ob.agent || ob.agent_id || ob.session_name || ob.session || (ob.conversation_id ? `conversation ${String(ob.conversation_id).slice(0, 8)}` : approvalSourceLabel(it))),
133
+ privileged: isPrivileged({ tool, risk: c.risk, flag: c.privileged === true || c.elevated === true || it.payload?.privileged === true, title: it.title })
134
+ };
135
+ }
136
+
137
+ /** Where an inbox approval came from: its source label, else the payload source, else its type. */
138
+ export function approvalSourceLabel(it) {
139
+ const c = it?.payload?.approval_context || {};
140
+ return String(c.source_label || it?.payload?.source || it?.source || (c.requested_by && c.requested_by.agent_label) || it?.payload?.type || 'plugin or workflow');
141
+ }
142
+
143
+ /**
144
+ * QUOXON-FW7: every open inbox approval, grouped for /approvals. 'this conversation' (names this conversation,
145
+ * or already a card here), 'other sessions' (raised on behalf of another conversation or agent), and
146
+ * 'plugins and workflows' (everything else, labelled by source). Empty groups are dropped.
147
+ */
148
+ export function groupApprovals(items, { conversationId = null, cardIds = [] } = {}) {
149
+ const g = { 'decisions asked of you': [], 'this conversation': [], 'other sessions': [], 'plugins and workflows': [] };
150
+ for (const item of items || []) {
151
+ if (item?.payload?.type === 'decision') { g['decisions asked of you'].push({ item, label: String(item.payload.session || 'session') }); continue; } // quox ask
152
+ const ob = item?.payload?.approval_context?.on_behalf_of || {};
153
+ const label = approvalSourceLabel(item);
154
+ // A card that names a conversation belongs here only when it is THIS one (a card shown in this window for another
155
+ // conversation, e.g. a privileged hat.switch, is still "other sessions"). No conversation named: a card here counts.
156
+ const mine = ob.conversation_id ? Boolean(conversationId) && ob.conversation_id === conversationId : cardIds.includes(item.id);
157
+ if (mine) g['this conversation'].push({ item, label });
158
+ else if (ob.conversation_id || ob.agent || ob.agent_id || ob.session_name || ob.session) g['other sessions'].push({ item, label: String(ob.session_name || ob.session || ob.agent || ob.agent_id || `conversation ${String(ob.conversation_id).slice(0, 8)}`) });
159
+ else g['plugins and workflows'].push({ item, label });
160
+ }
161
+ return Object.entries(g).filter(([, v]) => v.length).map(([group, v]) => ({ group, items: v }));
162
+ }
163
+
164
+ /** Privileged/elevated grants (hat switches and anything the server marks privileged or elevated) need an explicit confirm. */
165
+ export function isPrivileged({ tool = '', risk = null, flag = false, title = '' } = {}) {
166
+ if (flag === true) return true;
167
+ if (String(tool) === 'hat.switch' || /^hat[ .]switch/i.test(String(title))) return true;
168
+ return /privileged|elevated|critical/i.test(String(risk || ''));
169
+ }
170
+
171
+ let uid = 0;
172
+ const nid = () => ++uid;
173
+
174
+ const settled = (b) => b.kind === 'tool' ? b.status !== 'running' : b.kind !== 'text';
175
+
176
+ /** Move the settled prefix of live blocks into history. */
177
+ function flush(s, force = false) {
178
+ const blocks = [...s.blocks];
179
+ const out = [];
180
+ while (blocks.length) {
181
+ const b = blocks[0];
182
+ const done = force || settled(b) || (b.kind === 'text' && blocks.length > 1);
183
+ if (!done) break;
184
+ blocks.shift();
185
+ if (b.kind === 'text' && !b.text.trim()) continue;
186
+ out.push(b);
187
+ }
188
+ return out.length || blocks.length !== s.blocks.length ? { ...s, history: [...s.history, ...out], blocks } : s;
189
+ }
190
+
191
+ /** Fold the turn's VOLT tool count and list into its AOCL summary (footer count, /aocl list). */
192
+ function withTools(sum, turn) {
193
+ const t = turn?.toolsOffer;
194
+ return t && t.offered != null ? { ...sum, toolsOffered: t.offered, toolsTotal: t.total, toolsList: t.list } : sum;
195
+ }
196
+ function note(s, text, tone = 'dim') {
197
+ return { ...s, history: [...s.history, { id: nid(), kind: 'note', text, tone }] };
198
+ }
199
+
200
+ function addBlock(s, b) { return flush({ ...s, blocks: [...s.blocks, { id: nid(), ...b }] }); }
201
+
202
+ // Raw markdown of each answer, kept for /copy (history holds only the split, rendered blocks).
203
+ function recordRaw(s, content) {
204
+ const id = s.turn?.id ?? '_';
205
+ const a = s.answers || [];
206
+ const last = a[a.length - 1];
207
+ const next = last && last.id === id ? a.slice(0, -1).concat({ id, text: last.text + content }) : a.concat({ id, text: String(content) }).slice(-20);
208
+ return { ...s, answers: next };
209
+ }
210
+
211
+ function appendText(s00, content) {
212
+ const s0 = recordRaw(s00, content);
213
+ const s = s0.turn && !s0.turn.sawText ? { ...s0, turn: { ...s0.turn, sawText: true } } : s0;
214
+ const last = s.blocks[s.blocks.length - 1];
215
+ if (last && last.kind === 'text') {
216
+ let text = last.text + content;
217
+ let history = s.history;
218
+ // Completed markdown blocks leave the live region (-> <Static>); only the unfinished tail re-renders.
219
+ if (s.blocks.length === 1 && text.length > 1) {
220
+ const { settled, rest } = splitSettled(text);
221
+ if (settled.trim()) { history = [...history, { id: nid(), kind: 'text', text: settled }]; text = rest; }
222
+ }
223
+ return { ...s, history, blocks: s.blocks.slice(0, -1).concat({ ...last, text }) };
224
+ }
225
+ return addBlock(s, { kind: 'text', text: content });
226
+ }
227
+
228
+ /** QUOXON-FW3: who a delegation targets. Server fields first; older servers only put agent_id=<id> in the summary. */
229
+ export function delegateInfo(ev) {
230
+ if (ev.tool !== 'delegate_to_agent') return {};
231
+ const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || null;
232
+ if (!id) return {};
233
+ return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}) };
234
+ }
235
+
236
+ function upsertTool(s, ev, patch) {
237
+ const i = s.blocks.findIndex(b => b.kind === 'tool' && b.callId === ev.call_id);
238
+ if (i >= 0) {
239
+ const cur = s.blocks[i];
240
+ // started after finished (out of order): never downgrade a settled row
241
+ const p = cur.status !== 'running' && patch.status === 'running' ? { summary: cur.summary || patch.summary, tool: cur.tool || patch.tool } : patch;
242
+ const blocks = s.blocks.slice();
243
+ blocks[i] = { ...cur, ...p };
244
+ const done = p.status && p.status !== 'running' ? { ...s.doneCalls, [ev.call_id]: true } : s.doneCalls;
245
+ return flush({ ...s, blocks, doneCalls: done });
246
+ }
247
+ // A row that already settled and left the live region: a late tool.started (out of order) or a duplicate finish is dropped.
248
+ if (s.doneCalls[ev.call_id]) return s;
249
+ let n = patch.status !== 'running' ? { ...s, doneCalls: { ...s.doneCalls, [ev.call_id]: true } } : s;
250
+ if (n.turn) n = { ...n, turn: { ...n.turn, tools: n.turn.tools + 1 } };
251
+ return addBlock(n, { kind: 'tool', callId: ev.call_id, tool: ev.tool, ...patch });
252
+ }
253
+
254
+ function finishRunning(s, status, detail) {
255
+ const blocks = s.blocks.map(b => b.kind === 'tool' && b.status === 'running'
256
+ ? { ...b, status, resultSummary: detail } : b);
257
+ return { ...s, blocks };
258
+ }
259
+
260
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
261
+
262
+ /** One grey closing line from facts only (durations, counts, receipt ids). */
263
+ export function summarizeTurn(s, status, now) {
264
+ const t = s.turn;
265
+ const secs = t && now != null && t.startedAt != null ? Math.max(0, Math.round((now - t.startedAt) / 1000)) : null;
266
+ const finished = t?.finishedTools || 0;
267
+ const unknown = s.blocks.filter(b => b.kind === 'tool' && (b.status === 'running' || b.status === 'effect_unknown')).length;
268
+ if (status === 'unconfirmed') return 'Ended without confirmation from the server';
269
+ const head = status === 'interrupted' ? 'Interrupted' : status === 'failed' ? 'Failed' : 'Thought';
270
+ const parts = [secs == null ? head : `${head} ${status === 'completed' ? 'for' : 'after'} ${secs}s`];
271
+ if (t?.model) parts.push(t.model.length > 32 ? t.model.slice(0, 31) + '…' : t.model); // which model answered, as reported
272
+ // Real server usage when it arrived; otherwise the estimate from streamed text; nothing when neither exists.
273
+ const tk = turnTokens(t);
274
+ if (tk > 0) parts.push(`${fmtTokens(tk)} tokens`);
275
+ // A labelled estimate (cost_source, e.g. the claude CLI's own figure) is never shown as this turn's price; /cost explains it.
276
+ const cost = t?.usage?.costSource || t?.viaCli ? null : fmtCost(t?.usage?.cost ?? normUsage(null, t?.doneCost).cost);
277
+ if (cost) parts.push(cost);
278
+ if (status === 'interrupted' || status === 'failed') {
279
+ if (t?.tools) parts.push(`${plural(finished, 'tool')} finished`);
280
+ if (unknown) parts.push(`${unknown} effect unknown`);
281
+ } else if (t?.tools) parts.push(plural(t.tools, 'tool'));
282
+ if (t?.approvals) parts.push(plural(t.approvals, 'approval'));
283
+ const r = t?.receipts || [];
284
+ if (r.length === 1) parts.push(`receipt ward:${r[0]}`);
285
+ else if (r.length > 1) parts.push(`${r.length} receipts, last ward:${r[r.length - 1]}`);
286
+ return parts.join(' · ');
287
+ }
288
+
289
+ function endTurn(s, status, now) {
290
+ let n = s;
291
+ if (status === 'interrupted' || status === 'failed' || status === 'unconfirmed') {
292
+ n = finishRunning(n, 'effect_unknown', 'dispatched, no result received; verify before retrying');
293
+ } else {
294
+ n = finishRunning(n, 'effect_unknown', 'no result reported');
295
+ }
296
+ n = flush(n, true);
297
+ const summary = summarizeTurn(s, status, now);
298
+ if (status === 'interrupted') n = note(n, 'Interrupted. Nothing completed was undone.', 'warn');
299
+ // unaccepted steers become queued items, in front
300
+ let pending = n.pending;
301
+ let steering = n.steering;
302
+ if (steering.length) {
303
+ pending = [...steering.map(x => ({ id: x.id, text: x.text })), ...pending];
304
+ steering = [];
305
+ n = note(n, 'Steering was not applied before the turn ended; queued instead.', 'warn');
306
+ }
307
+ n = { ...n, history: [...n.history, { id: nid(), kind: 'summary', text: summary }] };
308
+ return {
309
+ ...n, turn: null, pending, steering,
310
+ // Only a confirmed-completed turn drains the queue; failed, interrupted or unconfirmed turns leave
311
+ // the pending items visible for the human to send.
312
+ autoDrain: status === 'completed' && pending.length > 0
313
+ };
314
+ }
315
+
316
+ function reduceInner(s, a) {
317
+ switch (a.type) {
318
+ case 'user': // a submitted prompt
319
+ return { ...s, history: [...s.history, { id: nid(), kind: 'user', text: a.text, queued: !!a.queued, ...(a.echo ? { echo: true } : {}) }] };
320
+ case 'turn.begin':
321
+ return { ...s, lastTurnId: a.turnId, turn: { id: a.turnId, status: 'running', startedAt: a.now, stage: null, activity: null, tools: 0, finishedTools: 0, approvals: 0, receipts: [], chars: 0, usage: null }, blocks: [], autoDrain: false };
322
+ case 'note':
323
+ return note(s, a.text, a.tone);
324
+ // ASK-SETTLE (owner 2026-10-17): an open ask is a live card; settling it removes the card and leaves one line.
325
+ case 'ask.open': { // the same ask shown again (after a chat about it) replaces its card, never a second one
326
+ const asks = (s.asks || []).filter(x => x.text !== a.text);
327
+ return { ...s, asks: [...asks, { id: a.id, text: a.text, at: a.at ?? null }] };
328
+ }
329
+ case 'ask.settle': {
330
+ const asks = s.asks || [];
331
+ if (!asks.some(x => x.id === a.id)) return s;
332
+ if (a.silent) return { ...s, asks: asks.filter(x => x.id !== a.id) }; // a walkthrough card that ended normally: no leftover line
333
+ return note({ ...s, asks: asks.filter(x => x.id !== a.id) }, a.text, a.tone);
334
+ }
335
+ case 'slotmsg.settle': { // SLOT-RUN: an answered/expired run request collapses to one line in place
336
+ let hit = false;
337
+ const history = s.history.map(it => {
338
+ if (it.kind !== 'slotmsg' || !it.line?.run || it.line.run.askId !== a.askId || it.line.run.settled) return it;
339
+ hit = true;
340
+ return { ...it, line: { ...it.line, run: { ...it.line.run, settled: a.text, settledTone: a.tone } } };
341
+ });
342
+ return hit ? { ...s, history } : s;
343
+ }
344
+ case 'bang.out': // QUOXON-FW8: command output, shown as is (not note-painted); the transcript records only the action name
345
+ return { ...s, history: [...s.history, { id: nid(), kind: 'bang', text: a.text }] };
346
+ case 'slotmsg': // QUOXON-MESH: a slot chat line (slotChat.js slotLine), incoming or sent
347
+ return { ...s, history: [...s.history, { id: nid(), kind: 'slotmsg', line: a.line }] };
348
+ case 'tip': // a "did you know" tip (tips.js); text + the command to try
349
+ return { ...s, history: [...s.history, { id: nid(), kind: 'tip', text: a.text, cmd: a.cmd }] };
350
+ case 'bcard': // QUOXON-BROADCAST: the sender's framed delivery card (slash/broadcast.js broadcastCard), pure data
351
+ return { ...s, history: [...s.history, { id: nid(), kind: 'bcard', card: a.card }] };
352
+ case 'whatsnew': // the startup "What's new" box (whatsnew.js): [{ title, cmd }] + how many more
353
+ return { ...s, history: [...s.history, { id: nid(), kind: 'whatsnew', items: a.items, more: a.more, head: a.head, pad: a.pad }] };
354
+ case 'notify':
355
+ return { ...s, notify: { ...s.notify, ...a.patch } };
356
+ case 'welcome':
357
+ return { ...s, history: [...s.history, { id: nid(), kind: 'welcome', version: a.version }] };
358
+ case 'introShow': // /welcome: the first screen again (Q art + the greeting text), settled in history
359
+ return { ...s, history: [...s.history, { id: nid(), kind: 'intro', text: a.text, cols: a.cols }] };
360
+ case 'help':
361
+ return { ...s, history: [...s.history, { id: nid(), kind: 'help', model: a.model }] };
362
+ case 'object': // inline object view (objects/): pure data, rebuilt at render width
363
+ return { ...s, history: [...s.history, { id: nid(), kind: 'object', obj: a.obj }] };
364
+ case 'error':
365
+ return flush(addBlock(s, { kind: 'error', text: a.text }), false);
366
+ case 'verbose':
367
+ return { ...s, verbose: a.mode };
368
+ case 'aocl.table': // /aocl: the last turn's per-layer table, on demand
369
+ return { ...s, history: [...s.history, { id: nid(), kind: 'aocl', table: true, sum: s.lastAocl || null }] };
370
+ case 'history.add': // restored turns (/resume): ordinary history items, plus their answers for /copy
371
+ return { ...s, history: [...s.history, ...(a.items || []).map(i => ({ id: nid(), ...i }))], answers: [...(s.answers || []), ...(a.answers || [])].slice(-20) };
372
+ case 'history.reset': // /clear: a fresh screen; nothing on the server is touched
373
+ return s.turn ? s : { ...s, history: [], blocks: [], answers: [] };
374
+ case 'ctx':
375
+ return { ...s, ctx: { ...s.ctx, ...a.ctx } };
376
+ case 'resume': { // QUOXON J2: open an existing conversation; its saved turns become the visible history
377
+ const items = [];
378
+ for (const t of a.turns || []) {
379
+ if (t.role === 'user') items.push({ id: nid(), kind: 'user', text: String(t.content ?? ''), queued: false });
380
+ else if (t.role === 'assistant') items.push({ id: nid(), kind: 'text', text: String(t.content ?? '') });
381
+ }
382
+ const n = items.filter((x) => x.kind === 'user').length;
383
+ const s2 = { ...s, ctx: { ...s.ctx, conversationId: a.conversationId }, history: [...s.history, ...items] };
384
+ return note(s2, `Resumed conversation ${a.conversationId} (${n} earlier turn${n === 1 ? '' : 's'}). New messages continue it.`);
385
+ }
386
+ case 'inbox':
387
+ return { ...s, inbox: a.items };
388
+ case 'queue.add':
389
+ return { ...s, pending: [...s.pending, { id: nid(), text: a.text }] };
390
+ case 'queue.remove':
391
+ return { ...s, pending: s.pending.filter(p => p.id !== a.id) };
392
+ case 'queue.shift':
393
+ return { ...s, pending: s.pending.slice(1), autoDrain: false };
394
+ case 'queue.pop': // Up pulls the last item back to the composer
395
+ return { ...s, pending: s.pending.slice(0, -1) };
396
+ case 'steer.add':
397
+ return { ...s, steering: [...s.steering, { id: a.id, text: a.text }] };
398
+ case 'steer.converted': {
399
+ const item = s.steering.find(x => x.id === a.id);
400
+ if (!item) return s;
401
+ const n = note({ ...s, steering: s.steering.filter(x => x.id !== a.id), pending: [...s.pending, { id: item.id, text: item.text }] },
402
+ a.reason || 'Steering is not available for this turn; queued instead.', 'warn');
403
+ return { ...n, autoDrain: !n.turn };
404
+ }
405
+ case 'steer.failed': { // ambiguous or denied: never auto-queued; the text goes back to the draft (the app does that)
406
+ if (!s.steering.some(x => x.id === a.id)) return s;
407
+ return note({ ...s, steering: s.steering.filter(x => x.id !== a.id) }, a.reason, 'warn');
408
+ }
409
+ case 'interrupt.requested':
410
+ return s.turn ? { ...s, turn: { ...s.turn, status: 'interrupting' } } : s;
411
+ case 'turn.deferred': // QUOXON-FW3: the send never reached the server; end the turn quietly (no failure summary, nothing ran)
412
+ return { ...s, turn: null, blocks: [], autoDrain: false };
413
+ case 'stopped.local': { // SSE aborted by us because the server has no interrupt route
414
+ let n = note(s, 'Stopped locally; work already sent may still finish. Verify before retrying.', 'warn');
415
+ return endTurn(n, 'interrupted', a.now);
416
+ }
417
+ case 'approval.found': { // pending approval seen by polling (covers provider paths that emit no approval.requested)
418
+ const card = cardFromInboxItem(a.item, { since: s.turn?.startedAt ?? a.since ?? 0, userId: a.userId, conversationId: s.ctx.conversationId, now: a.now || 0 });
419
+ if (card && card.approvalId) { // an event card raised without an inbox id learns it from the poll (else y answers a null id: 404)
420
+ const j = s.approvals.findIndex(x => !x.inboxItemId && x.approvalId === card.approvalId);
421
+ if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
422
+ }
423
+ if (card && card.approvalId) { // an event stub that could not be described learns its description from the inbox item
424
+ const j = s.approvals.findIndex(x => x.undescribable && (x.inboxItemId === card.inboxItemId || x.approvalId === card.approvalId));
425
+ if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, undescribable: false }; return { ...s, approvals }; }
426
+ }
427
+ if (!card || s.seenInbox[card.inboxItemId] || s.approvals.some(x => x.inboxItemId === card.inboxItemId || (card.approvalId && x.approvalId === card.approvalId))) return s;
428
+ if (card.foreign) { // another conversation's card: a compact notice, never the focused card
429
+ if (s.waiting.some(x => x.inboxItemId === card.inboxItemId)) return s;
430
+ return { ...s, waiting: [...s.waiting, card] };
431
+ }
432
+ return { ...s, approvals: [...s.approvals, { ...card, turnId: s.turn?.id ?? null }], seenInbox: { ...s.seenInbox, [card.inboxItemId]: true }, turn: s.turn ? { ...s.turn, approvals: s.turn.approvals + 1 } : s.turn };
433
+ }
434
+ case 'approval.gone': { // the server says it no longer knows this approval (404): someone already handled it
435
+ const hit = x => x.callId === a.callId || (a.inboxItemId && x.inboxItemId === a.inboxItemId);
436
+ const had = s.approvals.some(hit) || s.waiting.some(hit);
437
+ const n = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), approvals: s.approvals.filter(x => !hit(x)), waiting: s.waiting.filter(x => !hit(x)), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox };
438
+ return had ? note(n, 'approval already handled', 'accent') : n;
439
+ }
440
+ case 'approval.dismiss':
441
+ return { ...s, approvals: s.approvals.filter(x => x.callId !== a.callId) };
442
+ case 'approval.sent': { // the human answered from this terminal
443
+ const rest = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox, approvals: s.approvals.filter(x => x.callId !== a.callId && !(a.inboxItemId && x.inboxItemId === a.inboxItemId)), waiting: s.waiting.filter(x => x.callId !== a.callId && x.inboxItemId !== a.callId) };
444
+ if (a.batch) return rest; // /approve N prints its own one-line summary
445
+ if (a.quiet && a.action === 'approve') return rest;
446
+ if (a.action !== 'approve') return note(rest, `denied: ${a.tool || 'approval'}${a.target ? ` on ${a.target}` : ''}`, 'warn');
447
+ if (a.foreign) return note(rest, `✔ Approved ${a.tool || 'approval'}${a.target ? ` on ${a.target}` : ''}; the plugin or agent will act on it (its outcome is not tracked here)`, 'ok');
448
+ if (a.tool === 'hat.switch') { // applied server-side by the inbox approve; nothing to wait for
449
+ const to = String(a.target || '').split('\u2192').pop().replace(/\(privileged\)/, '').trim() || 'the new hat';
450
+ return note(rest, `Hat switched to ${to}. Ask again and I'll carry on.`, 'ok');
451
+ }
452
+ const run = { callId: a.callId, approvalId: a.approvalId || null, tool: a.tool, target: a.target };
453
+ return note({ ...rest, running: [...rest.running.filter(x => x.callId !== a.callId), run] },
454
+ `approved; running (result arrives asynchronously)${a.tool ? `: ${a.tool}${a.target ? ` on ${a.target}` : ''}` : ''}`, 'ok');
455
+ }
456
+ case 'approval.result': { // outcome of polling GET /api/v1/approvals/:id/result
457
+ const run = s.running.find(x => x.callId === a.callId);
458
+ const n = { ...s, running: s.running.filter(x => x.callId !== a.callId) };
459
+ const what = run ? `${run.tool || 'approval'}${run.target ? ` on ${run.target}` : ''}` : 'approval';
460
+ if (a.outcome === 'done') return note(n, `result: ${what}: ${a.text || 'completed'}`, 'ok');
461
+ if (a.outcome === 'failed') return note(n, `failed: ${what}: ${a.text || 'error'}`, 'warn');
462
+ if (a.outcome === 'denied') return note(n, `denied: ${what}`, 'warn');
463
+ // FW4: never silent and never a guess. Say what is unknown and where the authoritative answer is.
464
+ const check = run && run.approvalId ? `quox approvals result ${run.approvalId} (or /approvals)` : '/approvals (no approval id was returned, so it cannot be tracked from here)';
465
+ return note(n, `result not known yet: ${what}; approved, but the server has not said whether it ran. It may still be running or may have run. Check: ${check}`, 'warn');
466
+ }
467
+ case 'inbox.sync': { // open approval ids from a successful inbox refresh
468
+ const ids = new Set(a.ids);
469
+ const approvals = s.approvals.filter(x => !x.inboxItemId || ids.has(x.inboxItemId) || (a.now || 0) - x.shownAt < 5000);
470
+ const waiting = s.waiting.filter(x => ids.has(x.inboxItemId));
471
+ return approvals.length === s.approvals.length && waiting.length === s.waiting.length ? s : { ...s, approvals, waiting };
472
+ }
473
+ case 'event':
474
+ return reduceEvent(s, a.ev, a.now || 0);
475
+ default:
476
+ return s;
477
+ }
478
+ }
479
+
480
+ /** The first card is armed from the moment it is actually SHOWN, not from when it was received. */
481
+ export function reduce(s, a) {
482
+ let n = reduceInner(s, a);
483
+ // TUI-DIVIDER: the bottom-rule AOCL strip, moved only by real turn events (dividerAocl.js).
484
+ if (a.type === 'turn.begin' || a.type === 'event') {
485
+ const dv = dividerStep(s.divider ?? null, a, a.now || 0);
486
+ if (dv !== (n.divider ?? null)) n = { ...n, divider: dv };
487
+ }
488
+ // The honest activity word: only real events move it (see activity.js).
489
+ if (n.turn) {
490
+ const act = a.type === 'event' ? nextActivity(s.turn?.act, a.ev, a.now || 0, { hasText: Boolean(n.turn.chars) })
491
+ : actionActivity(n.turn.act, a, a.now || 0);
492
+ if (act !== n.turn.act) n = { ...n, turn: { ...n.turn, act } };
493
+ }
494
+ const first = n.approvals[0];
495
+ if (first && first.displayedAt == null) {
496
+ return { ...n, approvals: [{ ...first, displayedAt: a.now || 0 }, ...n.approvals.slice(1)] };
497
+ }
498
+ return n;
499
+ }
500
+
501
+ function reduceEvent(s, ev, now) {
502
+ switch (ev.type) {
503
+ case 'connected': return s;
504
+ // background.status is not in events v1 yet; handled so the line works the moment the server sends it (RESTORE-TUI: 4fbd3178c dropped it).
505
+ case 'background.status': return { ...s, background: Math.max(0, Number(ev.pending) || 0) };
506
+ case 'turn.started': {
507
+ const ctx = {
508
+ agent: ev.agent ?? s.ctx.agent, model: ev.model ?? s.ctx.model, provider: ev.provider ?? s.ctx.provider,
509
+ // B3: the host a self-hosted provider ran at; cleared when the server names none.
510
+ endpoint: ev.provider ? (ev.endpoint ?? null) : (s.ctx.endpoint ?? null),
511
+ // QUOXON-PARITY: which Claude path ran (subscription CLI vs API key); cleared like endpoint.
512
+ auth: ev.provider ? (ev.auth ?? null) : (s.ctx.auth ?? null),
513
+ hat: ev.hat ?? s.ctx.hat, conversationId: ev.conversation_id ?? s.ctx.conversationId
514
+ };
515
+ // The model THIS turn actually used, as the server reported it (never the profile's default).
516
+ const used = ev.model || ev.provider || null;
517
+ return { ...s, ctx, turn: s.turn ? { ...s.turn, gate: textGate(s.turn.gate, ev).st, ...(used ? { model: String(used) } : {}) } : s.turn };
518
+ }
519
+ case 'thinking': {
520
+ const stage = ev.stage || '';
521
+ const msg = String(ev.message || '');
522
+ if (!s.turn) return s;
523
+ const turn = { ...s.turn, stage: msg || stage, activity: stage.includes('tool') && !s.blocks.some(b => b.kind === 'tool' && b.status === 'running') ? msg : s.turn.activity };
524
+ let n = { ...s, turn };
525
+ // B3: a provider fact the user should see (e.g. "this model does not support tools"), always, dim.
526
+ if (stage === 'provider_note' && msg) return addBlock(n, { kind: 'note', text: msg, tone: 'dim' });
527
+ // QUOXON: the model's own reasoning text: one dim, collapsed line (size only); verbose full
528
+ // expands it. It is never answer text.
529
+ if (stage === 'model_reasoning') {
530
+ const why = String(ev.reasoning || '').trim();
531
+ const text = s.verbose === 'full' && why ? `thinking: ${why}` : (msg || 'thinking');
532
+ return addBlock(n, { kind: 'note', text, tone: 'dim', collapsed: !(s.verbose === 'full' && why) });
533
+ }
534
+ // Legacy servers report tool use only as a thinking stage: show it honestly, with no result claimed.
535
+ if (stage.includes('tool') && msg) n = addBlock(n, { kind: 'note', text: `working: ${msg}`, tone: 'dim', legacyTool: true });
536
+ if (s.verbose === 'full' && msg && !stage.includes('tool')) n = addBlock(n, { kind: 'note', text: `thinking: ${msg}`, tone: 'dim' });
537
+ return n;
538
+ }
539
+ case 'chunk': case 'text.delta': {
540
+ const g = textGate(s.turn?.gate, ev);
541
+ const n = s.turn ? { ...s, turn: { ...s.turn, gate: g.st } } : s;
542
+ if (!g.append) return n;
543
+ const counted = n.turn ? { ...n, turn: { ...n.turn, chars: (n.turn.chars || 0) + String(ev.content).length } } : n;
544
+ return appendText(counted, ev.content);
545
+ }
546
+ // QUOXON C5 (events v1.1): the client's own reconnect facts around a dropped durable turn.
547
+ case 'reconnecting':
548
+ return ev.attempt === 1 && s.turn ? addBlock(s, { kind: 'note', text: 'Connection to the server dropped; reconnecting. Quox keeps working on the turn meanwhile.', tone: 'warn' }) : s;
549
+ case 'resumed':
550
+ return s.turn ? addBlock(s, { kind: 'note', text: ev.after ? `Reconnected; picking up after event ${ev.after}.` : 'Reattached to the running turn.', tone: 'dim' }) : s;
551
+ case 'replay.gap':
552
+ return addBlock(s, { kind: 'note', text: `Events ${ev.from} to ${ev.to} of this turn were not kept on the server (size cap); the rest follows.`, tone: 'warn' });
553
+ case 'tool.started':
554
+ return upsertTool(s, ev, { status: 'running', summary: ev.summary || ev.tool, startedAt: now, ...delegateInfo(ev) });
555
+ case 'tool.finished': {
556
+ const status = TOOL_STATUSES.includes(ev.status) ? ev.status : 'unknown'; // never green for a status we do not know
557
+ const first = !s.doneCalls[ev.call_id] && ['ok', 'failed', 'denied'].includes(status);
558
+ let withTool = upsertTool(s, ev, {
559
+ status, resultSummary: toolResultText(ev.result_summary, s.verbose), rawSummary: ev.result_summary, durationMs: ev.duration_ms,
560
+ envelopeId: ev.envelope_id, receipt: ev.receipt || null,
561
+ ...(Number.isFinite(ev.tools_used) ? { toolsUsed: ev.tools_used } : {})
562
+ });
563
+ if (first && withTool.turn) withTool = { ...withTool, turn: { ...withTool.turn, finishedTools: withTool.turn.finishedTools + 1 } };
564
+ if (ev.receipt && ev.receipt.verified === true && withTool.turn && ev.receipt.ward_seq != null) {
565
+ return { ...withTool, turn: { ...withTool.turn, receipts: [...withTool.turn.receipts, ev.receipt.ward_seq] } };
566
+ }
567
+ return withTool;
568
+ }
569
+ case 'approval.requested': {
570
+ const card = {
571
+ callId: ev.call_id, inboxItemId: ev.inbox_item_id, approvalId: ev.approval_id, tool: ev.tool,
572
+ target: ev.target, scope: ev.scope, risk: ev.risk, url: ev.dashboard_url, shownAt: now,
573
+ undescribable: ev.describable === false || !ev.target,
574
+ turnId: s.turn?.id ?? ev.turn_id ?? null, // the turn this card arrived in (a stream event is always this conversation)
575
+ privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true })
576
+ };
577
+ if (s.approvals.some(x => x.callId === card.callId)) return s;
578
+ const dup = s.approvals.findIndex(x => (card.inboxItemId && x.inboxItemId === card.inboxItemId) || (card.approvalId && x.approvalId === card.approvalId));
579
+ if (dup >= 0) { // already raised by the poll: adopt the event's call id so approval.resolved finds it
580
+ const approvals = s.approvals.slice(); approvals[dup] = { ...approvals[dup], callId: card.callId };
581
+ return { ...s, approvals };
582
+ }
583
+ const wi = card.inboxItemId ? s.waiting.findIndex(x => x.inboxItemId === card.inboxItemId) : -1;
584
+ if (wi >= 0) s = { ...s, waiting: s.waiting.filter((_, i) => i !== wi) }; // the stream says it is ours after all
585
+ if (card.inboxItemId && s.seenInbox[card.inboxItemId]) return s; // already shown and answered
586
+ return { ...s, seenInbox: card.inboxItemId ? { ...s.seenInbox, [card.inboxItemId]: true } : s.seenInbox, approvals: [...s.approvals, card], turn: s.turn ? { ...s.turn, approvals: s.turn.approvals + 1 } : s.turn };
587
+ }
588
+ case 'approval.resolved': {
589
+ const card = s.approvals.find(x => x.callId === ev.call_id);
590
+ const n = { ...s, approvals: s.approvals.filter(x => x.callId !== ev.call_id) };
591
+ return card || ev.decision ? note(n, `${ev.decision === 'approved' ? '✔ approved by you' : `approval ${ev.decision || 'resolved'}`}${card ? `: ${card.tool} on ${card.target}` : ''}`, ev.decision === 'approved' ? 'ok' : 'warn') : n;
592
+ }
593
+ case 'steer.accepted': {
594
+ const i = s.steering.findIndex(x => x.text === ev.text);
595
+ const steering = i >= 0 ? s.steering.filter((_, k) => k !== i) : s.steering.slice(1);
596
+ return addBlock({ ...s, steering }, { kind: 'note', text: `steered: ${ev.text}`, tone: 'accent' });
597
+ }
598
+ case 'trace':
599
+ // QUOXON-AOCL: per-layer AOCL rows fold into ONE line per turn (aocl.summary); /aocl shows the table.
600
+ if (ev.protocol === 'volt' && /tools offered/i.test(ev.label || '')) { // kept for the footer count and /aocl, shown only in verbose
601
+ const m = String(ev.label).match(/(\d+)\s+of\s+(\d+)/);
602
+ const list = String(ev.detail || '');
603
+ const toolsOffered = m ? Number(m[1]) : (list ? list.split(/,\s*/).filter(Boolean).length : null);
604
+ if (s.turn) s = { ...s, turn: { ...s.turn, toolsOffer: { offered: toolsOffered, total: m ? Number(m[2]) : null, list } } };
605
+ }
606
+ if (isLayerTrace(ev)) return s.turn ? { ...s, turn: { ...s.turn, aoclTraces: [...(s.turn.aoclTraces || []), { label: ev.label, detail: ev.detail }] } } : s;
607
+ if (s.verbose === 'off') return s;
608
+ return addBlock(s, { kind: 'trace', protocol: ev.protocol, label: ev.label, ref: ev.ref, detail: ev.detail });
609
+ case 'aocl.summary': {
610
+ const sum = withTools(normaliseSummary(ev), s.turn);
611
+ const n = { ...s, lastAocl: sum, turn: s.turn ? { ...s.turn, aoclSummary: true } : s.turn };
612
+ return addBlock(n, { kind: 'aocl', sum }); // the one-line footer shows at every verbosity
613
+ }
614
+ case 'aocl.verdict': {
615
+ const v = verdictNote(ev);
616
+ return v.tone === 'error' ? addBlock(s, { kind: 'error', text: v.text }) : addBlock(s, { kind: 'note', text: v.text, tone: v.tone });
617
+ }
618
+ case 'error': {
619
+ const n = addBlock(s, { kind: 'error', text: isConsentError(ev) ? consentErrorText(ev) : String(ev.error || 'stream error') });
620
+ return n;
621
+ }
622
+ case 'turn.finished': case 'done': {
623
+ if (!s.turn) return s; // dedupe: done after turn.finished
624
+ let n = s;
625
+ const convo = ev.conversation_id || ev.session_id;
626
+ if (convo) n = { ...n, ctx: { ...n.ctx, conversationId: convo } };
627
+ if (ev.type === 'done' && !n.turn.sawText && ev.full_response) {
628
+ n = appendText(n, ev.full_response);
629
+ }
630
+ // `done` only carries text/session; turn.finished (sent just after it) ends the turn. A legacy
631
+ // server never sends turn.finished: the app synthesises one when the stream closes.
632
+ // usage: `done` may carry cost_usd; turn.finished carries `usage` (input/output tokens, maybe cost_usd).
633
+ // cli_session_id on `done` = the claude CLI path: its cost is that CLI's own estimate even from a server that
634
+ // does not label it yet (cost_source 'claude-cli', R3 D5).
635
+ if (ev.type === 'done') return { ...n, turn: { ...n.turn, doneCost: ev.cost_usd, viaCli: Boolean(ev.cli_session_id) } };
636
+ // An older server sends per-layer AOCL traces and no aocl.summary: fold them into the one line here.
637
+ if (!n.turn.aoclSummary && n.turn.aoclTraces?.length) {
638
+ const sum = withTools(summaryFromTraces(n.turn.aoclTraces), n.turn);
639
+ n = { ...n, lastAocl: sum };
640
+ n = addBlock(n, { kind: 'aocl', sum });
641
+ }
642
+ n = { ...n, turn: { ...n.turn, usage: normUsage(n.turn.viaCli && ev.usage && !ev.usage.cost_source ? { ...ev.usage, cost_source: 'claude-cli' } : ev.usage, n.turn.doneCost) } };
643
+ return endTurn(n, ev.status || 'completed', now);
644
+ }
645
+ default:
646
+ return s;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Approval key guard: dialog arms after 300 ms, ignores held keys (any key
652
+ * seen within KEY_REPEAT_MS), and the caller must never map Enter to approve.
653
+ */
654
+ export function approvalKeyAllowed({ now, shownAt, lastKeyAt, heldKey = null, ch = null }) {
655
+ if (now - shownAt < APPROVAL_ARM_MS) return false;
656
+ // a fresh press needs a release gap: any key within KEY_REPEAT_MS is a held/repeat sequence
657
+ if (lastKeyAt != null && now - lastKeyAt < KEY_REPEAT_MS) return false;
658
+ // the key that was already down when the card appeared stays dead until another key or a long gap
659
+ if (heldKey != null && ch === heldKey && (lastKeyAt == null || now - lastKeyAt < HELD_KEY_MS)) return false;
660
+ return true;
661
+ }
662
+
663
+ /**
664
+ * What an approval is for, without repeating who asked: titles often start "<who> wants to: ..." (the source label
665
+ * already says who), and a generic "Approval required" title falls back to the item's summary or description.
666
+ */
667
+ export function approvalWhat(it) {
668
+ const w = it?.payload?.what;
669
+ if (w && typeof w === 'object' && typeof w.summary === 'string' && w.summary.trim()) { // APPROVAL-WHAT: the server says what will happen
670
+ const s = w.summary.trim(); const tg = typeof w.target === 'string' ? w.target.trim() : '';
671
+ return tg && !s.includes(tg) ? `${s} (target: ${tg})` : (!tg && w.target_none ? `${s} (${String(w.target_none).trim()})` : s);
672
+ }
673
+ let t = String(it?.title || it?.id || '').replace(/^[^:]{0,80}\bwants to:\s*/i, '').trim();
674
+ const more = it?.summary || it?.description || it?.detail || '';
675
+ if ((!t || /^approval (?:required|needed)\b/i.test(t)) && more) t = String(more).trim();
676
+ return t || String(it?.id || 'approval');
677
+ }