quoxcli 0.2.0 → 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 (327) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/admin.js +26 -0
  5. package/lib/api/endpoints/agent-schedules.js +26 -0
  6. package/lib/api/endpoints/agents.js +7 -0
  7. package/lib/api/endpoints/assistants.js +12 -0
  8. package/lib/api/endpoints/away.js +15 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/contactsq.js +2 -0
  11. package/lib/api/endpoints/conversations.js +14 -0
  12. package/lib/api/endpoints/executions.js +13 -0
  13. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  14. package/lib/api/endpoints/files.js +14 -0
  15. package/lib/api/endpoints/hat.js +30 -0
  16. package/lib/api/endpoints/inbox.js +20 -0
  17. package/lib/api/endpoints/index.js +14 -0
  18. package/lib/api/endpoints/memory.js +19 -0
  19. package/lib/api/endpoints/mind.js +13 -0
  20. package/lib/api/endpoints/mon.js +6 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/plugins.js +18 -0
  23. package/lib/api/endpoints/presence.js +33 -0
  24. package/lib/api/endpoints/quoxplan.js +7 -0
  25. package/lib/api/endpoints/quoxseo.js +6 -0
  26. package/lib/api/endpoints/router.js +17 -0
  27. package/lib/api/endpoints/safety.js +7 -0
  28. package/lib/api/endpoints/skills.js +20 -0
  29. package/lib/api/endpoints/stepup.js +20 -0
  30. package/lib/api/endpoints/teams.js +2 -2
  31. package/lib/api/endpoints/triggers.js +26 -0
  32. package/lib/api/endpoints/v1.js +86 -0
  33. package/lib/api/endpoints/ward.js +7 -0
  34. package/lib/api/endpoints/workflows.js +7 -0
  35. package/lib/api/endpoints/x.js +8 -0
  36. package/lib/commands/actions.js +61 -0
  37. package/lib/commands/admin.js +16 -0
  38. package/lib/commands/agent.js +163 -27
  39. package/lib/commands/agentAttach.js +94 -6
  40. package/lib/commands/approvals.js +73 -1
  41. package/lib/commands/aside.js +24 -0
  42. package/lib/commands/ask.js +144 -0
  43. package/lib/commands/assistant.js +59 -4
  44. package/lib/commands/auth.js +8 -2
  45. package/lib/commands/backup.js +8 -4
  46. package/lib/commands/brain.js +105 -14
  47. package/lib/commands/build.js +19 -5
  48. package/lib/commands/chat.js +57 -2
  49. package/lib/commands/claude.js +34 -8
  50. package/lib/commands/contacts.js +49 -0
  51. package/lib/commands/conversation.js +35 -4
  52. package/lib/commands/dashboard.js +43 -0
  53. package/lib/commands/department.js +28 -7
  54. package/lib/commands/devResident.js +15 -3
  55. package/lib/commands/devSupervisor.js +13 -1
  56. package/lib/commands/discord.js +75 -39
  57. package/lib/commands/done.js +98 -28
  58. package/lib/commands/engagement.js +17 -6
  59. package/lib/commands/fetchAllowlist.js +71 -0
  60. package/lib/commands/file.js +38 -1
  61. package/lib/commands/fleet.js +311 -4
  62. package/lib/commands/governance.js +12 -3
  63. package/lib/commands/gripe.js +44 -13
  64. package/lib/commands/hat.js +178 -9
  65. package/lib/commands/helpers.js +18 -1
  66. package/lib/commands/hyperion.js +58 -0
  67. package/lib/commands/inbox.js +74 -23
  68. package/lib/commands/licence.js +14 -155
  69. package/lib/commands/lucid.js +10 -4
  70. package/lib/commands/memory.js +85 -8
  71. package/lib/commands/mind.js +71 -0
  72. package/lib/commands/mon.js +69 -2
  73. package/lib/commands/monitoring.js +1 -1
  74. package/lib/commands/motion.js +2 -2
  75. package/lib/commands/nucleus.js +48 -17
  76. package/lib/commands/objective.js +11 -0
  77. package/lib/commands/offload.js +89 -0
  78. package/lib/commands/onboard.js +2 -1
  79. package/lib/commands/org.js +11 -1
  80. package/lib/commands/peek.js +28 -0
  81. package/lib/commands/plan.js +38 -0
  82. package/lib/commands/plugin.js +110 -0
  83. package/lib/commands/presence.js +89 -0
  84. package/lib/commands/provider.js +159 -0
  85. package/lib/commands/qlarity.js +19 -3
  86. package/lib/commands/quoxchat.js +13 -4
  87. package/lib/commands/quoxseo.js +38 -0
  88. package/lib/commands/reach.js +2 -1
  89. package/lib/commands/router.js +77 -0
  90. package/lib/commands/rows.js +358 -0
  91. package/lib/commands/run.js +64 -9
  92. package/lib/commands/server.js +292 -0
  93. package/lib/commands/setup.js +33 -7
  94. package/lib/commands/skills.js +48 -5
  95. package/lib/commands/slot.js +260 -7
  96. package/lib/commands/status.js +1 -1
  97. package/lib/commands/team.js +3 -1
  98. package/lib/commands/tips.js +23 -0
  99. package/lib/commands/tool.js +133 -45
  100. package/lib/commands/trace.js +12 -7
  101. package/lib/commands/trigger.js +95 -0
  102. package/lib/commands/update.js +46 -0
  103. package/lib/commands/vault.js +31 -8
  104. package/lib/commands/vm.js +237 -0
  105. package/lib/commands/voice.js +34 -3
  106. package/lib/commands/volt.js +45 -3
  107. package/lib/commands/walkthrough.js +32 -0
  108. package/lib/commands/watch.js +73 -21
  109. package/lib/commands/welcome.js +14 -0
  110. package/lib/commands/workflow.js +189 -32
  111. package/lib/commands/x.js +13 -4
  112. package/lib/core/actingAs.js +2 -2
  113. package/lib/core/activeSummary.js +186 -0
  114. package/lib/core/aside.js +54 -0
  115. package/lib/core/askDecision.js +60 -0
  116. package/lib/core/auth.js +164 -13
  117. package/lib/core/callerSource.js +1 -1
  118. package/lib/core/cliBuild.js +62 -0
  119. package/lib/core/client.js +144 -35
  120. package/lib/core/commandRegistrars.js +121 -0
  121. package/lib/core/config.js +142 -24
  122. package/lib/core/customHats.js +168 -0
  123. package/lib/core/doneEngineFetch.js +8 -0
  124. package/lib/core/errors.js +329 -20
  125. package/lib/core/global-flags.js +105 -5
  126. package/lib/core/hyperion.js +102 -0
  127. package/lib/core/inboxResolution.js +65 -0
  128. package/lib/core/lazyCommands.js +4 -1
  129. package/lib/core/offload.js +504 -0
  130. package/lib/core/org-override.js +1 -1
  131. package/lib/core/orgResolve.js +70 -0
  132. package/lib/core/output.js +4 -4
  133. package/lib/core/peek.js +126 -0
  134. package/lib/core/profileUsers.js +51 -0
  135. package/lib/core/promptCancel.js +18 -0
  136. package/lib/core/repoPath.js +11 -0
  137. package/lib/core/residentSeed.js +3 -1
  138. package/lib/core/rootOptions.js +39 -0
  139. package/lib/core/router.js +168 -0
  140. package/lib/core/selfUpdate.js +188 -0
  141. package/lib/core/sessionsOverview.js +64 -0
  142. package/lib/core/slotKeeper.js +248 -3
  143. package/lib/core/slotWake.js +120 -0
  144. package/lib/core/supervisorEngine.js +71 -2
  145. package/lib/core/version.js +1 -1
  146. package/lib/core/vm.js +361 -0
  147. package/lib/core/vmTui.js +86 -0
  148. package/lib/core/walkthrough.js +35 -0
  149. package/lib/hyperion/deck.js +29 -0
  150. package/lib/lucid/LucidCard.jsx +68 -0
  151. package/lib/lucid/LucidIntro.jsx +45 -58
  152. package/lib/lucid/cards.js +86 -0
  153. package/lib/lucid/markdown.js +81 -0
  154. package/lib/lucid/motion.js +56 -0
  155. package/lib/quoxon/activity.js +14 -2
  156. package/lib/quoxon/aocl.js +117 -0
  157. package/lib/quoxon/api.js +504 -25
  158. package/lib/quoxon/app.js +3000 -230
  159. package/lib/quoxon/approvalFacts.js +94 -0
  160. package/lib/quoxon/approvalsView.js +325 -0
  161. package/lib/quoxon/asideView.js +40 -0
  162. package/lib/quoxon/ask.js +121 -0
  163. package/lib/quoxon/away.js +52 -0
  164. package/lib/quoxon/bang.js +224 -0
  165. package/lib/quoxon/bulkRisk.js +17 -0
  166. package/lib/quoxon/capabilities.js +13 -6
  167. package/lib/quoxon/cells.js +53 -0
  168. package/lib/quoxon/commandShort.js +112 -0
  169. package/lib/quoxon/commands.js +363 -38
  170. package/lib/quoxon/connect.js +210 -23
  171. package/lib/quoxon/connectCard.js +19 -2
  172. package/lib/quoxon/consent.js +86 -0
  173. package/lib/quoxon/copy.js +4 -3
  174. package/lib/quoxon/countdownLine.js +11 -0
  175. package/lib/quoxon/demo.autoplay.js +29 -0
  176. package/lib/quoxon/demo.fixtures.js +1 -1
  177. package/lib/quoxon/demo.js +44 -3
  178. package/lib/quoxon/devObserver.js +20 -0
  179. package/lib/quoxon/dividerAocl.js +191 -0
  180. package/lib/quoxon/dividerRule.js +57 -0
  181. package/lib/quoxon/dragonModes.js +150 -0
  182. package/lib/quoxon/dragonView.js +41 -0
  183. package/lib/quoxon/earlyInput.js +40 -0
  184. package/lib/quoxon/echo.js +15 -0
  185. package/lib/quoxon/egg.js +2 -2
  186. package/lib/quoxon/events.js +244 -25
  187. package/lib/quoxon/feedback.js +50 -0
  188. package/lib/quoxon/gallery.js +21 -7
  189. package/lib/quoxon/handoffOffer.js +78 -0
  190. package/lib/quoxon/hatSwitch.js +37 -0
  191. package/lib/quoxon/headless.js +161 -0
  192. package/lib/quoxon/hostUrls.js +41 -0
  193. package/lib/quoxon/indicator.js +6 -2
  194. package/lib/quoxon/intent.js +12 -1
  195. package/lib/quoxon/keys.js +49 -6
  196. package/lib/quoxon/launcher.js +103 -0
  197. package/lib/quoxon/main.js +92 -160
  198. package/lib/quoxon/markdown.js +45 -21
  199. package/lib/quoxon/markdownSettle.js +19 -0
  200. package/lib/quoxon/matrixDomains.json +46 -0
  201. package/lib/quoxon/mentions.js +90 -0
  202. package/lib/quoxon/mind.js +103 -0
  203. package/lib/quoxon/mode.js +46 -0
  204. package/lib/quoxon/model.js +132 -26
  205. package/lib/quoxon/modelPrefs.js +345 -0
  206. package/lib/quoxon/motion/clock.js +3 -2
  207. package/lib/quoxon/motion/level.js +91 -0
  208. package/lib/quoxon/mouse.js +211 -0
  209. package/lib/quoxon/noai.js +57 -0
  210. package/lib/quoxon/objects/away.js +72 -0
  211. package/lib/quoxon/objects/fetch.js +17 -5
  212. package/lib/quoxon/objects/fleet.js +149 -0
  213. package/lib/quoxon/objects/health.js +186 -0
  214. package/lib/quoxon/objects/index.js +24 -3
  215. package/lib/quoxon/objects/mirror.js +137 -0
  216. package/lib/quoxon/objects/peek.js +17 -0
  217. package/lib/quoxon/objects/prove.js +130 -0
  218. package/lib/quoxon/objects/replay.js +125 -0
  219. package/lib/quoxon/objects/sitrep.js +58 -0
  220. package/lib/quoxon/objects/view.js +11 -0
  221. package/lib/quoxon/onboarding/copy.js +32 -4
  222. package/lib/quoxon/onboarding/credential.js +17 -4
  223. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  224. package/lib/quoxon/onboarding/failureCause.js +33 -0
  225. package/lib/quoxon/onboarding/finish.js +15 -0
  226. package/lib/quoxon/onboarding/index.js +34 -7
  227. package/lib/quoxon/onboarding/installRunner.js +64 -5
  228. package/lib/quoxon/onboarding/preflight.js +17 -3
  229. package/lib/quoxon/onboarding/probe.js +65 -0
  230. package/lib/quoxon/onboarding/view.js +76 -19
  231. package/lib/quoxon/orgLabel.js +10 -0
  232. package/lib/quoxon/orgSwitch.js +33 -0
  233. package/lib/quoxon/ownBroadcast.js +22 -0
  234. package/lib/quoxon/palette.js +43 -0
  235. package/lib/quoxon/print.js +9 -5
  236. package/lib/quoxon/profileSwitch.js +53 -0
  237. package/lib/quoxon/promptHistory.js +41 -0
  238. package/lib/quoxon/providers.js +48 -0
  239. package/lib/quoxon/qart.js +20 -51
  240. package/lib/quoxon/qdragon.js +410 -0
  241. package/lib/quoxon/qdragonFly.js +61 -0
  242. package/lib/quoxon/qdragonScenes.js +282 -0
  243. package/lib/quoxon/qeyes.js +213 -0
  244. package/lib/quoxon/qmotion.js +58 -37
  245. package/lib/quoxon/render.js +638 -119
  246. package/lib/quoxon/resend.js +72 -0
  247. package/lib/quoxon/roster.js +95 -9
  248. package/lib/quoxon/route.js +6 -5
  249. package/lib/quoxon/rows.js +711 -0
  250. package/lib/quoxon/rowsStore.js +80 -0
  251. package/lib/quoxon/screenContext.js +150 -0
  252. package/lib/quoxon/screenFrames.js +254 -0
  253. package/lib/quoxon/session.js +172 -2
  254. package/lib/quoxon/sessionDead.js +17 -0
  255. package/lib/quoxon/signin.js +125 -0
  256. package/lib/quoxon/slash/agents.js +112 -0
  257. package/lib/quoxon/slash/argSources.js +65 -0
  258. package/lib/quoxon/slash/args.js +65 -0
  259. package/lib/quoxon/slash/automate.js +89 -0
  260. package/lib/quoxon/slash/broadcast.js +140 -0
  261. package/lib/quoxon/slash/broadcastRun.js +108 -0
  262. package/lib/quoxon/slash/card.js +30 -0
  263. package/lib/quoxon/slash/challenge.js +114 -0
  264. package/lib/quoxon/slash/cliCommand.js +53 -0
  265. package/lib/quoxon/slash/cliLeaves.json +948 -0
  266. package/lib/quoxon/slash/compact.js +1 -1
  267. package/lib/quoxon/slash/confirm.js +30 -0
  268. package/lib/quoxon/slash/crystallize.js +69 -0
  269. package/lib/quoxon/slash/dashboard.js +136 -0
  270. package/lib/quoxon/slash/draft.js +54 -0
  271. package/lib/quoxon/slash/hatWalk.js +206 -0
  272. package/lib/quoxon/slash/inverses.js +77 -0
  273. package/lib/quoxon/slash/killer.js +24 -0
  274. package/lib/quoxon/slash/killerApi.js +16 -0
  275. package/lib/quoxon/slash/ledger.js +13 -8
  276. package/lib/quoxon/slash/listView.js +3 -1
  277. package/lib/quoxon/slash/modelCmd.js +261 -0
  278. package/lib/quoxon/slash/palette.js +21 -7
  279. package/lib/quoxon/slash/paletteController.js +117 -7
  280. package/lib/quoxon/slash/panels.js +21 -6
  281. package/lib/quoxon/slash/picker.js +20 -2
  282. package/lib/quoxon/slash/restart.js +71 -13
  283. package/lib/quoxon/slash/rowCmd.js +157 -0
  284. package/lib/quoxon/slash/rowWalk.js +175 -0
  285. package/lib/quoxon/slash/settings.js +16 -6
  286. package/lib/quoxon/slash/spawn.js +177 -0
  287. package/lib/quoxon/slash/stepUp.js +274 -0
  288. package/lib/quoxon/slash/stepUpCard.js +60 -0
  289. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  290. package/lib/quoxon/slash/undoApi.js +38 -0
  291. package/lib/quoxon/slash/undoCmd.js +49 -0
  292. package/lib/quoxon/slash/undoWish.js +15 -0
  293. package/lib/quoxon/slash/watch.js +164 -0
  294. package/lib/quoxon/slash/wish.js +23 -0
  295. package/lib/quoxon/slash/wishCmd.js +32 -0
  296. package/lib/quoxon/slotChat.js +300 -0
  297. package/lib/quoxon/slotFormat.js +195 -0
  298. package/lib/quoxon/slotRun.js +131 -0
  299. package/lib/quoxon/sound/mapping.js +2 -1
  300. package/lib/quoxon/stray.js +22 -0
  301. package/lib/quoxon/suggestCommand.js +59 -0
  302. package/lib/quoxon/term.js +46 -11
  303. package/lib/quoxon/textBuffer.js +176 -2
  304. package/lib/quoxon/theme.js +58 -25
  305. package/lib/quoxon/tips.js +433 -0
  306. package/lib/quoxon/toolError.js +41 -0
  307. package/lib/quoxon/transcript.js +42 -4
  308. package/lib/quoxon/web/qface.js +18 -10
  309. package/lib/quoxon/welcome.js +1 -1
  310. package/lib/quoxon/whatsnew.js +136 -0
  311. package/lib/quoxon/whatsnew.jsonl +89 -0
  312. package/lib/quoxon/windowOrg.js +113 -0
  313. package/lib/server/diag.js +129 -0
  314. package/lib/server/doctor.js +71 -0
  315. package/lib/server/install.js +149 -0
  316. package/lib/server/pluginEnable.js +161 -0
  317. package/lib/server/skew.js +56 -0
  318. package/lib/server/snapshot.js +131 -0
  319. package/lib/server/status.js +128 -0
  320. package/lib/server/sys.js +65 -0
  321. package/lib/server/uninstall.js +63 -0
  322. package/lib/server/update.js +234 -0
  323. package/lib/toolOutputShape.js +315 -0
  324. package/lib/tui/views/HyperionDeck.jsx +79 -0
  325. package/lib/tui/views/LucidDeck.jsx +209 -217
  326. package/package.json +4 -1
  327. package/lib/core/websiteSession.js +0 -148
@@ -18,7 +18,135 @@ const UNDO_LIMIT = 100;
18
18
  const PASTE_RE = /\[Pasted text #(\d+) \+\d+ lines\]/g;
19
19
 
20
20
  export function initialBuffer(text = '') {
21
- return { text, cursor: text.length, undo: [], redo: [], pastes: {}, pasteSeq: 0 };
21
+ return { text, cursor: text.length, anchor: null, undo: [], redo: [], pastes: {}, pasteSeq: 0 };
22
+ }
23
+
24
+ /**
25
+ * QUOXON-MOUSE: the composer selection, { from, to } (from < to), or null. A selection is the range between
26
+ * `anchor` (where a drag started) and the cursor; only the mouse sets an anchor.
27
+ */
28
+ export function selection(s) {
29
+ if (s?.anchor == null || s.anchor === s.cursor) return null;
30
+ return { from: Math.min(s.anchor, s.cursor), to: Math.max(s.anchor, s.cursor) };
31
+ }
32
+
33
+ /** The word under `idx` for a double-click: a run of non-space characters, or the one space character itself. */
34
+ export function wordAt(text, idx) {
35
+ const t = String(text);
36
+ const i = Math.max(0, Math.min(idx, t.length - 1));
37
+ if (!t.length) return { from: 0, to: 0 };
38
+ if (/\s/.test(t[i])) return { from: i, to: nextBoundary(t, i) };
39
+ let a = i; let b = i;
40
+ while (a > 0 && !/\s/.test(t[a - 1])) a--;
41
+ while (b < t.length && !/\s/.test(t[b])) b++;
42
+ return { from: a, to: b };
43
+ }
44
+
45
+ /** Snap an index onto a grapheme boundary (at or before it). */
46
+ function snap(text, idx) {
47
+ const i = Math.max(0, Math.min(Number(idx) || 0, text.length));
48
+ return i === text.length ? i : prevBoundary(text, i + 1);
49
+ }
50
+
51
+ /** The selected range widened so it never leaves half a paste placeholder behind. */
52
+ function selRange(s, sel) {
53
+ let { from, to } = sel;
54
+ PASTE_RE.lastIndex = 0;
55
+ let m;
56
+ while ((m = PASTE_RE.exec(s.text))) {
57
+ const a = m.index, b = a + m[0].length;
58
+ if (to > a && from < b) { from = Math.min(from, a); to = Math.max(to, b); }
59
+ }
60
+ return { from, to };
61
+ }
62
+
63
+ const EDITS = new Set(['insert', 'newline', 'paste', 'backspace', 'delete', 'deleteWordBack', 'killLineBack', 'killLineForward', 'deleteWordForward']);
64
+
65
+ /** COMPOSER-EDIT: the selected text, paste placeholders expanded (what Shift selection copies). '' when none. */
66
+ export function selectedText(s) {
67
+ const sel = selection(s);
68
+ if (!sel) return '';
69
+ const { from, to } = selRange(s, sel);
70
+ return expandPastes({ text: s.text.slice(from, to), pastes: s.pastes || {} });
71
+ }
72
+
73
+ // ---- COMPOSER-EDIT: words and soft-wrapped rows ------------------------------------------------
74
+ const WORD = /[\p{L}\p{N}_]/u;
75
+ /** Readline backward-word: skip non-word characters, then the word. */
76
+ function wordLeftIndex(t, i) {
77
+ while (i > 0 && !WORD.test(t[i - 1])) i--;
78
+ while (i > 0 && WORD.test(t[i - 1])) i--;
79
+ return i;
80
+ }
81
+ /** Readline forward-word: skip non-word characters, then the word (lands at its end). */
82
+ function wordRightIndex(t, i) {
83
+ while (i < t.length && !WORD.test(t[i])) i++;
84
+ while (i < t.length && WORD.test(t[i])) i++;
85
+ return i;
86
+ }
87
+ /** Remove [from, to), widened so no paste placeholder is left half cut. */
88
+ function cutRange(s, from, to) {
89
+ if (from >= to) return s;
90
+ const r = selRange(s, { from, to });
91
+ return commit(s, { text: s.text.slice(0, r.from) + s.text.slice(r.to), cursor: r.from });
92
+ }
93
+
94
+ const cellW = (g) => (/[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/u.test(g) || (/\p{Extended_Pictographic}/u.test(g) && (g.codePointAt(0) >= 0x1f000 || g.includes('\uFE0F'))) ? 2 : 1);
95
+ /**
96
+ * The draft as the rows the terminal shows at `width` cells: [{ start, end }] (UTF-16 indices, end exclusive,
97
+ * newline not included). Word wrap like Ink's: a word that does not fit starts a new row, spaces hang at the row
98
+ * end, and a word longer than a row is split hard.
99
+ */
100
+ export function visualRows(text, width) {
101
+ const out = [];
102
+ let o = 0;
103
+ for (const line of String(text).split('\n')) {
104
+ if (!(width > 0) || !line.length) { out.push({ start: o, end: o + line.length }); o += line.length + 1; continue; }
105
+ const tokens = line.match(/\s+|\S+/g);
106
+ let rowStart = o; let w = 0; let p = o;
107
+ for (const tok of tokens) {
108
+ const gs = graphemes(tok);
109
+ const tw = gs.reduce((n, g) => n + cellW(g.text), 0);
110
+ if (/^\s/.test(tok) || w + tw <= width) { w += tw; p += tok.length; continue; }
111
+ if (tw <= width) { out.push({ start: rowStart, end: p }); rowStart = p; w = tw; p += tok.length; continue; }
112
+ for (const g of gs) { // longer than a row: split it hard
113
+ const cw = cellW(g.text);
114
+ if (w + cw > width && w > 0) { out.push({ start: rowStart, end: p }); rowStart = p; w = 0; }
115
+ w += cw; p += g.text.length;
116
+ }
117
+ }
118
+ out.push({ start: rowStart, end: o + line.length });
119
+ o += line.length + 1;
120
+ }
121
+ return out;
122
+ }
123
+ /** Index of the visual row holding `cursor`. A cursor at a soft break sits at the start of the next row. */
124
+ function rowOf(rows, cursor) {
125
+ for (let i = rows.length - 1; i >= 0; i--) if (cursor >= rows[i].start) return i;
126
+ return 0;
127
+ }
128
+ /** COMPOSER-EDIT: is the cursor on the first (`up`) or last (`down`) row the terminal shows? */
129
+ export function atVisualEdge(s, dir, width) {
130
+ if (!(width > 0)) {
131
+ return dir === 'up' ? !s.text.slice(0, s.cursor).includes('\n') : !s.text.slice(s.cursor).includes('\n');
132
+ }
133
+ const rows = visualRows(s.text, width);
134
+ const r = rowOf(rows, s.cursor);
135
+ return dir === 'up' ? r === 0 : r === rows.length - 1;
136
+ }
137
+ function moveVisual(s, dir, width) {
138
+ const rows = visualRows(s.text, width);
139
+ const r = rowOf(rows, s.cursor);
140
+ const t = dir === 'up' ? r - 1 : r + 1;
141
+ if (t < 0 || t >= rows.length) return s;
142
+ const col = graphemes(s.text.slice(rows[r].start, s.cursor)).reduce((n, g) => n + cellW(g.text), 0);
143
+ const row = rows[t];
144
+ const soft = t + 1 < rows.length && rows[t + 1].start === row.end; // the row breaks softly: its end is the next row's start
145
+ const gs = graphemes(s.text.slice(row.start, row.end));
146
+ let at = 0; let idx = row.end;
147
+ for (const g of gs) { const w = cellW(g.text); if (col < at + w) { idx = row.start + g.index; break; } at += w; }
148
+ if (soft && idx >= row.end) idx = prevBoundary(s.text, row.end);
149
+ return { ...s, cursor: idx };
22
150
  }
23
151
 
24
152
  export function graphemes(text) {
@@ -86,6 +214,14 @@ function indexAtLineCol(text, line, col) {
86
214
  return start + gs[col].index;
87
215
  }
88
216
 
217
+ /** First logical line the composer shows (it shows at most `maxLines`, keeping the cursor line in view). */
218
+ export function composerFrom(buf, maxLines = 8) {
219
+ const n = buf.text.split('\n').length;
220
+ if (n <= maxLines) return 0;
221
+ const { line } = lineCol(buf.text, buf.cursor);
222
+ return Math.min(Math.max(0, line - Math.floor(maxLines / 2)), n - maxLines);
223
+ }
224
+
89
225
  export function normalizeNewlines(t) { return String(t).replace(/\r\n?/g, '\n'); }
90
226
 
91
227
  /** Build the collapsed form of a paste, or null when it is small enough to inline. */
@@ -94,7 +230,36 @@ export function shouldCollapse(text) {
94
230
  return text.length > PASTE_CHAR_LIMIT || lines > PASTE_LINE_LIMIT;
95
231
  }
96
232
 
97
- export function bufferReducer(s, a) {
233
+ export function bufferReducer(s0, a) {
234
+ const sel = selection(s0);
235
+ const s = s0.anchor == null ? s0 : { ...s0, anchor: null };
236
+ if (sel && EDITS.has(a.type)) {
237
+ // QUOXON-MOUSE: Backspace/Delete remove the selection; typing or pasting replaces it. One undo step.
238
+ const { from, to } = selRange(s, sel);
239
+ const cut = { ...s, text: s.text.slice(0, from) + s.text.slice(to), cursor: from };
240
+ if (a.type !== 'insert' && a.type !== 'newline' && a.type !== 'paste') return commit(s, { text: cut.text, cursor: from });
241
+ const next = baseReducer({ ...cut, undo: [] }, a);
242
+ return commit(s, { text: next.text, cursor: next.cursor, pastes: next.pastes, pasteSeq: next.pasteSeq });
243
+ }
244
+ if (sel && a.type === 'left') return { ...s, cursor: sel.from };
245
+ if (sel && a.type === 'right') return { ...s, cursor: sel.to };
246
+ switch (a.type) {
247
+ case 'extend': { // COMPOSER-EDIT: Shift+movement grows the selection from where it started
248
+ const anchor = s0.anchor ?? s0.cursor;
249
+ const moved = baseReducer({ ...s, anchor: null }, { type: a.move, width: a.width });
250
+ return { ...s, cursor: moved.cursor, anchor: moved.cursor === anchor ? null : anchor };
251
+ }
252
+ case 'selectAll': return s.text ? { ...s, anchor: 0, cursor: s.text.length } : s;
253
+ case 'place': return { ...s, cursor: snap(s.text, a.index) };
254
+ case 'select': {
255
+ const anchor = snap(s.text, a.anchor); const cursor = snap(s.text, a.cursor);
256
+ return { ...s, anchor: anchor === cursor ? null : anchor, cursor };
257
+ }
258
+ default: return baseReducer(s, a);
259
+ }
260
+ }
261
+
262
+ function baseReducer(s, a) {
98
263
  switch (a.type) {
99
264
  case 'insert': {
100
265
  const t = normalizeNewlines(a.text);
@@ -144,6 +309,14 @@ export function bufferReducer(s, a) {
144
309
  if (from === s.cursor) return s;
145
310
  return commit(s, { text: s.text.slice(0, from) + s.text.slice(s.cursor), cursor: from });
146
311
  }
312
+ case 'wordLeft': return { ...s, cursor: wordLeftIndex(s.text, s.cursor) };
313
+ case 'wordRight': return { ...s, cursor: wordRightIndex(s.text, s.cursor) };
314
+ case 'killLineForward': { // Ctrl+K: to the end of the line; at the end, join the next line
315
+ const nl = s.text.indexOf('\n', s.cursor);
316
+ const end = nl === -1 ? s.text.length : (nl === s.cursor ? nl + 1 : nl);
317
+ return cutRange(s, s.cursor, end);
318
+ }
319
+ case 'deleteWordForward': return cutRange(s, s.cursor, wordRightIndex(s.text, s.cursor)); // Alt+D
147
320
  case 'left': return { ...s, cursor: prevBoundary(s.text, s.cursor) };
148
321
  case 'right': return { ...s, cursor: nextBoundary(s.text, s.cursor) };
149
322
  case 'home': return { ...s, cursor: lineCol(s.text, s.cursor).start };
@@ -152,6 +325,7 @@ export function bufferReducer(s, a) {
152
325
  return { ...s, cursor: nl === -1 ? s.text.length : nl };
153
326
  }
154
327
  case 'up': case 'down': {
328
+ if (a.width > 0) return moveVisual(s, a.type, a.width); // COMPOSER-EDIT: through soft-wrapped rows
155
329
  const { line, col } = lineCol(s.text, s.cursor);
156
330
  const target = a.type === 'up' ? line - 1 : line + 1;
157
331
  const count = s.text.split('\n').length;
@@ -8,6 +8,7 @@
8
8
  * Purple is for keys and accents only, never whole sentences.
9
9
  */
10
10
  import { graphemes } from "./textBuffer.js";
11
+ import { SEMANTIC_DARK, SEMANTIC_LIGHT, stripMarks, MARK } from './palette.js';
11
12
 
12
13
  export const MIN_COLS = 40;
13
14
 
@@ -20,14 +21,18 @@ export const DARK = {
20
21
  value: ['#f1f5f9', 255],
21
22
  number: ['#38bdf8', 75],
22
23
  link: ['#38bdf8', 75],
23
- code: ['#06b6d4', 38], codeBg: ['#1f2937', 235], codeRule: ['#155e75', 24],
24
- key: ['#a855f7', 135],
25
- muted: ['#7d8594', 245],
24
+ code: ['#2dd4bf', 80], codeBg: ['#1f2937', 235], codeRule: ['#155e75', 24],
25
+ key: ['#2dd4bf', 80], // a thing you type: the command colour (purple is brand chrome only)
26
+ muted: ['#bcc8e8', 189],
26
27
  ok: ['#4ade80', 78], warn: ['#f59e0b', 214], error: ['#f87171', 210],
28
+ ask: ['#f472b6', 211], // QUOXON-ASK: "needs you" (every y/N and approval): glyph and keys, bold line
29
+ gold: ['#d4af37', 178], // the authorisation moment: step-up PIN card frame, filled dots, human-present countdown
27
30
  // legacy names, kept so existing call sites read the same
28
31
  sky: ['#38bdf8', 75], violet: ['#a855f7', 135], indigo: ['#818cf8', 105], teal: ['#06b6d4', 38],
29
- fail: ['#f87171', 210], dim: ['#7d8594', 245], faint: ['#4b5563', 240],
30
- text: ['#cbd5e1', 252], input: ['#ffffff', 231], placeholder: ['#9ca3af', 248]
32
+ fail: ['#f87171', 210], dim: ['#bcc8e8', 189], faint: ['#7c8fe0', 105],
33
+ text: ['#f1f5f9', 255], input: ['#ffffff', 231], placeholder: ['#bcc8e8', 189],
34
+ // semantic roles (palette.js)
35
+ prose: SEMANTIC_DARK.prose, command: SEMANTIC_DARK.command, id: SEMANTIC_DARK.value, brand: SEMANTIC_DARK.brand, quiet: SEMANTIC_DARK.quiet, rule: SEMANTIC_DARK.rule
31
36
  };
32
37
  export const LIGHT = {
33
38
  heading: ['#0369a1', 25],
@@ -37,16 +42,19 @@ export const LIGHT = {
37
42
  value: ['#0f172a', 234],
38
43
  number: ['#0369a1', 25],
39
44
  link: ['#0369a1', 25],
40
- code: ['#0e7490', 30], codeBg: ['#ecfeff', 195], codeRule: ['#67e8f9', 117],
41
- key: ['#7e22ce', 91],
42
- muted: ['#4b5563', 240],
45
+ code: ['#0f766e', 30], codeBg: ['#ecfeff', 195], codeRule: ['#67e8f9', 117],
46
+ key: ['#0f766e', 30],
47
+ muted: ['#3b4f87', 60],
43
48
  ok: ['#15803d', 28], warn: ['#b45309', 130], error: ['#b91c1c', 124],
49
+ ask: ['#be185d', 161],
50
+ gold: ['#8a6500', 94],
44
51
  sky: ['#0369a1', 25], violet: ['#7e22ce', 91], indigo: ['#4338ca', 62], teal: ['#0e7490', 30],
45
- fail: ['#b91c1c', 124], dim: ['#4b5563', 240], faint: ['#9ca3af', 248],
46
- text: ['#334155', 238], input: ['#0f172a', 234], placeholder: ['#6b7280', 243]
52
+ fail: ['#b91c1c', 124], dim: ['#3b4f87', 60], faint: ['#7280c8', 104],
53
+ text: ['#0f172a', 234], input: ['#0f172a', 234], placeholder: ['#3b4f87', 60],
54
+ prose: SEMANTIC_LIGHT.prose, command: SEMANTIC_LIGHT.command, id: SEMANTIC_LIGHT.value, brand: SEMANTIC_LIGHT.brand, quiet: SEMANTIC_LIGHT.quiet, rule: SEMANTIC_LIGHT.rule
47
55
  };
48
56
  /** Roles a reader must be able to read: each is held to >= 4.5:1 on its background (tests). */
49
- export const ROLES = ['heading', 'icon', 'bullet1', 'bullet2', 'bullet3', 'label', 'value', 'number', 'link', 'code', 'key', 'muted', 'ok', 'warn', 'error', 'text', 'placeholder'];
57
+ export const ROLES = ['heading', 'icon', 'bullet1', 'bullet2', 'bullet3', 'label', 'value', 'number', 'link', 'code', 'key', 'muted', 'ok', 'warn', 'error', 'ask', 'gold', 'text', 'placeholder', 'prose', 'command', 'id', 'brand', 'quiet'];
50
58
 
51
59
  /** Heading gradient stops across the word: sky -> indigo -> violet (light: the -700 shades). */
52
60
  export const HEADING_STOPS = { dark: ['#38bdf8', '#6366f1', '#a855f7'], light: ['#0369a1', '#4338ca', '#7e22ce'] };
@@ -59,12 +67,12 @@ export const AGENT_SHADES = {
59
67
  /** Model providers in their brand colours (dark, light). Unknown = muted. */
60
68
  export const PROVIDER_COLORS = {
61
69
  openai: ['#f5f5f5', '#111111'], anthropic: ['#D97757', '#b4532e'], google: ['#4796E3', '#1a62c4'],
62
- ollama: ['#e5e7eb', '#4b5563'], mistral: ['#FA520F', '#c2410c'], deepseek: ['#4D6BFE', '#2f4fe0'],
70
+ ollama: ['#e5e7eb', '#3b4f87'], mistral: ['#FA520F', '#c2410c'], deepseek: ['#4D6BFE', '#2f4fe0'],
63
71
  perplexity: ['#20808D', '#1a6b76'], xai: ['#f5f5f5', '#111111'],
64
72
  // B5: OpenAI-compatible presets (dark bg shade, light bg shade; all >= 4.5:1 on their background)
65
73
  openrouter: ['#6566F1', '#4f46e5'], huggingface: ['#FFD21E', '#8a6500'], groq: ['#F55036', '#c2290f'],
66
74
  together: ['#0F6FFF', '#0b57d0'], lmstudio: ['#a78bfa', '#6d28d9'], vllm: ['#30A2FF', '#0b6bb8'],
67
- openai_compatible: ['#9ca3af', '#4b5563']
75
+ openai_compatible: ['#c7d2fe', '#3b4f87']
68
76
  };
69
77
  /** Protocol trace marks (AEE, AOCL, VOLT, WARD), each its own Q shade (dark, light). */
70
78
  export const PROTO_COLORS = {
@@ -187,21 +195,21 @@ export const bulletRole = (depth) => ['bullet1', 'bullet2', 'bullet3'][((depth %
187
195
 
188
196
  // ---- paint: split a plain line into coloured runs; the characters never change ---------------------
189
197
  const GLYPH_ROLE = {
190
- '●': 'ok', '✔': 'ok', '◐': 'warn', '○': 'muted', '?': 'muted', '✖': 'error', '⚠': 'warn', '⊘': 'warn',
191
- '◉': 'icon', 'Ⓠ': 'icon', '▸': 'key', '⛔': 'warn', '↳': 'icon', '»': 'muted', '·': 'muted', '⬢': 'icon', '⬡': 'muted'
198
+ '●': 'ok', '✔': 'ok', '◐': 'warn', '○': 'quiet', '?': 'quiet', '✖': 'error', '⚠': 'warn', '⊘': 'warn',
199
+ '◉': 'icon', 'Ⓠ': 'icon', '▸': 'brand', '⛔': 'warn', '↳': 'icon', '»': 'quiet', '·': 'quiet', '⬢': 'icon', '⬡': 'quiet', '▶': 'ask'
192
200
  };
193
201
  const KEYS = '(?:(?:Ctrl|Shift|Alt|Cmd|Option)\\+\\S+|Enter|Tab|Esc|Up|Down|Left|Right|Space|Backspace|PgUp|PgDn)';
194
202
  const TOKEN = new RegExp([
195
203
  '(https?:\\/\\/[^\\s)]+)', // 1 link
196
204
  '(\\bquox(?: (?![·])[^\\s·,;)]+)+)', // 2 a quox command
197
- '((?<![\\w/])\\/[a-z][\\w-]*)', // 3 slash command
205
+ '((?<![\\w/])\\/[a-z][\\w-]*(?: (?:--?[a-z][\\w-]*|<[^>\\s]+>|\\[[^\\]\\s]+\\]|\\d+|[\\w-]+(?:\\|[\\w-]+)+))*)', // 3 slash command + its flags/args
198
206
  `((?<![\\w+])${KEYS}(?![\\w+]))`, // 4 keyboard key
199
207
  '((?<![\\w<])<[^>\\s]+>|\\[[^\\]\\s]+\\])', // 5 placeholder <name> or [args]
200
208
  '( · )', // 6 separator
201
209
  '((?<![\\w/.-])(?:(?:gpt|claude|gemini|o[134]|mistral|codestral|deepseek|sonar|grok)-[\\w.:-]*\\w|ollama:[\\w.:-]*\\w|OpenAI|openai|Anthropic|anthropic|Claude(?: API)?|Gemini|Ollama|ollama|Mistral|DeepSeek|Perplexity|Grok|xAI)(?![\\w-]))', // 7 provider/model
202
210
  '((?<![\\w.#-])\\d+(?:[.,:-]\\d+)*(?:%|ms|s|k|m|h|d|x|KB|MB|GB)?(?![\\w]))' // 8 number
203
211
  ].join('|'), 'g');
204
- const ROLE_OF = [null, 'link', 'code', 'key', 'key', 'muted', 'muted', 'provider', 'number'];
212
+ const ROLE_OF = [null, 'id', 'command', 'command', 'command', 'command', 'quiet', 'provider', 'id'];
205
213
  const LETTER_KEY = /^([a-z])(?= (?:approve|deny|open|stop|peek|confirm|edit|retry|cancel|copy|quit)\b)/;
206
214
 
207
215
  /** Runs of `text` with roles; `base` colours the prose between tokens. Pure. */
@@ -211,6 +219,16 @@ export function paintRuns(text, base) {
211
219
  const s = String(text ?? '');
212
220
  let i = 0;
213
221
  const startsSeg = (at) => at === 0 || s.slice(0, at).endsWith(' · ') || /^\s*$/.test(s.slice(0, at));
222
+ if (/[⟦⟪]/.test(s)) { // explicit markup: `command` and «value» are atomic spans, the rest is auto-tokenised
223
+ let j = 0;
224
+ for (const m of s.matchAll(MARK)) {
225
+ for (const r of paintRuns(s.slice(j, m.index), base)) push(r.text, r.role);
226
+ push(m[1] ?? m[2], m[1] !== undefined ? 'command' : 'id');
227
+ j = m.index + m[0].length;
228
+ }
229
+ for (const r of paintRuns(s.slice(j), base)) push(r.text, r.role);
230
+ return out;
231
+ }
214
232
  for (const m of s.matchAll(TOKEN)) {
215
233
  proseRuns(s.slice(i, m.index), base, push, startsSeg(i));
216
234
  const g = m.findIndex((x, k) => k > 0 && x !== undefined);
@@ -222,32 +240,47 @@ export function paintRuns(text, base) {
222
240
  }
223
241
  function proseRuns(seg, base, push, segStart) {
224
242
  const k = segStart && seg.match(LETTER_KEY);
225
- if (k) { push(k[1], 'key'); seg = seg.slice(1); }
243
+ if (k) { push(k[1], 'command'); seg = seg.slice(1); }
226
244
  push(seg, base);
227
245
  }
228
246
 
229
247
  /**
230
248
  * A note/list line as runs: a leading state glyph, then `Label value` (two spaces) or `label: value`,
231
- * then tokens. tone: undefined/'dim' (neutral), 'ok', 'warn', 'accent', 'error'.
249
+ * then tokens. tone: undefined/'dim' (neutral), 'ok', 'warn', 'accent', 'error', 'ask' (a question for the human).
232
250
  */
233
251
  export function paintLine(text, tone) {
234
252
  const s = String(text ?? '');
235
253
  const out = [];
236
- const neutral = !tone || tone === 'dim';
237
- const prose = tone === 'warn' ? 'warn' : tone === 'error' || tone === 'fail' ? 'error' : tone === 'accent' ? 'sky' : null;
238
- const m = s.match(/^(\s*)(?:([●✔◐○?✖⚠⊘◉Ⓠ▸⛔↳»·⬢⬡])( ))?(.*)$/u);
254
+ // The tone colours the GLYPH only. The sentence stays prose, so commands and values inside keep their roles.
255
+ const TONE_GLYPH = { warn: 'warn', error: 'error', fail: 'error', ok: 'ok', accent: 'brand', ask: 'ask' };
256
+ // [\s\S]* not .*: a notice can carry a newline (a server error body), and a failed match crashed the TUI
257
+ // ("m is not iterable", 2026-10-15 03:44 while the collector was down).
258
+ const m = s.match(/^(\s*)(?:([●✔◐○?✖⚠⊘◉Ⓠ▸⛔↳»·⬢⬡▶])( ))?([\s\S]*)$/u) || [s, '', undefined, undefined, s];
239
259
  const [, lead, glyph, gsp, rest0] = m;
240
260
  if (lead) out.push({ text: lead, role: undefined });
241
- if (glyph) out.push({ text: glyph, role: tone === 'warn' && glyph !== '○' ? 'warn' : GLYPH_ROLE[glyph] }, { text: gsp, role: undefined });
261
+ if (glyph) out.push({ text: glyph, role: ['ok', 'warn', 'error'].includes(GLYPH_ROLE[glyph]) ? GLYPH_ROLE[glyph] : (TONE_GLYPH[tone] || GLYPH_ROLE[glyph]) }, { text: gsp, role: undefined });
242
262
  let rest = rest0;
263
+ if (tone === 'ask') { // QUOXON-ASK: "▶ reason. question? keys": the keys after the last "? " are the ask colour
264
+ const q = rest.lastIndexOf('? ');
265
+ if (q >= 0) { out.push(...paintRuns(rest.slice(0, q + 2), 'prose'), ...paintRuns(rest.slice(q + 2), 'ask')); return out; }
266
+ out.push(...paintRuns(rest, 'prose')); return out;
267
+ }
243
268
  const lv = rest.match(/^(\S[^·]{0,30}?\S|\S)( {2,})(\S.*)$/) || rest.match(/^([A-Za-z][\w .-]{0,22}:)( )(\S.*)$/);
244
- let valueBase = prose || (tone === 'ok' && !glyph ? 'ok' : neutral && !lv && !glyph ? 'muted' : 'text');
269
+ const valueBase = 'prose';
245
270
  if (lv && !/^https?:/.test(lv[1])) {
246
271
  const pv = providerOf(lv[1].replace(/\s*\(.*\)$/, ''), null);
247
272
  out.push({ text: lv[1], role: pv ? `provider:${pv}` : 'label' }, { text: lv[2], role: undefined });
248
273
  rest = lv[3];
249
- if (!prose) valueBase = 'value';
250
274
  }
251
275
  out.push(...paintRuns(rest, valueBase));
252
276
  return out;
253
277
  }
278
+
279
+ /** The glyph a bare notice line gets so its tone shows without tinting the sentence. '' for neutral tones. */
280
+ export const TONE_PREFIX = { warn: '⚠ ', error: '✖ ', fail: '✖ ', ok: '✔ ' };
281
+ export const toneText = (text, tone) => {
282
+ const p = TONE_PREFIX[tone];
283
+ const t = String(text ?? '');
284
+ return p && !/^\s*[●✔◐○?✖⚠⊘◉Ⓠ▸⛔↳»·⬢⬡▶] /u.test(t) ? p + t : t;
285
+ };
286
+ export { stripMarks };