quoxcli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/admin.js +7 -0
  6. package/lib/api/endpoints/agent-attach.js +7 -0
  7. package/lib/api/endpoints/compliance.js +25 -0
  8. package/lib/api/endpoints/governance.js +7 -0
  9. package/lib/api/endpoints/inbox.js +6 -0
  10. package/lib/api/endpoints/inboxq.js +58 -0
  11. package/lib/api/endpoints/index.js +4 -0
  12. package/lib/api/endpoints/lucid.js +17 -0
  13. package/lib/api/endpoints/mcp.js +35 -0
  14. package/lib/api/endpoints/mon.js +122 -0
  15. package/lib/api/endpoints/notesq.js +2 -0
  16. package/lib/api/endpoints/quoxplan.js +51 -0
  17. package/lib/api/endpoints/tools.js +13 -0
  18. package/lib/api/endpoints/v1.js +13 -0
  19. package/lib/commands/actions.js +51 -13
  20. package/lib/commands/admin.js +35 -0
  21. package/lib/commands/agentAttach.js +15 -0
  22. package/lib/commands/approvals.js +193 -3
  23. package/lib/commands/ask.js +1 -1
  24. package/lib/commands/auth.js +64 -11
  25. package/lib/commands/backup.js +11 -4
  26. package/lib/commands/capabilities.js +3 -2
  27. package/lib/commands/chat.js +15 -0
  28. package/lib/commands/claude.js +4 -1
  29. package/lib/commands/compliance.js +109 -0
  30. package/lib/commands/connect.js +69 -0
  31. package/lib/commands/contacts.js +2 -0
  32. package/lib/commands/dev.js +6 -8
  33. package/lib/commands/doctor.js +22 -3
  34. package/lib/commands/evidence.js +2 -1
  35. package/lib/commands/gripe.js +24 -13
  36. package/lib/commands/inbox.js +37 -3
  37. package/lib/commands/inboxq.js +147 -0
  38. package/lib/commands/kuma.js +78 -5
  39. package/lib/commands/licenceLinkView.js +311 -0
  40. package/lib/commands/linkedin.js +19 -0
  41. package/lib/commands/loop.js +30 -3
  42. package/lib/commands/lucid.js +7 -0
  43. package/lib/commands/lucidGoals.js +389 -0
  44. package/lib/commands/mail.js +427 -25
  45. package/lib/commands/mcp.js +190 -0
  46. package/lib/commands/memory.js +68 -4
  47. package/lib/commands/memorySync.js +158 -0
  48. package/lib/commands/mon.js +69 -144
  49. package/lib/commands/monitoring.js +68 -0
  50. package/lib/commands/notes.js +29 -7
  51. package/lib/commands/notesSeal.js +617 -0
  52. package/lib/commands/objective.js +5 -2
  53. package/lib/commands/offload.js +52 -12
  54. package/lib/commands/plan.js +487 -52
  55. package/lib/commands/provider.js +29 -8
  56. package/lib/commands/qlarity.js +32 -1
  57. package/lib/commands/run.js +2 -1
  58. package/lib/commands/server.js +37 -0
  59. package/lib/commands/setup.js +127 -75
  60. package/lib/commands/skills.js +4 -1
  61. package/lib/commands/slot.js +49 -9
  62. package/lib/commands/status.js +92 -44
  63. package/lib/commands/tool.js +30 -0
  64. package/lib/commands/vault.js +6 -3
  65. package/lib/commands/volt.js +6 -1
  66. package/lib/commands/walkthrough.js +14 -2
  67. package/lib/commands/watch.js +11 -2
  68. package/lib/commands/wazuh.js +880 -0
  69. package/lib/commands/wazuhIntake.js +358 -0
  70. package/lib/commands/wazuhReport.js +192 -0
  71. package/lib/commands/workflow.js +47 -4
  72. package/lib/commands/x.js +50 -0
  73. package/lib/core/announce.js +86 -0
  74. package/lib/core/auth.js +20 -2
  75. package/lib/core/callMe.js +118 -0
  76. package/lib/core/client.js +27 -1
  77. package/lib/core/commandRegistrars.js +9 -0
  78. package/lib/core/config.js +42 -3
  79. package/lib/core/confirm.js +68 -7
  80. package/lib/core/crossOrgApprovals.js +69 -0
  81. package/lib/core/errors.js +52 -1
  82. package/lib/core/governedSession.js +135 -5
  83. package/lib/core/inboxResolution.js +3 -2
  84. package/lib/core/loginGuard.js +73 -0
  85. package/lib/core/offload.js +120 -0
  86. package/lib/core/org-override.js +5 -1
  87. package/lib/core/output.js +30 -5
  88. package/lib/core/profileTrust.js +182 -0
  89. package/lib/core/quoxglyph.js +22 -1
  90. package/lib/core/router.js +14 -0
  91. package/lib/core/selfUpdate.js +2 -0
  92. package/lib/core/stepUpPrompt.js +99 -0
  93. package/lib/core/version.js +1 -1
  94. package/lib/core/walkthrough.js +9 -5
  95. package/lib/dragon/select.js +2 -2
  96. package/lib/quoxon/EXTENSIONS.md +142 -19
  97. package/lib/quoxon/api.js +189 -34
  98. package/lib/quoxon/app.js +1277 -188
  99. package/lib/quoxon/approvalBatch.js +117 -0
  100. package/lib/quoxon/approvalFacts.js +56 -2
  101. package/lib/quoxon/approvalSentence.js +8 -4
  102. package/lib/quoxon/approvalsView.js +72 -12
  103. package/lib/quoxon/ask.js +4 -2
  104. package/lib/quoxon/autoOpen.js +94 -0
  105. package/lib/quoxon/backgroundWork.js +59 -0
  106. package/lib/quoxon/bang.js +8 -2
  107. package/lib/quoxon/capabilities.js +7 -2
  108. package/lib/quoxon/commandShort.js +8 -1
  109. package/lib/quoxon/commands.js +42 -12
  110. package/lib/quoxon/connect.js +5 -3
  111. package/lib/quoxon/deckLazy.js +58 -0
  112. package/lib/quoxon/delegationDragon.js +49 -0
  113. package/lib/quoxon/destructive.js +35 -0
  114. package/lib/quoxon/dnd.js +118 -0
  115. package/lib/quoxon/dragonCompanion.js +37 -7
  116. package/lib/quoxon/dragonMoments.js +78 -0
  117. package/lib/quoxon/echo.js +1 -0
  118. package/lib/quoxon/events.js +55 -8
  119. package/lib/quoxon/extensions.js +400 -38
  120. package/lib/quoxon/foreignText.js +49 -0
  121. package/lib/quoxon/gripeQueue.js +135 -0
  122. package/lib/quoxon/inboxView.js +125 -22
  123. package/lib/quoxon/keys.js +2 -2
  124. package/lib/quoxon/launcher.js +2 -1
  125. package/lib/quoxon/loopCard.js +7 -2
  126. package/lib/quoxon/loopCards.js +17 -8
  127. package/lib/quoxon/loopStream.js +20 -9
  128. package/lib/quoxon/mailView.js +71 -0
  129. package/lib/quoxon/main.js +27 -6
  130. package/lib/quoxon/markdown.js +1 -1
  131. package/lib/quoxon/model.js +15 -0
  132. package/lib/quoxon/neverAnswered.js +20 -0
  133. package/lib/quoxon/objects/away.js +43 -5
  134. package/lib/quoxon/onboarding/connectServer.js +162 -0
  135. package/lib/quoxon/onboarding/copy.js +6 -0
  136. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  137. package/lib/quoxon/onboarding/index.js +5 -43
  138. package/lib/quoxon/onboarding/probe.js +20 -1
  139. package/lib/quoxon/onboarding/view.js +16 -3
  140. package/lib/quoxon/orgSwitch.js +22 -0
  141. package/lib/quoxon/print.js +2 -0
  142. package/lib/quoxon/profileSwitch.js +7 -0
  143. package/lib/quoxon/promptHistory.js +3 -1
  144. package/lib/quoxon/render.js +68 -16
  145. package/lib/quoxon/resend.js +14 -0
  146. package/lib/quoxon/roster.js +10 -5
  147. package/lib/quoxon/rosterSessions.js +44 -0
  148. package/lib/quoxon/rows.js +3 -1
  149. package/lib/quoxon/screenContext.js +1 -1
  150. package/lib/quoxon/secretShape.js +88 -0
  151. package/lib/quoxon/slash/agents.js +2 -1
  152. package/lib/quoxon/slash/argSources.js +9 -2
  153. package/lib/quoxon/slash/args.js +4 -2
  154. package/lib/quoxon/slash/broadcast.js +48 -11
  155. package/lib/quoxon/slash/broadcastRun.js +24 -7
  156. package/lib/quoxon/slash/cliLeaves.json +98 -1
  157. package/lib/quoxon/slash/dashboard.js +2 -0
  158. package/lib/quoxon/slash/hatWalk.js +1 -1
  159. package/lib/quoxon/slash/listView.js +2 -1
  160. package/lib/quoxon/slash/palette.js +63 -1
  161. package/lib/quoxon/slash/paletteController.js +15 -5
  162. package/lib/quoxon/slash/restart.js +9 -4
  163. package/lib/quoxon/slash/stepUp.js +5 -5
  164. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  165. package/lib/quoxon/slashSpinner.js +35 -0
  166. package/lib/quoxon/slotAttach.js +8 -0
  167. package/lib/quoxon/slotChat.js +24 -3
  168. package/lib/quoxon/slotFormat.js +1 -1
  169. package/lib/quoxon/slotOutbox.js +113 -0
  170. package/lib/quoxon/slotRun.js +2 -0
  171. package/lib/quoxon/suggestCommand.js +1 -1
  172. package/lib/quoxon/term.js +25 -1
  173. package/lib/quoxon/transcript.js +2 -1
  174. package/lib/quoxon/webPty.js +15 -0
  175. package/lib/quoxon/welcome.js +1 -1
  176. package/lib/quoxon/whatsnew.js +1 -1
  177. package/lib/quoxon/whatsnew.jsonl +42 -0
  178. package/lib/render/planBrief.js +614 -0
  179. package/lib/server/installFlow.js +168 -0
  180. package/lib/server/status.js +15 -1
  181. package/lib/toolOutputShape.js +61 -3
  182. package/package.json +3 -2
  183. package/vendor/dragonlab/SOURCE +3 -3
  184. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  185. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  186. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  187. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  188. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  189. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  190. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  191. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  192. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  193. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  194. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  195. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  196. package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
  197. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  198. package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
  199. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
@@ -41,7 +41,7 @@ const asItem = (x) =>
41
41
  /** Static choices of a level as items. */
42
42
  export const staticItems = (lv) => (lv?.choices || []).map(asItem);
43
43
 
44
- /** Prefix matches first, then substring; the exact sole match is dropped (nothing to add). */
44
+ /** Prefix matches first, then substring, then display-name matches; the exact sole match is dropped (nothing to add). */
45
45
  export function rankArgs(items, partial) {
46
46
  const q = String(partial || '').toLowerCase();
47
47
  const seen = new Set();
@@ -51,7 +51,9 @@ export function rankArgs(items, partial) {
51
51
  const sub = uniq.filter(
52
52
  (i) => !i.value.toLowerCase().startsWith(q) && i.value.toLowerCase().includes(q),
53
53
  );
54
- const out = [...pre, ...sub];
54
+ // A display name (desc) that matches also surfaces the item, after every id match: /attach vulcan finds forge.
55
+ const byDesc = uniq.filter((i) => !pre.includes(i) && !sub.includes(i) && String(i.desc || '').toLowerCase().includes(q));
56
+ const out = [...pre, ...sub, ...byDesc];
55
57
  return out.length === 1 && out[0].value.toLowerCase() === q ? [] : out;
56
58
  }
57
59
 
@@ -3,8 +3,8 @@
3
3
  * No I/O. v1 scope: all held slots or --slots, --priority urgent, --ack, /page. Scheduling, voice,
4
4
  * fleet and org reach, bulletins and drafting are not built.
5
5
  */
6
- export const BROADCAST_USAGE = 'Usage: /broadcast [--slots a,b] [--priority urgent] [--ack] <text>';
7
- export const PAGE_USAGE = 'Usage: /page <slot> <text>';
6
+ export const BROADCAST_USAGE = 'Usage: /broadcast [--slots a,b] [--priority urgent] [--ack] [--active [--dry-run]] <text>';
7
+ export const PAGE_USAGE = 'Usage: /page <slot>[,<slot>...] <text> (several slots: a,b,c or a b c)';
8
8
  const NOT_YET = new Set(['--fleet', '--org', '--voice', '--at']);
9
9
 
10
10
  /**
@@ -13,13 +13,15 @@ const NOT_YET = new Set(['--fleet', '--org', '--voice', '--at']);
13
13
  */
14
14
  export function parseBroadcastArgs(arg) {
15
15
  const toks = String(arg || '').trim().split(/\s+/).filter(Boolean);
16
- let slots = null; let priority = 'normal'; let ack = false;
16
+ let slots = null; let priority = 'normal'; let ack = false; let active = false; let dryRun = false;
17
17
  let i = 0;
18
18
  for (; i < toks.length && toks[i].startsWith('--'); i += 1) {
19
19
  const [flag, inline] = toks[i].split(/=(.*)/s);
20
20
  if (NOT_YET.has(flag)) return { ok: false, message: `${flag} is not available yet (v1 sends to held slots in this org: --slots, --priority, --ack).` };
21
21
  if (flag === '--all') { slots = null; continue; }
22
22
  if (flag === '--ack') { ack = true; continue; }
23
+ if (flag === '--active') { active = true; continue; }
24
+ if (flag === '--dry-run') { dryRun = true; continue; }
23
25
  const val = inline !== undefined ? inline : toks[++i];
24
26
  if (flag === '--slots') {
25
27
  const list = String(val || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
@@ -34,14 +36,43 @@ export function parseBroadcastArgs(arg) {
34
36
  }
35
37
  const text = toks.slice(i).join(' ');
36
38
  if (!text) return { ok: false, message: BROADCAST_USAGE };
37
- return { ok: true, text, slots, priority, ack };
39
+ return { ok: true, text, slots, priority, ack, ...(active ? { active: true } : {}), ...(dryRun ? { dryRun: true } : {}) };
38
40
  }
39
41
 
40
- /** `/page <slot> <text>` -> urgent, ack-required, one recipient. */
41
- export function parsePageArgs(arg) {
42
- const m = /^(\S+)\s+([\s\S]*\S[\s\S]*)$/.exec(String(arg || '').trim());
43
- if (!m || m[1].startsWith('--')) return { ok: false, message: PAGE_USAGE };
44
- return { ok: true, text: m[2].trim(), slots: [m[1].toLowerCase()], priority: 'urgent', ack: true };
42
+ /** QuoxGlyph announce line for a broadcast to active sessions: { reached, skipped, dryRun }. */
43
+ export function activeLine({ reached = 0, skipped = 0, dryRun = false } = {}) {
44
+ return `\u{1F4CD} QUOXSLOTS \u2502 broadcast to active \u2502 ${reached} ${dryRun ? 'would reach' : 'reached'} \u00B7 ${skipped} skipped dormant \u2714`;
45
+ }
46
+
47
+ /**
48
+ * TUI-WAKE-MULTI (owner 2026-11-08): the leading slot names of a /page or /wake line, then the message.
49
+ * The first name is always a target. A name joined by a tight comma (seer,quoxon) is always a target, known or not,
50
+ * so an unknown one is reported per slot by the server. A name after a space, or after ", ", is a target only when
51
+ * it is in `known` (the org's slot names); the first word that is not starts the message, kept exactly as typed.
52
+ * With no `known` list only the tight comma list is targets. -> { slots: [lowercase, deduped], text } | null.
53
+ */
54
+ export function splitTargets(arg, known = null) {
55
+ const set = known ? new Set([...known].map((x) => String(x).toLowerCase())) : null;
56
+ const slots = []; let rest = String(arg || '').trim(); let tight = true;
57
+ for (;;) {
58
+ const m = /^\s*([^\s,]+)/.exec(rest);
59
+ if (!m) break;
60
+ const name = m[1].toLowerCase();
61
+ if (!slots.length && name.startsWith('--')) return null;
62
+ if (!tight && !(set && set.has(name))) break;
63
+ slots.push(name); rest = rest.slice(m[0].length);
64
+ const c = /^\s*,(\s*)/.exec(rest);
65
+ tight = Boolean(c && !c[1]);
66
+ if (c) rest = rest.slice(c[0].length);
67
+ }
68
+ return slots.length ? { slots: [...new Set(slots)], text: rest.trim() } : null;
69
+ }
70
+
71
+ /** `/page <slot>[,<slot>...| <known slot>...] <text>` -> urgent, ack-required, every named recipient. */
72
+ export function parsePageArgs(arg, known = null) {
73
+ const t = splitTargets(arg, known);
74
+ if (!t || !t.text) return { ok: false, message: PAGE_USAGE };
75
+ return { ok: true, text: t.text, slots: t.slots, priority: 'urgent', ack: true };
45
76
  }
46
77
 
47
78
  const plural = (n) => (n === 1 ? 'session' : 'sessions');
@@ -72,9 +103,10 @@ export function broadcastCard(r, { kind = 'broadcast' } = {}) {
72
103
  const c = r?.counts || {};
73
104
  const rc = roomC(r);
74
105
  const rows = [];
75
- if (!c.total) rows.push([['\u26A0 no held slots in this org to hear it', 'warn']]);
106
+ if (!c.total) rows.push([[r?.skipped_dormant ? `\u26A0 no active sessions to hear it \u00B7 ${r.skipped_dormant.length} dormant skipped` : '\u26A0 no held slots in this org to hear it', 'warn']]);
76
107
  else if (c.delivered) {
77
- rows.push([[`\u2714 ${c.delivered} ${c.delivered === 1 ? 'session has' : 'sessions have'} it`, 'ok']]);
108
+ const act = r?.skipped_dormant ? 'active ' : ''; // --active: say who was left alone
109
+ rows.push([[`\u2714 ${c.delivered} ${act}${c.delivered === 1 ? 'session has' : 'sessions have'} it${r?.skipped_dormant ? ` \u00B7 ${c.dormant_skipped ?? r.skipped_dormant.length} dormant skipped` : ''}`, 'ok']]);
78
110
  if (namesRow(r)) rows.push([[namesRow(r), 'prose']]); // an older server sends no per-slot list: no empty row
79
111
  }
80
112
  for (const d of badSlots(r).slice(0, 3)) rows.push([[`\u2716 ${failText(d)}`, 'error']]);
@@ -105,7 +137,12 @@ export function broadcastSummary(r) {
105
137
  const c = r?.counts || {};
106
138
  const rc = roomC(r);
107
139
  const lines = [];
140
+ if (!c.total && r.skipped_dormant) return [[`Broadcast ${r.id}: no active sessions to hear it \u00B7 ${r.skipped_dormant.length} dormant skipped${r.skipped_dormant.length ? ` (${r.skipped_dormant.join(', ')})` : ''}.`, 'warn']];
108
141
  if (!c.total) return [[`Broadcast ${r.id}: no held slots in this org to hear it.`, 'warn']];
142
+ if (r.skipped_dormant) { // --active: say who was left alone
143
+ const dn = r.skipped_dormant;
144
+ lines.push([`Broadcast ${r.id} ${titleWord(r)}: ${(c.delivered || 0) + (c.queued || 0)} active have it${okSlots(r).length ? ` (${okSlots(r).map((d) => d.slot).join(', ')})` : ''} \u00B7 ${dn.length} dormant skipped${dn.length ? ` (${dn.join(', ')})` : ''}`, c.failed ? 'warn' : 'plain']);
145
+ } else
109
146
  lines.push([`Broadcast ${r.id} ${titleWord(r)}: ${(c.delivered || 0) + (c.queued || 0)} of ${c.total} ${plural(c.total)} have it${okSlots(r).length ? ` (${okSlots(r).map((d) => d.slot).join(', ')})` : ''}`, c.failed ? 'warn' : 'plain']);
110
147
  if (c.delivered && roomText(rc, ', ')) lines.push([` chat rooms: ${roomText(rc, ', ')}`, rc.failed ? 'warn' : 'plain']);
111
148
  for (const d of badSlots(r)) lines.push([` ${failText(d)}`, 'warn']);
@@ -3,7 +3,7 @@
3
3
  * One server call fans out (POST /api/v1/slots/broadcast); with --ack the sender then sees the delivery line
4
4
  * update as recipients acknowledge. The poll is display only: the broadcast and its acks live on the server.
5
5
  */
6
- import { parseBroadcastArgs, parsePageArgs, parseAckArgs, parseStatusArgs, broadcastSummary, broadcastCard, BROADCAST_USAGE } from './broadcast.js';
6
+ import { parseBroadcastArgs, parsePageArgs, splitTargets, parseAckArgs, parseStatusArgs, broadcastSummary, broadcastCard, BROADCAST_USAGE, activeLine } from './broadcast.js';
7
7
 
8
8
  const roomC = (r) => ({ sent: 0, sending: 0, failed: 0, ...(r?.counts?.room || {}) });
9
9
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -20,11 +20,22 @@ export async function runBroadcast({ api, kind, arg, from, say, card = null, isC
20
20
  const st = api.slotBroadcastStatus ? await api.slotBroadcastStatus(st0.id) : { ok: false, message: 'not available in this mode' };
21
21
  return st.ok ? show(st.data) : say(`Broadcast status failed: ${st.message}`, 'warn');
22
22
  }
23
- const parsed = kind === 'page' ? parsePageArgs(arg) : parseBroadcastArgs(arg);
23
+ const parsed = kind === 'page' ? parsePageArgs(arg, await slotNames(api)) : parseBroadcastArgs(arg);
24
24
  if (!parsed.ok) return say(parsed.message, parsed.message === BROADCAST_USAGE ? 'plain' : 'warn');
25
- const r = await api.slotBroadcast({ text: parsed.text, slots: parsed.slots, priority: parsed.priority, ack: parsed.ack, from });
25
+ const r = await api.slotBroadcast({ text: parsed.text, slots: parsed.slots, priority: parsed.priority, ack: parsed.ack, active: parsed.active, ...(parsed.dryRun ? { dryRun: true } : {}), from });
26
26
  if (!r.ok) return say(`${kind === 'page' ? 'Page' : 'Broadcast'} failed: ${r.message}`, 'warn');
27
+ if (r.data?.dry_run) { // BROADCAST-ACTIVE --dry-run: who would hear it, who is dormant; nothing sent, no card
28
+ const would = r.data.would_reach || []; const dorm = r.data.skipped_dormant || [];
29
+ say(activeLine({ reached: would.length, skipped: dorm.length, dryRun: true }), 'ok');
30
+ if (would.length) say(` would reach: ${would.join(', ')}`, 'plain');
31
+ if (dorm.length) say(` skipped dormant: ${dorm.join(', ')}`, 'plain');
32
+ return say('Dry run: nothing was sent.', 'plain');
33
+ }
27
34
  show(r.data);
35
+ if (parsed.active && r.data?.skipped_dormant) {
36
+ say(activeLine({ reached: r.data.counts?.delivered || 0, skipped: r.data.skipped_dormant.length }), 'ok');
37
+ if (r.data.skipped_dormant.length) say(` skipped dormant: ${r.data.skipped_dormant.join(', ')}`, 'plain');
38
+ }
28
39
  // PRESENCE: it reached a human slot whose human is away; approvals to them will queue.
29
40
  if (r.data?.human_presence?.away && r.data.human_presence.notice) say(`⏸ ${r.data.human_presence.notice}`, 'warn');
30
41
  // POKE-WAKE: a page also asks each delivered slot's keeper to wake its idle session, so the page is read now,
@@ -61,13 +72,19 @@ export async function runBroadcast({ api, kind, arg, from, say, card = null, isC
61
72
  }
62
73
 
63
74
  /**
64
- * FORCE-WAKE: `/poke|/wake|/slap|/nudge <slot> [text]`. `--force` is accepted anywhere as a no-op (every human
75
+ * FORCE-WAKE: `/poke|/wake|/slap|/nudge <slot>[,<slot>...] [text]` (several slots: see splitTargets). `--force` is accepted anywhere as a no-op (every human
65
76
  * wake already sets an unsent draft aside and puts it back) and is never page text. null = no slot given.
66
77
  */
67
- export function parsePokeArgs(restArg) {
78
+ export function parsePokeArgs(restArg, known = null) {
68
79
  const s = String(restArg || '').replace(/(^|\s)--force(?=\s|$)/g, '$1').trim();
69
- const m = /^(\S+)(?:\s+([\s\S]+))?$/.exec(s);
70
- return m ? { slot: m[1], text: m[2] ? m[2].trim() : null } : null;
80
+ const t = splitTargets(s, known); // TUI-WAKE-MULTI: `slot` is the comma-joined target list, /page splits it again
81
+ return t ? { slot: t.slots.join(','), text: t.text || null } : null;
82
+ }
83
+
84
+ /** The org's slot names for splitTargets, or null when they cannot be read (then only a comma list is targets). */
85
+ export async function slotNames(api) {
86
+ if (!api?.slots) return null;
87
+ try { const rows = await api.slots(); return Array.isArray(rows) ? rows.map((r) => r?.name).filter(Boolean) : null; } catch { return null; }
71
88
  }
72
89
 
73
90
  /**
@@ -25,6 +25,7 @@
25
25
  "admin retention stats":"read",
26
26
  "admin service list":"read",
27
27
  "admin stats":"read",
28
+ "admin users verify-email":"write",
28
29
  "aee conversations":"read",
29
30
  "aee get":"read",
30
31
  "aee query":"read",
@@ -34,6 +35,7 @@
34
35
  "agent attach":"never",
35
36
  "agent attach-heartbeat":"never",
36
37
  "agent attach-status":"read",
38
+ "agent attaches":"write",
37
39
  "agent bind-slot":"write",
38
40
  "agent create":"write",
39
41
  "agent deactivate":"write",
@@ -53,11 +55,16 @@
53
55
  "agent tool revoke":"write",
54
56
  "agent unbind-slot":"write",
55
57
  "agent update":"write",
58
+ "approvals approve":"write",
59
+ "approvals call-me":"write",
60
+ "approvals deny":"write",
56
61
  "approvals grants create":"write",
57
62
  "approvals grants list":"read",
58
63
  "approvals grants revoke":"write",
64
+ "approvals list":"read",
59
65
  "approvals result":"read",
60
66
  "approvals walkthrough":"never",
67
+ "approvals withdraw":"write",
61
68
  "aside":"never",
62
69
  "ask loosening":"write",
63
70
  "ask status":"read",
@@ -147,12 +154,16 @@
147
154
  "chat-status":"read",
148
155
  "claude":"never",
149
156
  "completion":"never",
157
+ "compliance jobs list":"read",
158
+ "compliance reports list":"read",
159
+ "compliance reports show":"read",
150
160
  "config get":"read",
151
161
  "config profile add":"write",
152
162
  "config profile list":"read",
153
163
  "config profile remove":"write",
154
164
  "config profile use":"write",
155
165
  "config set":"write",
166
+ "connect":"write",
156
167
  "consent grant":"never",
157
168
  "consent history":"read",
158
169
  "consent list":"read",
@@ -374,6 +385,7 @@
374
385
  "inbox list":"read",
375
386
  "inbox respond":"write",
376
387
  "inbox stats":"read",
388
+ "inbox unread":"write",
377
389
  "inbox walkthrough":"never",
378
390
  "instagram accounts":"read",
379
391
  "instagram comments":"read",
@@ -409,6 +421,7 @@
409
421
  "kuma monitors pause":"write",
410
422
  "kuma monitors remove":"write",
411
423
  "kuma monitors resume":"write",
424
+ "kuma monitors wire-alerts":"write",
412
425
  "kuma refresh":"write",
413
426
  "kuma sla list":"read",
414
427
  "kuma sla verify":"read",
@@ -428,6 +441,7 @@
428
441
  "linkedin org":"write",
429
442
  "linkedin post":"write",
430
443
  "linkedin posts":"read",
444
+ "linkedin whoami":"read",
431
445
  "llm status":"read",
432
446
  "llm-policy get":"read",
433
447
  "llm-policy set":"write",
@@ -444,12 +458,25 @@
444
458
  "lucid area":"read",
445
459
  "lucid ask":"read",
446
460
  "lucid blind":"write",
461
+ "lucid brief":"read",
447
462
  "lucid deck":"never",
448
463
  "lucid do":"write",
449
464
  "lucid feeds":"read",
450
465
  "lucid forget":"write",
466
+ "lucid goals add":"write",
467
+ "lucid goals archive":"write",
468
+ "lucid goals change":"write",
469
+ "lucid goals confirm":"write",
470
+ "lucid goals erase":"never",
471
+ "lucid goals export":"write",
472
+ "lucid goals history":"read",
473
+ "lucid goals link":"write",
474
+ "lucid goals restore":"never",
475
+ "lucid goals show":"read",
451
476
  "lucid herald set":"write",
452
477
  "lucid herald test":"write",
478
+ "lucid insights show":"read",
479
+ "lucid insights status":"read",
453
480
  "lucid journal":"read",
454
481
  "lucid lessons":"read",
455
482
  "lucid replay":"read",
@@ -460,10 +487,14 @@
460
487
  "lucid teach":"write",
461
488
  "lucid told":"read",
462
489
  "lucid why":"read",
490
+ "mail account pin":"write",
463
491
  "mail accounts":"read",
492
+ "mail block":"write",
493
+ "mail blocked":"read",
464
494
  "mail connect":"never",
465
495
  "mail disconnect":"write",
466
496
  "mail import":"never",
497
+ "mail junk":"write",
467
498
  "mail list":"read",
468
499
  "mail moves":"read",
469
500
  "mail proposals":"read",
@@ -471,11 +502,19 @@
471
502
  "mail report":"write",
472
503
  "mail resolve":"write",
473
504
  "mail sync":"write",
505
+ "mail sync-status":"write",
506
+ "mail trust":"write",
507
+ "mail unblock":"write",
474
508
  "mail undo":"write",
475
509
  "mail watch active":"write",
476
510
  "mail watch add":"write",
477
511
  "mail watch list":"read",
512
+ "mail watch rm":"write",
478
513
  "mail zones":"read",
514
+ "mcp bundle":"never",
515
+ "mcp token create":"never",
516
+ "mcp token list":"read",
517
+ "mcp token revoke":"write",
479
518
  "memory core":"read",
480
519
  "memory create":"write",
481
520
  "memory delete":"write",
@@ -488,6 +527,7 @@
488
527
  "memory reinstate":"write",
489
528
  "memory search":"read",
490
529
  "memory stats":"read",
530
+ "memory sync":"never",
491
531
  "memory timeline":"read",
492
532
  "memory unpin":"write",
493
533
  "memory update":"write",
@@ -515,16 +555,23 @@
515
555
  "mon docker stats":"read",
516
556
  "mon metrics":"read",
517
557
  "mon wazuh active-response":"write",
518
- "mon wazuh agents":"read",
558
+ "mon wazuh agents get":"read",
519
559
  "mon wazuh alerts":"read",
560
+ "mon wazuh coverage":"read",
520
561
  "mon wazuh deploy-agent":"write",
521
562
  "mon wazuh deploy-bulk":"write",
522
563
  "mon wazuh deploy-history":"read",
564
+ "mon wazuh fim":"read",
565
+ "mon wazuh rootcheck":"read",
566
+ "mon wazuh sca":"read",
523
567
  "mon wazuh status":"read",
568
+ "mon wazuh triage":"read",
524
569
  "mon wazuh vulns":"read",
525
570
  "monitoring health":"read",
571
+ "monitoring incidents claim":"write",
526
572
  "monitoring incidents get":"read",
527
573
  "monitoring incidents list":"read",
574
+ "monitoring incidents status":"write",
528
575
  "motion":"write",
529
576
  "notes actions":"write",
530
577
  "notes append":"write",
@@ -555,6 +602,14 @@
555
602
  "notes schedule resume":"write",
556
603
  "notes schedule rm":"write",
557
604
  "notes scorecard":"write",
605
+ "notes seal convert":"never",
606
+ "notes seal edit":"never",
607
+ "notes seal init":"write",
608
+ "notes seal new":"write",
609
+ "notes seal open":"never",
610
+ "notes seal print":"write",
611
+ "notes seal purge":"never",
612
+ "notes seal status":"read",
558
613
  "notes search":"read",
559
614
  "notes settings list":"read",
560
615
  "notes settings set":"write",
@@ -577,6 +632,7 @@
577
632
  "objective list":"read",
578
633
  "objective retry":"write",
579
634
  "objective start":"write",
635
+ "offload leases":"read",
580
636
  "offload release":"write",
581
637
  "offload status":"read",
582
638
  "onboard":"write",
@@ -598,15 +654,22 @@
598
654
  "org update":"write",
599
655
  "peek":"read",
600
656
  "plan add":"write",
657
+ "plan brief":"read",
601
658
  "plan completed":"read",
602
659
  "plan focus":"read",
660
+ "plan followup add":"write",
661
+ "plan followup done":"write",
662
+ "plan followup status":"write",
663
+ "plan followups":"read",
603
664
  "plan list":"read",
604
665
  "plan move":"write",
605
666
  "plan note":"write",
606
667
  "plan phase":"write",
607
668
  "plan plan":"write",
608
669
  "plan rename":"write",
670
+ "plan roadmap archive":"write",
609
671
  "plan roadmap create":"write",
672
+ "plan roadmap unarchive":"write",
610
673
  "plan roadmaps":"read",
611
674
  "plan search":"read",
612
675
  "plan show":"read",
@@ -614,6 +677,7 @@
614
677
  "plan status":"write",
615
678
  "plan timeline":"read",
616
679
  "plan update":"write",
680
+ "plan watch":"never",
617
681
  "plugin assign add":"write",
618
682
  "plugin assign list":"read",
619
683
  "plugin assign remove":"write",
@@ -772,6 +836,7 @@
772
836
  "server diag":"write",
773
837
  "server doctor":"read",
774
838
  "server go":"never",
839
+ "server install":"never",
775
840
  "server list":"read",
776
841
  "server mark":"write",
777
842
  "server roles list":"read",
@@ -881,8 +946,10 @@
881
946
  "tool exec":"write",
882
947
  "tool get":"read",
883
948
  "tool install":"write",
949
+ "tool installed":"read",
884
950
  "tool list":"read",
885
951
  "tool publish":"write",
952
+ "tool uninstall":"write",
886
953
  "tool update":"write",
887
954
  "trace compare":"read",
888
955
  "trace comparison create":"write",
@@ -982,6 +1049,35 @@
982
1049
  "volt verify":"read",
983
1050
  "watch fleet":"read",
984
1051
  "watch services":"read",
1052
+ "wazuh active-response":"write",
1053
+ "wazuh agents get":"read",
1054
+ "wazuh alerts":"read",
1055
+ "wazuh connect":"never",
1056
+ "wazuh coverage":"read",
1057
+ "wazuh deploy":"write",
1058
+ "wazuh deploy-bulk":"write",
1059
+ "wazuh deploy-history":"read",
1060
+ "wazuh fim":"read",
1061
+ "wazuh hooks create":"never",
1062
+ "wazuh hooks delete":"write",
1063
+ "wazuh hooks install":"never",
1064
+ "wazuh hooks list":"read",
1065
+ "wazuh hooks rotate":"never",
1066
+ "wazuh incidents close":"write",
1067
+ "wazuh incidents diagnose":"write",
1068
+ "wazuh incidents escalate":"write",
1069
+ "wazuh incidents show":"read",
1070
+ "wazuh indexer install-credential":"write",
1071
+ "wazuh remove":"write",
1072
+ "wazuh report generate":"write",
1073
+ "wazuh report list":"read",
1074
+ "wazuh report show":"read",
1075
+ "wazuh report verify":"read",
1076
+ "wazuh rootcheck":"read",
1077
+ "wazuh sca":"read",
1078
+ "wazuh status":"read",
1079
+ "wazuh triage":"read",
1080
+ "wazuh vulns":"read",
985
1081
  "webhook create":"write",
986
1082
  "webhook delete":"write",
987
1083
  "webhook list":"read",
@@ -1015,6 +1111,7 @@
1015
1111
  "x status":"read",
1016
1112
  "x thread":"write",
1017
1113
  "x timeline":"write",
1114
+ "x whoami":"write",
1018
1115
  "youtube analytics":"write",
1019
1116
  "youtube channel":"write",
1020
1117
  "youtube comment":"write",
@@ -10,6 +10,7 @@
10
10
  * on the dashboard home instead; nothing breaks.
11
11
  */
12
12
  import { spawn as nodeSpawn } from 'node:child_process';
13
+ import { isWebPty } from '../webPty.js';
13
14
 
14
15
  /** Page name -> real route in src/config/routeRegistry.js (approvals = the HITL /inbox, inbox = InboxQ mail). */
15
16
  export const PAGES = {
@@ -40,6 +41,7 @@ export function openerFor(platform = process.platform) {
40
41
 
41
42
  /** -> { opened: true } | { opened: false, reason: 'ssh' | 'no-opener' | 'failed' }. Never throws, never uses a shell. */
42
43
  export function openInBrowser(url, { env = process.env, platform = process.platform, spawn = nodeSpawn } = {}) {
44
+ if (isWebPty(env)) return Promise.resolve({ opened: false, reason: 'no-opener' }); // QUOXON web PTY: the link is printed, nothing starts on the server
43
45
  if (isRemoteSession(env)) return Promise.resolve({ opened: false, reason: 'ssh' });
44
46
  const op = openerFor(platform);
45
47
  if (!op) return Promise.resolve({ opened: false, reason: 'no-opener' });
@@ -122,7 +122,7 @@ export function createHatWalk({ api, say, bump = () => {} }) {
122
122
  if (!m.hat) { say(`No hat matches "${arg}". Your own hats: ${hatChoices(mine) || 'none yet'}.`, 'warn'); return null; }
123
123
  if (!isCustom(m.hat)) {
124
124
  say(verb === 'edit'
125
- ? `${nameOf(m.hat)} is a built-in hat, so it is not edited here. It can be reset, or used as a starting point with /hat new.`
125
+ ? `${nameOf(m.hat)} is a built-in hat, so it is not edited here. Reset it, or start from it with /hat new.`
126
126
  : `${nameOf(m.hat)} is a built-in hat and cannot be deleted. It can be reset, or used as a starting point with /hat new.`, 'warn');
127
127
  return null;
128
128
  }
@@ -9,7 +9,7 @@ import { c, clip } from '../theme.js';
9
9
 
10
10
  const h = React.createElement;
11
11
 
12
- export function ListView({ title = null, rows = [], sel = 0, cols = 80, hint = null, empty = null, more = 0 }) {
12
+ export function ListView({ title = null, rows = [], sel = 0, cols = 80, hint = null, empty = null, more = 0, notes = [] }) {
13
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
14
  // one that does not gets the ellipsis from truncate-end, never a silent cut.
15
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);
@@ -26,5 +26,6 @@ export function ListView({ title = null, rows = [], sel = 0, cols = 80, hint = n
26
26
  h(Text, { color: c(r.dim ? 'muted' : 'text'), wrap: 'truncate-end' }, clip(String(body || ''), textW)));
27
27
  }),
28
28
  more > 0 ? h(Text, { color: c('muted') }, ` … ${more} more, keep typing to narrow`) : null,
29
+ ...(notes || []).map((n, i) => h(Text, { key: `note:${i}`, color: c('muted'), wrap: 'truncate-end' }, ` ${clip(n, cols - 4)}`)), // never selectable: no marker, not in rows
29
30
  hint ? h(Text, { color: c('dim') }, ` ${clip(hint, cols - 4)}`) : null);
30
31
  }
@@ -16,6 +16,10 @@ export function paletteQuery(draft) {
16
16
  return t.slice(1).toLowerCase();
17
17
  }
18
18
 
19
+ const TRANSIENT = /not running|not reachable|unreachable|could not reach|cannot reach|connection (refused|reset)|timed? ?out|econn|enotfound|fetch failed|service unavailable|\b50[234]\b/i;
20
+ /** Does this capability reason read as a failed dynamic check rather than a standing fact about the command? */
21
+ export const transientReason = (r) => TRANSIENT.test(String(r || ''));
22
+
19
23
  /**
20
24
  * Why a command cannot run in this session, or null. `api` is the session's server wrapper (a method
21
25
  * missing means this mode has no route for it); `caps` is GET /api/v1/capabilities when the server has
@@ -23,7 +27,10 @@ export function paletteQuery(draft) {
23
27
  */
24
28
  export function unavailableReason(entry, { api = {}, caps = null, blocked = null } = {}) {
25
29
  const cap = caps ? caps[entry.cap || entry.id] : null;
26
- if (cap && cap.available === false) return cap.reason || 'not available on this server';
30
+ // A reason that is really a failed probe ("Auth service not running or not reachable...") is an error string, not a
31
+ // property of the command: it never becomes the palette note. The row keeps its static description; running the
32
+ // command reports the real failure.
33
+ if (cap && cap.available === false && !transientReason(cap.reason)) return cap.reason || 'not available on this server';
27
34
  for (const m of entry.needs || []) if (typeof api[m] !== 'function') return 'not available in this mode';
28
35
  if (blocked && entry.server) return '(sign in)';
29
36
  return null;
@@ -102,6 +109,61 @@ export function filterPalette(items, query) {
102
109
  return scored.sort((a, b) => a.best - b.best || a.idx - b.idx).map(x => x.it);
103
110
  }
104
111
 
112
+ /**
113
+ * A match too loose to pre-highlight: not a prefix of the name or an alias, and not a description word that starts
114
+ * with a 2+ letter query. "/y" -> /hyperion (a letter inside the name) is weak; "/one" -> /approvals ("one at a time")
115
+ * is not. A weak top row is shown without the marker until the user moves the selection.
116
+ */
117
+ const isSubsequence = (q, s) => { let i = 0; for (const ch of s) if (ch === q[i]) i++; return i === q.length; };
118
+
119
+ export function weakMatch(query, it) {
120
+ const q = String(query ?? '').toLowerCase();
121
+ if (!q || !it) return false;
122
+ if ([it.id, ...(it.aliases || []).map(a => a.slice(1))].some(n => String(n).toLowerCase().startsWith(q))) return false;
123
+ // A 3+ letter abbreviation of the name ("/aprvl" -> /approvals) is deliberate, not a stray letter inside it.
124
+ if (q.length >= 3 && isSubsequence(q, String(it.id).toLowerCase())) return false;
125
+ const long = String(it.longDesc || it.desc || '').toLowerCase();
126
+ return !(q.length >= 2 && new RegExp('(^|[^a-z0-9])' + q.replace(/[^a-z0-9]/g, '\\$&')).test(long));
127
+ }
128
+
129
+ /** Bulk or destructive commands: never offered (so never pre-highlighted) by a loose query (owner 2026-10-26: Enter on "/" approved everything). */
130
+ export const GUARDED_IDS = new Set(['yestoall', 'approve', 'deny']);
131
+
132
+ /**
133
+ * Drops GUARDED_IDS rows unless the query is an exact name, or a 3+ letter prefix of the name that no unguarded command
134
+ * shares. So they are reachable only by typing enough of the name to make them the unique match.
135
+ */
136
+ export function guardBulk(list, query) {
137
+ const q = String(query ?? '').toLowerCase();
138
+ const names = (it) => [it.id, ...it.aliases.map(a => a.slice(1))].map(n => n.toLowerCase());
139
+ const shared = list.some(o => !GUARDED_IDS.has(o.id) && names(o).some(n => q && n.startsWith(q)));
140
+ return list.filter(it => !GUARDED_IDS.has(it.id) || names(it).includes(q) || (q.length >= 3 && !shared && names(it).some(n => n.startsWith(q))));
141
+ }
142
+
143
+ /**
144
+ * The guarded commands a loose query hid, so the palette can say why they are missing instead of leaving a weak
145
+ * fuzzy hit on top (owner 2026-11-05: "/y" showed /hyperion). Only names that START with the query count, and only
146
+ * for a non-empty query (a bare "/" stays quiet). `reach` is the shortest input that makes it the unique match.
147
+ * These are notes, never rows: the guard still decides what can be selected.
148
+ */
149
+ export function guardedHints(items, query) {
150
+ const q = String(query ?? '').toLowerCase();
151
+ if (!q) return [];
152
+ const shown = (p) => guardBulk(filterPalette(items, p), p).map(i => i.id);
153
+ const now = shown(q);
154
+ const out = [];
155
+ for (const it of items) {
156
+ if (!GUARDED_IDS.has(it.id) || it.planned || now.includes(it.id) || !it.id.startsWith(q)) continue;
157
+ let reach = it.name;
158
+ for (let n = Math.max(3, q.length + 1); n < it.id.length; n++) {
159
+ const p = it.id.slice(0, n);
160
+ if (shown(p).includes(it.id)) { reach = '/' + p; break; }
161
+ }
162
+ out.push({ name: it.name, reach });
163
+ }
164
+ return out;
165
+ }
166
+
105
167
  /** What Tab puts in the draft: the name, plus a space when the command takes arguments. */
106
168
  export function completion(item) {
107
169
  const takesArgs = /^\S+\s+\S/.test(String(item.usage).trim());
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { findEntry } from '../commands.js';
7
7
  import { signedOutNotice } from '../sessionDead.js';
8
- import { paletteQuery, paletteItems, filterPalette, completion, paletteView, PALETTE_MAX } from './palette.js';
8
+ import { paletteQuery, paletteItems, filterPalette, guardBulk, guardedHints, weakMatch, completion, paletteView, PALETTE_MAX } from './palette.js';
9
9
  import { argContext, levelFor, staticItems, rankArgs, applyArg } from './args.js';
10
10
  import { mentionContext, rankMentions, applyMention } from '../mentions.js';
11
11
 
@@ -21,7 +21,7 @@ export function createPalette({
21
21
  getDraft, setDraft, run, say, ctx = () => ({}), source = null, refresh = () => {}, now = Date.now,
22
22
  mentions = null // MENTIONS: { cache (mentions.js createMentionCache), local: () => receipt items }
23
23
  }) {
24
- const st = { sel: 0, q: null, dismissed: null, picked: false, aq: null, mq: null };
24
+ const st = { hold: null, sel: 0, q: null, dismissed: null, picked: false, aq: null, mq: null };
25
25
  const cache = new Map(); // 'src:param' -> { at, ttl, items, pending }
26
26
  /** Cached items for an async source; fetches when missing or stale. Never throws or blocks. */
27
27
  const fetchItems = (src, param) => {
@@ -77,10 +77,16 @@ export function createPalette({
77
77
  const rows = () => {
78
78
  const q = paletteQuery(getDraft());
79
79
  if (q !== st.q) { st.q = q; st.sel = 0; st.picked = false; }
80
- return q === null ? [] : filterPalette(paletteItems(ctx()), q);
80
+ return q === null ? [] : guardBulk(filterPalette(paletteItems(ctx()), q), q);
81
81
  };
82
+ /** The top row is only a weak match (weakMatch) and the user has not moved the selection: it is not highlighted. */
83
+ const weakTop = (list) => !st.picked && list.length > 0 && weakMatch(st.q, list[0]);
82
84
  const isOpen = () => {
83
85
  const d = getDraft();
86
+ if (st.hold !== null) { // a command was just submitted: closed until the draft is genuinely edited (owner 2026-10-27: a stale palette ran /sleep)
87
+ if (d === st.hold || d === '') return false;
88
+ st.hold = null;
89
+ }
84
90
  if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
85
91
  if (paletteQuery(d) === null) return argOpen() !== null || mentionOpen() !== null;
86
92
  return st.dismissed !== d;
@@ -158,12 +164,16 @@ export function createPalette({
158
164
  }
159
165
  const list = rows();
160
166
  const v = paletteView(list, st.sel, PALETTE_MAX);
167
+ const q = paletteQuery(getDraft());
168
+ const notes = guardedHints(paletteItems(ctx()), q).map(g => `${g.name} is held back so a stray Enter cannot run it: type ${g.reach} to reach it`);
161
169
  return {
162
170
  rows: v.rows.map(it => ({ key: it.name, text: (it.aliases.length ? `(also ${it.aliases.join(', ')}) ` : '') + it.desc, dim: Boolean(it.disabled), note: it.disabled || null })),
163
- sel: v.sel, more: v.more,
171
+ sel: weakTop(list) ? -1 : v.sel, more: v.more, notes,
164
172
  empty: 'No command matches. Enter sends it anyway; /help lists everything.',
165
173
  hint: '↑↓ select · Tab complete · Enter run · Esc close'
166
174
  };
167
175
  }
168
- return { isOpen, onKey, view, state: st };
176
+ /** The app calls this when Enter submits a command line: the palette closes and that Enter activates nothing. */
177
+ const submitted = (draft = getDraft()) => { st.hold = String(draft ?? ''); st.sel = 0; st.picked = false; };
178
+ return { isOpen, onKey, view, submitted, state: st };
169
179
  }