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
@@ -13,7 +13,8 @@ export function readUsage(u) {
13
13
  const cacheRead = num(o.cache_read_input_tokens);
14
14
  const cacheWrite = num(o.cache_creation_input_tokens);
15
15
  const cost = num(o.cost_usd);
16
- return { input, output, cacheRead, cacheWrite, cost, reported: input != null || output != null };
16
+ const costSource = typeof o.cost_source === 'string' ? o.cost_source : null; // 'claude-cli' = that CLI's own estimate
17
+ return { input, output, cacheRead, cacheWrite, cost, costSource, reported: input != null || output != null };
17
18
  }
18
19
 
19
20
  export function createLedger() {
@@ -23,9 +24,10 @@ export function createLedger() {
23
24
  const finalize = (usage, convId) => {
24
25
  if (!turn) return;
25
26
  const t = turn; turn = null;
26
- const u = readUsage(usage ?? t.doneUsage);
27
+ const raw = usage ?? t.doneUsage;
28
+ const u = readUsage(t.viaCli && raw && !raw.cost_source ? { ...raw, cost_source: 'claude-cli' } : raw); // the claude CLI path, from a server that does not label it yet
27
29
  const key = `${t.provider || '?'}|${t.model || '?'}`;
28
- const r = models.get(key) || { provider: t.provider, model: t.model, turns: 0, reported: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, costTurns: 0 };
30
+ const r = models.get(key) || { provider: t.provider, model: t.model, turns: 0, reported: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, costTurns: 0, estimate: 0, estimateTurns: 0 };
29
31
  r.turns++;
30
32
  if (u.reported) {
31
33
  r.reported++;
@@ -33,14 +35,15 @@ export function createLedger() {
33
35
  const id = convId || t.conversationId;
34
36
  if (id) convs.set(id, (convs.get(id) || 0) + (u.input || 0) + (u.output || 0));
35
37
  }
36
- if (u.cost != null) { r.cost += u.cost; r.costTurns++; }
38
+ if (u.cost != null && u.costSource) { r.estimate += u.cost; r.estimateTurns++; } // reported by a tool, not a price
39
+ else if (u.cost != null) { r.cost += u.cost; r.costTurns++; }
37
40
  models.set(key, r);
38
41
  };
39
42
  return {
40
43
  observe(ev) {
41
44
  if (!ev || typeof ev !== 'object') return;
42
45
  if (ev.type === 'turn.started') turn = { provider: ev.provider || null, model: ev.model || null, conversationId: ev.conversation_id || null, doneUsage: null };
43
- else if (ev.type === 'done' && turn) turn.doneUsage = ev.usage || (ev.cost_usd != null ? { cost_usd: ev.cost_usd } : null);
46
+ else if (ev.type === 'done' && turn) { turn.doneUsage = ev.usage || (ev.cost_usd != null ? { cost_usd: ev.cost_usd } : null); turn.viaCli = Boolean(ev.cli_session_id); }
44
47
  else if (ev.type === 'turn.finished') finalize(ev.usage, ev.conversation_id);
45
48
  },
46
49
  /** The stream closed: settle a turn whose turn.finished never came (legacy servers send only done). */
@@ -48,7 +51,7 @@ export function createLedger() {
48
51
  rows: () => [...models.values()],
49
52
  conversationTokens: (id) => (id ? convs.get(id) ?? 0 : 0),
50
53
  totals() {
51
- const t = { turns: 0, reported: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, costTurns: 0 };
54
+ const t = { turns: 0, reported: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, costTurns: 0, estimate: 0, estimateTurns: 0 };
52
55
  for (const r of models.values()) for (const k of Object.keys(t)) t[k] += r[k];
53
56
  return t;
54
57
  }
@@ -65,7 +68,8 @@ function line(r) {
65
68
  if (r.cacheRead || r.cacheWrite) parts.push(`cache ${n(r.cacheRead)} read / ${n(r.cacheWrite)} written`);
66
69
  }
67
70
  if (r.reported < r.turns) parts.push(`${r.turns - r.reported} without usage from the server`);
68
- parts.push(r.costTurns === r.turns && r.turns ? usd(r.cost) : r.costTurns ? `${usd(r.cost)} for ${r.costTurns} of ${r.turns} turns` : 'cost not reported');
71
+ if (r.costTurns || !r.estimateTurns) parts.push(r.costTurns === r.turns && r.turns ? usd(r.cost) : r.costTurns ? `${usd(r.cost)} for ${r.costTurns} of ${r.turns} turns` : 'cost not reported');
72
+ if (r.estimateTurns) parts.push(`${usd(r.estimate)} claude CLI's own estimate${r.estimateTurns < r.turns ? ` for ${r.estimateTurns} of ${r.turns} turns` : ''}`);
69
73
  return parts.join(' · ');
70
74
  }
71
75
 
@@ -77,7 +81,8 @@ export function costModel(ledger) {
77
81
  for (const r of rows) out.push({ kind: 'row', key: `${r.provider || '?'} ${r.model || ''}`.trim(), text: line(r) });
78
82
  const t = ledger.totals();
79
83
  if (rows.length > 1) out.push({ kind: 'row', key: 'Total', text: line(t) });
80
- if (t.costTurns < t.turns) out.push({ kind: 'dim', text: 'Cost is blank where the server sent tokens but no cost_usd for that provider; Quox does not guess prices.' });
84
+ if (t.costTurns + t.estimateTurns < t.turns) out.push({ kind: 'dim', text: 'Cost is blank where the server sent tokens but no cost_usd for that provider; Quox does not guess prices.' });
85
+ if (t.estimateTurns) out.push({ kind: 'dim', text: "The Claude subscription path shows what the claude CLI reports: its own API-equivalent estimate, passed through as is. It is not a bill." });
81
86
  if (t.reported < t.turns) out.push({ kind: 'dim', text: 'Some turns ended without usage (an older server, or a turn that failed or was interrupted).' });
82
87
  return out;
83
88
  }
@@ -10,7 +10,9 @@ import { c, clip } from '../theme.js';
10
10
  const h = React.createElement;
11
11
 
12
12
  export function ListView({ title = null, rows = [], sel = 0, cols = 80, hint = null, empty = null, more = 0 }) {
13
- const keyW = Math.min(22, Math.max(8, ...rows.map(r => [...String(r.key || '')].length)) + 1);
13
+ // The key column grows with the window (22 cells minimum, 32 at most) and always leaves a one-cell gap, so a name that fits is shown whole;
14
+ // one that does not gets the ellipsis from truncate-end, never a silent cut.
15
+ const keyW = Math.min(Math.max(22, Math.min(32, Math.floor(cols / 3))), Math.max(8, ...rows.map(r => [...String(r.key || '')].length)) + 1);
14
16
  const textW = Math.max(10, cols - keyW - 6);
15
17
  return h(Box, { flexDirection: 'column', paddingLeft: 1 },
16
18
  title ? h(Text, { color: c('muted'), bold: true }, clip(title, cols - 2)) : null,
@@ -0,0 +1,261 @@
1
+ /**
2
+ * QUOXON-FW6: the /model favourites, default and chooser behaviour, kept out of app.js.
3
+ *
4
+ * Favourites and the default live in the user's server-side preferences (api.modelPrefs and
5
+ * api.setModelPrefs); this module only holds the in-memory copy (`mp.prefs`) and calls the api to
6
+ * save. Provider parity: nothing here knows one provider from another; the connected list and the
7
+ * live model lists decide what is shown.
8
+ */
9
+ import {
10
+ CHOICES,
11
+ connectedChoices,
12
+ providerId,
13
+ choiceId,
14
+ modelLabel,
15
+ normChoice,
16
+ } from '../model.js';
17
+ import {
18
+ normPrefs,
19
+ addFav,
20
+ rmFav,
21
+ setDefault,
22
+ clearDefault,
23
+ toggleFav,
24
+ favLines,
25
+ liveModels,
26
+ checkModelId,
27
+ chooserItems,
28
+ chooserRow,
29
+ currentKey,
30
+ label,
31
+ } from '../modelPrefs.js';
32
+ import { clip } from '../theme.js';
33
+
34
+ /**
35
+ * deps: api, say, command, mc (ref to the stored choice), mp (ref { prefs, touched, warned }),
36
+ * picker, state (ref), bump, cols.
37
+ */
38
+ export function createModelCmd({ api, say, command, mc, mp, picker, state, bump, cols }) {
39
+ const byId = (id) => CHOICES.find((x) => x.id === id) || null;
40
+ const current = () => currentKey(mc.current, choiceId);
41
+
42
+ /** Optimistic: the in-memory copy changes at once; the server (user preferences) is the truth. */
43
+ async function save(next) {
44
+ mp.current.prefs = next;
45
+ bump();
46
+ if (!api.setModelPrefs) return true;
47
+ const r = await api.setModelPrefs(next);
48
+ if (!r.synced) {
49
+ say(
50
+ `Saved on this machine only (${r.error || 'the server did not take it'}). It is not on ` +
51
+ 'the server yet, so it will not follow you to other machines. ' +
52
+ 'Making the change again retries.',
53
+ 'warn',
54
+ );
55
+ }
56
+ return Boolean(r.synced);
57
+ }
58
+
59
+ async function addOrDefault(sub) {
60
+ const pid = providerId(sub.provider);
61
+ if (!pid) {
62
+ const ids = CHOICES.map((x) => x.id).join(', ');
63
+ return say(`No provider called "${sub.provider}". Use ${ids}.`, 'warn');
64
+ }
65
+ // New models appear before lists update: an id missing from the live list is kept, with a
66
+ // warning.
67
+ const live = await liveModels(api, pid);
68
+ const chk = checkModelId(live.ids, sub.model, byId(pid).name);
69
+ const prefs = mp.current.prefs;
70
+ const r =
71
+ sub.kind === 'fav-add' ? addFav(prefs, pid, chk.model) : setDefault(prefs, pid, chk.model);
72
+ if (r.added === false || r.ok === false) return say(r.text, r.tone);
73
+ await save(r.prefs);
74
+ say(clip(r.text, cols() - 4), r.tone);
75
+ if (chk.note) say(clip(chk.note, cols() - 4), 'warn');
76
+ return undefined;
77
+ }
78
+
79
+ /**
80
+ * Handles the favourites / default / chooser forms of /model.
81
+ * -> undefined when `sub` (parseModelSub) is not one of them (the caller carries on), else a
82
+ * promise.
83
+ */
84
+ function handle(sub) {
85
+ const prefs = mp.current.prefs;
86
+ switch (sub.kind) {
87
+ case 'error':
88
+ return Promise.resolve(say(sub.text, 'warn'));
89
+ case 'fav-list':
90
+ for (const [t, tone] of favLines(prefs, current())) say(clip(t, cols() - 4), tone);
91
+ return Promise.resolve();
92
+ case 'use': {
93
+ const f = prefs.favourites[sub.n - 1];
94
+ if (f) return command(`/model ${f.provider} ${f.model}`);
95
+ return Promise.resolve(say(`No favourite ${sub.n}. /model fav lists them.`, 'warn'));
96
+ }
97
+ case 'default-show':
98
+ return Promise.resolve(
99
+ say(
100
+ prefs.default
101
+ ? `Default model: ${label(prefs.default)}. New sessions start on it when its ` +
102
+ 'provider is connected. /model default clear removes it.'
103
+ : 'No default model. /model default <provider> <model> sets one ' +
104
+ '(or press d in the /model chooser).',
105
+ ),
106
+ );
107
+ case 'default-clear': {
108
+ const c = clearDefault(prefs);
109
+ return save(c.prefs).then(() => say(c.text, c.tone));
110
+ }
111
+ case 'fav-rm': {
112
+ const r = rmFav(prefs, sub.ref);
113
+ return (r.removed ? save(r.prefs) : Promise.resolve()).then(() => say(r.text, r.tone));
114
+ }
115
+ case 'fav-add':
116
+ case 'default-set':
117
+ return addOrDefault(sub);
118
+ case 'chooser':
119
+ return openChooser();
120
+ default:
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ /** The /model chooser: favourites first, then each CONNECTED provider's live models. */
126
+ async function openChooser() {
127
+ if (!api.connections) return say('Model choice is not available in this mode.', 'warn');
128
+ const [res, caps] = await Promise.all([
129
+ api.connections(),
130
+ api.capabilities ? api.capabilities() : null,
131
+ ]);
132
+ const conn = connectedChoices(res, caps);
133
+ if (!conn.length) {
134
+ return say(
135
+ res?.ok === false
136
+ ? `Could not read what is connected: ${res.error || 'no answer'}`
137
+ : 'No AI provider is connected yet. /connect openai, /connect anthropic or ' +
138
+ '/connect ollama <url>.',
139
+ 'warn',
140
+ );
141
+ }
142
+ const groups = await Promise.all(
143
+ conn.map(async (c) => {
144
+ const l = await liveModels(api, c.id);
145
+ const note = l.ok ? null : 'could not list its models; Enter uses the server default';
146
+ return { id: c.id, name: c.name, ids: l.ids.slice(0, 200), note };
147
+ }),
148
+ );
149
+ const items = () => chooserItems(mp.current.prefs, groups);
150
+ const hidden = () =>
151
+ mp.current.prefs.favourites.filter((f) => !groups.some((g) => g.id === f.provider)).length;
152
+ const title = () => {
153
+ const now = modelLabel(state.current.ctx, mc.current) || 'auto';
154
+ const def = mp.current.prefs.default ? label(mp.current.prefs.default) : 'none';
155
+ const h = hidden();
156
+ const note = h ? ` · ${h} favourite${h === 1 ? '' : 's'} hidden, provider not connected` : '';
157
+ return `Model · current ${now} · default ${def}${note}`;
158
+ };
159
+ picker.open({
160
+ titleFn: title,
161
+ rowsFn: () => items().map((it) => chooserRow(it, mp.current.prefs, current())),
162
+ hint: '↑↓ select · Enter use · f favourite · d default · Esc close',
163
+ onChar: (ch, sel) => {
164
+ if (ch !== 'f' && ch !== 'd') return undefined;
165
+ const it = items()[sel];
166
+ if (!it || !it.model) {
167
+ say('That row has no model to save. Pick a model row.', 'warn');
168
+ return true;
169
+ }
170
+ const m = { provider: it.provider, model: it.model };
171
+ if (ch === 'f') {
172
+ const t = toggleFav(mp.current.prefs, m);
173
+ save(t.prefs);
174
+ say(`${t.added ? 'Added to' : 'Removed from'} favourites: ${label(m)}.`, 'ok');
175
+ } else {
176
+ save(setDefault(mp.current.prefs, m.provider, m.model).prefs);
177
+ say(`Default model: ${label(m)}. New sessions start on it.`, 'ok');
178
+ }
179
+ const next = items();
180
+ const same = (x) =>
181
+ x.kind === it.kind && x.provider === it.provider && x.model === it.model;
182
+ const j = next.findIndex(same);
183
+ return j >= 0 ? j : Math.min(sel, Math.max(0, next.length - 1));
184
+ },
185
+ onPick: (_, i) => {
186
+ const it = items()[i];
187
+ if (it) command(it.model ? `/model ${it.provider} ${it.model}` : `/model ${it.provider}`);
188
+ },
189
+ });
190
+ return undefined;
191
+ }
192
+
193
+ /**
194
+ * New sessions start on the saved default model when its provider is connected. A --provider
195
+ * flag, or a /model typed first, wins. A default on a provider that is not connected is said
196
+ * once and nothing changes. -> a cancel function.
197
+ */
198
+ function startOnDefault({ flagged, off, dispatchCtx }) {
199
+ if (!api.modelPrefs || off) return () => {};
200
+ let dead = false;
201
+ (async () => {
202
+ let got = null;
203
+ try {
204
+ got = await api.modelPrefs();
205
+ } catch {
206
+ got = null;
207
+ }
208
+ if (dead || !got) return;
209
+ mp.current.prefs = normPrefs(got.prefs);
210
+ bump();
211
+ const def = mp.current.prefs.default;
212
+ if (!def || flagged || mp.current.touched) return;
213
+ let res = null;
214
+ let caps = null;
215
+ try {
216
+ [res, caps] = await Promise.all([
217
+ api.connections?.(),
218
+ api.capabilities ? api.capabilities() : null,
219
+ ]);
220
+ } catch {
221
+ return; // cannot tell what is connected: stay quiet, the model is unchanged
222
+ }
223
+ if (dead || mp.current.touched || !res || res.ok === false) return;
224
+ const row = byId(def.provider);
225
+ if (!row) return;
226
+ if (!connectedChoices(res, caps).some((c) => c.id === row.id)) {
227
+ if (!mp.current.warned) {
228
+ mp.current.warned = true;
229
+ say(
230
+ `Your default model ${label(def)} is on ${row.name}, which is not connected, so ` +
231
+ `the model is unchanged. /connect ${row.connect} to use it, or ` +
232
+ '/model default clear.',
233
+ 'warn',
234
+ );
235
+ }
236
+ return;
237
+ }
238
+ let host = null;
239
+ if (row.dynamic) {
240
+ try {
241
+ const lst =
242
+ row.id === 'ollama' ? await api.ollamaModels?.() : await api.compatModels?.(row.id);
243
+ host = lst?.host || null;
244
+ } catch {
245
+ host = null;
246
+ }
247
+ }
248
+ if (dead || mp.current.touched) return;
249
+ const via = row.via ? { via: row.via } : {};
250
+ const at = host && !row.hosted ? { host } : {};
251
+ mc.current = normChoice({ provider: row.provider, model: def.model, ...via, ...at });
252
+ dispatchCtx();
253
+ bump();
254
+ })();
255
+ return () => {
256
+ dead = true;
257
+ };
258
+ }
259
+
260
+ return { handle, openChooser, startOnDefault, save };
261
+ }
@@ -4,7 +4,7 @@
4
4
  * never disagree. Only commands that can work here are bright; anything this session cannot run is
5
5
  * dim with the reason. Planned commands stay hidden unless typed exactly.
6
6
  */
7
- import { REGISTRY } from '../commands.js';
7
+ import { REGISTRY, entryVisible } from '../commands.js';
8
8
 
9
9
  export const PALETTE_MAX = 8;
10
10
 
@@ -25,7 +25,7 @@ export function unavailableReason(entry, { api = {}, caps = null, blocked = null
25
25
  const cap = caps ? caps[entry.cap || entry.id] : null;
26
26
  if (cap && cap.available === false) return cap.reason || 'not available on this server';
27
27
  for (const m of entry.needs || []) if (typeof api[m] !== 'function') return 'not available in this mode';
28
- if (blocked && entry.server) return 'not signed in';
28
+ if (blocked && entry.server) return '(sign in)';
29
29
  return null;
30
30
  }
31
31
 
@@ -44,11 +44,12 @@ export function capabilityMap(res) {
44
44
 
45
45
  /** All palette rows for this session, registry order. */
46
46
  export function paletteItems(ctx = {}, registry = REGISTRY) {
47
- return registry.filter(e => e.kind === 'command').map(e => ({
47
+ return registry.filter(e => e.kind === 'command' && entryVisible(e)).map(e => ({
48
48
  id: e.id,
49
49
  name: '/' + e.id,
50
50
  usage: e.usage,
51
- desc: e.desc,
51
+ desc: e.short || e.desc, // the palette row is short (commandShort.js); the long wording is searched and shown by /help
52
+ longDesc: e.desc,
52
53
  aliases: (e.aliases || []).map(a => '/' + a),
53
54
  planned: e.status === 'planned',
54
55
  disabled: e.status === 'planned' ? 'planned, not built yet' : unavailableReason(e, ctx)
@@ -73,13 +74,26 @@ export function fuzzy(q, s) {
73
74
  export function filterPalette(items, query) {
74
75
  const q = String(query ?? '').toLowerCase();
75
76
  const exact = (it) => it.id === q || it.aliases.includes('/' + q);
77
+ const wordStart = (text) => q.length >= 2 && new RegExp('(^|[^a-z0-9])' + q.replace(/[^a-z0-9]/g, '\\$&')).test(String(text).toLowerCase());
76
78
  const scored = [];
77
79
  items.forEach((it, idx) => {
78
80
  if (it.planned && !exact(it)) return;
79
- const names = [it.id, ...it.aliases.map(a => a.slice(1))];
81
+ // Rank: exact, prefix of the name, prefix of an alias, inside a name, a word in the description at a word start,
82
+ // then fuzzy subsequence, then any other mention in the description.
80
83
  let best = null;
81
- for (const n of names) { const sc = fuzzy(q, n); if (sc != null && (best == null || sc < best)) best = sc; }
82
- if (best == null && q.length >= 3 && it.desc.toLowerCase().includes(q)) best = 500; // words in the description
84
+ const long = it.longDesc || it.desc;
85
+ const nameSc = fuzzy(q, it.id);
86
+ if (nameSc === 0) best = 0;
87
+ else if (it.aliases.some(a => a.slice(1).toLowerCase().startsWith(q))) best = 1;
88
+ else if (nameSc != null && nameSc < 100) best = nameSc; // a substring of the name (10 + position)
89
+ else {
90
+ for (const a of it.aliases) { const sc = fuzzy(q, a.slice(1)); if (sc != null && sc < 100 && (best == null || sc < best)) best = sc; }
91
+ if (best == null && wordStart(long)) best = 60;
92
+ if (best == null) { // fuzzy subsequence of the name or an alias
93
+ for (const n of [it.id, ...it.aliases.map(a => a.slice(1))]) { const sc = fuzzy(q, n); if (sc != null && (best == null || sc < best)) best = sc; }
94
+ }
95
+ if (best == null && q.length >= 3 && long.toLowerCase().includes(q)) best = 500;
96
+ }
83
97
  if (best == null) return;
84
98
  if (exact(it)) best = -1;
85
99
  if (it.disabled) best += 0.5; // a working command outranks an unavailable one on a tie
@@ -4,27 +4,121 @@
4
4
  * which narrows the list because the draft IS the filter.
5
5
  */
6
6
  import { findEntry } from '../commands.js';
7
+ import { signedOutNotice } from '../sessionDead.js';
7
8
  import { paletteQuery, paletteItems, filterPalette, completion, paletteView, PALETTE_MAX } from './palette.js';
9
+ import { argContext, levelFor, staticItems, rankArgs, applyArg } from './args.js';
10
+ import { mentionContext, rankMentions, applyMention } from '../mentions.js';
8
11
 
9
- export function createPalette({ getDraft, setDraft, run, say, ctx = () => ({}) }) {
10
- const st = { sel: 0, q: null, dismissed: null };
12
+ const ARG_TTL = 30000; // a fetched list is reused this long
13
+ const ARG_FAIL_TTL = 8000; // a failed fetch is quiet (no rows) and retried after this
14
+
15
+ const startsLike = (q, it) => {
16
+ const w = String(q || '').toLowerCase().replace(/^\//, '');
17
+ return [it.name, ...(it.aliases || [])].some(n => String(n).toLowerCase().replace(/^\//, '').startsWith(w));
18
+ };
19
+
20
+ export function createPalette({
21
+ getDraft, setDraft, run, say, ctx = () => ({}), source = null, refresh = () => {}, now = Date.now,
22
+ mentions = null // MENTIONS: { cache (mentions.js createMentionCache), local: () => receipt items }
23
+ }) {
24
+ const st = { sel: 0, q: null, dismissed: null, picked: false, aq: null, mq: null };
25
+ const cache = new Map(); // 'src:param' -> { at, ttl, items, pending }
26
+ /** Cached items for an async source; fetches when missing or stale. Never throws or blocks. */
27
+ const fetchItems = (src, param) => {
28
+ if (!source) return [];
29
+ const key = `${src}:${param ?? ''}`;
30
+ const hit = cache.get(key);
31
+ if (hit && (hit.pending || now() - hit.at < hit.ttl)) return hit.items;
32
+ const ent = { at: now(), ttl: ARG_TTL, items: hit?.items ?? [], pending: true };
33
+ cache.set(key, ent);
34
+ Promise.resolve().then(() => source(src, param)).then(
35
+ (items) => {
36
+ ent.items = Array.isArray(items) ? items : [];
37
+ ent.at = now(); ent.pending = false;
38
+ },
39
+ () => { ent.items = []; ent.ttl = ARG_FAIL_TTL; ent.at = now(); ent.pending = false; }
40
+ ).then(() => refresh());
41
+ return ent.items;
42
+ };
43
+ /** The argument suggestions for the current draft, or null when it is not at an argument. */
44
+ const argState = () => {
45
+ const d = getDraft();
46
+ const cx = argContext(d);
47
+ if (!cx) return null;
48
+ const lv = levelFor(cx.entry, cx.words);
49
+ if (!lv) return null;
50
+ const param = lv.of === 'partial' ? cx.partial.slice(0, cx.partial.lastIndexOf('/') + 1) : lv.of != null ? cx.words[lv.of] : undefined; // 'partial': the directory typed so far (QUOXON-MESH /send paths)
51
+ const items = [...staticItems(lv), ...(lv.src ? fetchItems(lv.src, param) : [])];
52
+ const rows = rankArgs(items, cx.partial);
53
+ const key = d.slice(0, cx.start) + '\u0000' + cx.partial;
54
+ if (key !== st.aq) { st.aq = key; st.sel = 0; st.picked = false; }
55
+ return { cx, rows };
56
+ };
57
+ const argOpen = () => {
58
+ const d = getDraft();
59
+ if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
60
+ if (st.dismissed === d) return null;
61
+ const a = argState();
62
+ return a && a.rows.length ? a : null;
63
+ };
64
+ /** MENTIONS: the @word at the end of the draft and its ranked rows from the session cache, or null. */
65
+ const mentionOpen = () => {
66
+ if (!mentions) return null;
67
+ const d = getDraft();
68
+ const cx = mentionContext(d);
69
+ if (!cx) return null;
70
+ if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
71
+ if (st.dismissed === d) return null;
72
+ const rows = rankMentions([...mentions.cache.items(), ...(mentions.local?.() || [])], cx.partial);
73
+ const key = d.slice(0, cx.start) + '\u0000' + cx.partial;
74
+ if (key !== st.mq) { st.mq = key; st.sel = 0; st.picked = false; }
75
+ return rows.length ? { cx, rows } : null;
76
+ };
11
77
  const rows = () => {
12
78
  const q = paletteQuery(getDraft());
13
- if (q !== st.q) { st.q = q; st.sel = 0; }
79
+ if (q !== st.q) { st.q = q; st.sel = 0; st.picked = false; }
14
80
  return q === null ? [] : filterPalette(paletteItems(ctx()), q);
15
81
  };
16
82
  const isOpen = () => {
17
83
  const d = getDraft();
18
84
  if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
19
- return paletteQuery(d) !== null && st.dismissed !== d;
85
+ if (paletteQuery(d) === null) return argOpen() !== null || mentionOpen() !== null;
86
+ return st.dismissed !== d;
20
87
  };
88
+ /** MENTIONS keys: Up/Down choose, Tab or Enter accept, Esc closes. */
89
+ function onMentionKey(name, m) {
90
+ const n = m.rows.length;
91
+ switch (name) {
92
+ case 'up': st.sel = (st.sel - 1 + n) % n; st.picked = true; return true;
93
+ case 'down': st.sel = (st.sel + 1) % n; st.picked = true; return true;
94
+ case 'escape': st.dismissed = getDraft(); return true;
95
+ case 'tab': case 'enter': setDraft(applyMention(getDraft(), m.cx, m.rows[Math.min(st.sel, n - 1)].value)); return true;
96
+ default: return false;
97
+ }
98
+ }
21
99
  /** -> true when the key was the palette's. */
100
+ function onArgKey(name, a) {
101
+ const n = a.rows.length;
102
+ switch (name) {
103
+ case 'up': st.sel = (st.sel - 1 + n) % n; st.picked = true; return true;
104
+ case 'down': st.sel = (st.sel + 1) % n; st.picked = true; return true;
105
+ case 'escape': st.dismissed = getDraft(); return true;
106
+ case 'tab': case 'enter': {
107
+ if (name === 'enter' && !st.picked) return false; // Enter sends what was typed
108
+ const it = a.rows[Math.min(st.sel, n - 1)];
109
+ setDraft(applyArg(getDraft(), a.cx, it.value));
110
+ return true;
111
+ }
112
+ default: return false;
113
+ }
114
+ }
22
115
  function onKey(name) {
23
116
  if (!isOpen()) return false;
117
+ if (paletteQuery(getDraft()) === null) { const a = argOpen(); return a ? onArgKey(name, a) : onMentionKey(name, mentionOpen()); }
24
118
  const list = rows();
25
119
  switch (name) {
26
- case 'up': if (!list.length) return false; st.sel = (st.sel - 1 + list.length) % list.length; return true;
27
- case 'down': if (!list.length) return false; st.sel = (st.sel + 1) % list.length; return true;
120
+ case 'up': if (!list.length) return false; st.sel = (st.sel - 1 + list.length) % list.length; st.picked = true; return true;
121
+ case 'down': if (!list.length) return false; st.sel = (st.sel + 1) % list.length; st.picked = true; return true;
28
122
  case 'escape': st.dismissed = getDraft(); return true;
29
123
  case 'tab': {
30
124
  const it = list[st.sel];
@@ -37,7 +131,10 @@ export function createPalette({ getDraft, setDraft, run, say, ctx = () => ({}) }
37
131
  if (typed && typed.kind === 'command') return false; // an exact command runs as typed (planned ones say so)
38
132
  const it = list[st.sel];
39
133
  if (!it) return false; // nothing matches: the normal path says "Unknown command"
40
- if (it.disabled) { say(`${it.name} is ${it.disabled}.`, 'warn'); return true; }
134
+ // A loose (subsequence) match only runs when the user chose it with the arrows. Typing an unknown
135
+ // word and pressing Enter must not run some other command (z2h: "/hat" ran /approvals).
136
+ if (!st.picked && !startsLike(st.q, it)) return false;
137
+ if (it.disabled) { say(signedOutNotice(it.name, it.disabled), 'warn'); return true; }
41
138
  setDraft('');
42
139
  run(it.name);
43
140
  return true;
@@ -46,6 +143,19 @@ export function createPalette({ getDraft, setDraft, run, say, ctx = () => ({}) }
46
143
  }
47
144
  }
48
145
  function view() {
146
+ if (paletteQuery(getDraft()) === null) {
147
+ const a = argOpen();
148
+ if (!a) {
149
+ const mo = mentionOpen();
150
+ if (!mo) return { rows: [], sel: 0, more: 0, empty: null, hint: null };
151
+ const mv = paletteView(mo.rows, st.sel, PALETTE_MAX);
152
+ return { rows: mv.rows.map(it => ({ key: `@${it.value}`, text: `${it.kind}${it.label ? ` · ${it.label}` : ''}` })), sel: mv.sel, more: mv.more, empty: null, hint: '↑↓ select · Tab or Enter accept · Esc close' };
153
+ }
154
+ const v = paletteView(a.rows, st.sel, PALETTE_MAX);
155
+ const rows = v.rows.map(it => ({ key: it.value, text: it.desc || '' }));
156
+ const hint = '↑↓ select · Tab complete · Esc close';
157
+ return { rows, sel: v.sel, more: v.more, empty: null, hint };
158
+ }
49
159
  const list = rows();
50
160
  const v = paletteView(list, st.sel, PALETTE_MAX);
51
161
  return {
@@ -3,6 +3,8 @@
3
3
  * Every value comes from the caller's real data; anything not known is said plainly, never filled in.
4
4
  */
5
5
  import { capabilityMap } from './palette.js';
6
+ import { CHOICES } from '../model.js';
7
+ import { providerState, capabilityName } from '../providers.js';
6
8
 
7
9
  const n = (v) => Number(v || 0).toLocaleString('en-US');
8
10
  const row = (key, text) => ({ kind: 'row', key, text: String(text) });
@@ -53,6 +55,11 @@ export function contextModel(d = {}) {
53
55
  const mem = d.memory;
54
56
  out.push(row('Memory', !mem ? '? not checked (/context checks it live)' : mem.ok ? 'connected (persistent memory answers)' : `not reachable: ${mem.error}`));
55
57
  out.push(row('Agent', d.agent || 'CommanderQ (quox)'));
58
+ if (d.screen) {
59
+ const l = d.screen.last;
60
+ out.push(row('Terminal', !d.screen.on ? 'off for this session (/context on)' : l ? `${l.bytes} bytes sent last turn: ${l.sections.join(', ') || 'nothing new'}${l.unchanged?.length ? ` (unchanged: ${l.unchanged.join(', ')})` : ''} · /context off stops it` : 'on, sent with your next message · /context off stops it'));
61
+ if (d.screen.on && l?.text) for (const t of l.text.split('\n')) out.push({ kind: 'dim', key: '', text: ' ' + t });
62
+ }
56
63
  return out;
57
64
  }
58
65
 
@@ -83,10 +90,12 @@ export async function chatChecks(api, providerTypes = ['openai', 'anthropic', 'o
83
90
  }),
84
91
  one('AI provider', async () => {
85
92
  if (!api.connections) return { status: 'skip', detail: 'not available in this mode' };
86
- const r = await api.connections();
87
- if (!r.ok) return { status: 'fail', detail: `could not read the vault: ${r.error}` };
88
- const have = providerTypes.filter(t => r.rows.some(x => x.integration_type === t));
89
- return have.length ? { status: 'pass', detail: `connected: ${have.join(', ')}` } : { status: 'warn', detail: 'no AI provider in your org vault; /connect openai' };
93
+ // The same provider status as /model and /connect (providers.js): the server's capabilities first.
94
+ const [r, caps] = await Promise.all([api.connections(), api.capabilities ? api.capabilities() : null]);
95
+ const ready = CHOICES.filter(c => providerState({ cap: c.cap || c.id, vault: c.vault }, caps, r).state === 'connected').map(c => c.name);
96
+ if (ready.length) return { status: 'pass', detail: `ready: ${ready.join(', ')}` };
97
+ if (!r.ok && !caps?.ok) return { status: 'fail', detail: `could not read the vault: ${r.error}` };
98
+ return { status: 'warn', detail: 'no AI provider is ready; /connect openai' };
90
99
  }),
91
100
  one('Capabilities', async () => {
92
101
  if (!api.capabilities) return { status: 'skip', detail: 'not available in this mode' };
@@ -94,8 +103,14 @@ export async function chatChecks(api, providerTypes = ['openai', 'anthropic', 'o
94
103
  if (res && res.ok === false) return res.missing ? { status: 'warn', detail: 'this server has no capability contract yet (GET /api/v1/capabilities)' } : { status: 'fail', detail: res.error || 'failed' };
95
104
  const m = capabilityMap(res);
96
105
  if (!m) return { status: 'warn', detail: 'unexpected capability response' };
97
- const off = Object.entries(m).filter(([, v]) => v && v.available === false).map(([k]) => k);
98
- return { status: off.length ? 'warn' : 'pass', detail: `${Object.keys(m).length} known${off.length ? `, unavailable: ${off.slice(0, 6).join(', ')}` : ', all available'}` };
106
+ const names = (a) => res.capabilities.filter(c => c?.availability === a).map(capabilityName);
107
+ const off = names('unavailable'), setup = names('requires_setup');
108
+ const more = (l) => `${l.slice(0, 4).join(', ')}${l.length > 4 ? ` +${l.length - 4} more` : ''}`;
109
+ const parts = [`${Object.keys(m).length} known`];
110
+ if (off.length) parts.push(`unavailable here: ${more(off)}`);
111
+ if (setup.length) parts.push(`needs setup: ${more(setup)}`);
112
+ if (!off.length && !setup.length) parts.push('all available');
113
+ return { status: off.length ? 'warn' : 'pass', detail: parts.join(', ') };
99
114
  })
100
115
  ]);
101
116
  }