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.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/events.js
CHANGED
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
* turn : null | { id, status: running|interrupting, startedAt, stage, activity }
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { dividerStep } from './dividerAocl.js';
|
|
15
|
+
import { approvalFacts } from './approvalFacts.js';
|
|
16
|
+
import { isConsentError, consentErrorText } from './consent.js';
|
|
17
|
+
import { splitSettled } from './markdownSettle.js'; // not markdown.js: that loads React + Ink (QUOXON-LATENCY)
|
|
15
18
|
import { nextActivity, actionActivity } from './activity.js';
|
|
16
19
|
import { normUsage, turnTokens, fmtTokens, fmtCost } from './indicator.js';
|
|
20
|
+
import { isLayerTrace, normaliseSummary, summaryFromTraces, verdictNote } from './aocl.js';
|
|
21
|
+
import { toolResultText } from './toolError.js';
|
|
17
22
|
|
|
18
23
|
export const TOOL_STATUSES = ['ok', 'failed', 'denied', 'effect_unknown'];
|
|
19
24
|
export const APPROVAL_ARM_MS = 300;
|
|
@@ -23,10 +28,14 @@ export const HELD_KEY_MS = 1500;
|
|
|
23
28
|
export const initialState = (over = {}) => ({
|
|
24
29
|
history: [],
|
|
25
30
|
blocks: [],
|
|
31
|
+
asks: [], // ASK-SETTLE: open y/N/c ask cards (live, never scrollback) until answered/expired/closed
|
|
26
32
|
turn: null,
|
|
27
33
|
ctx: { agent: null, model: null, provider: null, endpoint: null, hat: null, conversationId: null },
|
|
28
34
|
pending: [], // client-side queue (Tab)
|
|
29
35
|
steering: [], // steer messages not yet accepted
|
|
36
|
+
lastTurnId: null, // the newest turn begun here: a card from an older turn is stale
|
|
37
|
+
lastAocl: null, // QUOXON-AOCL: the last turn's normalised aocl.summary (for /aocl)
|
|
38
|
+
waiting: [], // approvals raised by ANOTHER conversation/session: a notice only, never a focused card (answered via /approvals)
|
|
30
39
|
approvals: [], // approval cards waiting for a human, first is shown; survive the turn that raised them
|
|
31
40
|
running: [], // approved from this terminal, result arrives asynchronously (polled)
|
|
32
41
|
inbox: [], // last /approvals listing
|
|
@@ -46,21 +55,51 @@ export const initialState = (over = {}) => ({
|
|
|
46
55
|
* streams keep using `chunk`. Returns { append, st } where st is { v1, lastChunk }.
|
|
47
56
|
*/
|
|
48
57
|
export function textGate(st, ev) {
|
|
49
|
-
const g = st || { v1: false, lastChunk: null };
|
|
58
|
+
const g = st || { v1: false, lastChunk: null, acc: '' };
|
|
59
|
+
const acc = g.acc || '';
|
|
60
|
+
const add = (c) => (acc + c).slice(-200000);
|
|
50
61
|
if (ev.type === 'turn.started') return { append: false, st: { ...g, v1: true } };
|
|
51
62
|
if (ev.type === 'chunk') {
|
|
52
|
-
|
|
53
|
-
return { append:
|
|
63
|
+
const c = ev.content;
|
|
64
|
+
if (!c) return { append: false, st: g };
|
|
65
|
+
// RESTORE-TUI: on a v1 stream a chunk is skipped only when it mirrors text already shown (the full text so far,
|
|
66
|
+
// or the delta it repeats). A chunk carrying NEW text is still answer text (it was dropped since dae720283).
|
|
67
|
+
if (g.v1 && acc.endsWith(c)) return { append: false, st: g };
|
|
68
|
+
return { append: true, st: { ...g, lastChunk: c, acc: add(c) } };
|
|
54
69
|
}
|
|
55
70
|
if (ev.type === 'text.delta') {
|
|
56
|
-
const
|
|
57
|
-
|
|
71
|
+
const c = ev.content;
|
|
72
|
+
const mirrored = g.lastChunk != null && g.lastChunk === c;
|
|
73
|
+
const append = Boolean(c) && !mirrored;
|
|
74
|
+
return { append, st: { v1: true, lastChunk: null, acc: append ? add(c) : acc } };
|
|
58
75
|
}
|
|
59
76
|
return { append: false, st: g };
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
const tms = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return Number.isFinite(t) ? t : null; };
|
|
63
80
|
|
|
81
|
+
const short = (v, n = 120) => { const t = typeof v === 'string' ? v : (v == null ? '' : JSON.stringify(v)); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
82
|
+
const argText = (a) => a && typeof a === 'object' && !Array.isArray(a) && (a.argv || a.command) ? argText(a.argv ?? a.command) : Array.isArray(a) ? a.join(' ') : (a && typeof a === 'object' ? Object.entries(a).filter(([, v]) => v != null && typeof v !== 'object').slice(0, 4).map(([k, v]) => `${k}=${v}`).join(' ') : (a == null ? '' : String(a)));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Describe ANY inbox approval from what the item carries: the tool or leaf, the command or key args, the target
|
|
86
|
+
* (explicit target, else the org it acts on), and who asked and when. Never invents; empty fields are null.
|
|
87
|
+
*/
|
|
88
|
+
export function describeInboxItem(it) {
|
|
89
|
+
const p = it?.payload || {};
|
|
90
|
+
const c = p.approval_context || {};
|
|
91
|
+
const m = c.method || {};
|
|
92
|
+
const ob = c.on_behalf_of || {};
|
|
93
|
+
const w = p.what && typeof p.what === 'object' ? p.what : {}; // APPROVAL-WHAT: the server's own action/target/summary wins
|
|
94
|
+
const tool = w.action || m.tool || c.tool || c.leaf || p.tool || p.leaf || null;
|
|
95
|
+
const argv = m.argv ?? m.args ?? c.argv ?? c.args ?? p.argv ?? p.args ?? m.command ?? c.command ?? p.command ?? null;
|
|
96
|
+
const cmd = short([m.leaf || c.leaf || p.leaf, argText(argv)].filter(Boolean).join(' ').trim() || null);
|
|
97
|
+
const target = w.target || m.target || c.target || p.target || cmd || w.target_none || c.target_org || c.org_name || p.org_name || c.org_id || p.org_id || null;
|
|
98
|
+
const by = ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null;
|
|
99
|
+
const what = w.summary ? String(w.summary) : (it?.title ? short(String(it.title), 160) : null);
|
|
100
|
+
return { tool, target: target ? short(target, 200) : null, title: what, by: by ? String(by) : null, at: it?.created_at ?? it?.createdAt ?? null };
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
/**
|
|
65
104
|
* A pending inbox approval found by polling -> approval card, or null when it does not qualify.
|
|
66
105
|
* Qualifies when created after `since` (the turn start, else app start), or raised on behalf of this
|
|
@@ -68,19 +107,67 @@ const tms = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return
|
|
|
68
107
|
*/
|
|
69
108
|
export function cardFromInboxItem(it, { since = 0, userId = null, conversationId = null, now = 0 } = {}) {
|
|
70
109
|
if (!it || !it.id) return null;
|
|
110
|
+
if (it.payload?.type === 'decision') return null; // quox ask: answered with /answer N key from /approvals, never a y/n key card
|
|
71
111
|
const c = it.payload?.approval_context || {};
|
|
72
112
|
const m = c.method || {};
|
|
73
113
|
const created = tms(it.created_at ?? it.createdAt);
|
|
74
114
|
const mine = Boolean(userId && c.on_behalf_of?.user_id && String(c.on_behalf_of.user_id) === String(userId));
|
|
75
115
|
const here = Boolean(conversationId && c.on_behalf_of?.conversation_id === conversationId);
|
|
76
116
|
if (!(created != null && created >= since) && !mine && !here) return null;
|
|
117
|
+
// QUOXON-REVIEW: collector tool approvals mint the inbox item with aee_correlation_id = the approval id;
|
|
118
|
+
// without it the card had no id, polling was skipped and every terminal approve read "timeout".
|
|
119
|
+
const toolApproval = it.payload?.type === 'native_tool_approval';
|
|
120
|
+
const approvalId = it.payload?.approval_id || it.approval_id || it.approvalId
|
|
121
|
+
|| (toolApproval ? (it.aee_correlation_id || it.aeeCorrelationId || null) : null) || null;
|
|
122
|
+
const ob = c.on_behalf_of || {};
|
|
123
|
+
const d = describeInboxItem(it);
|
|
124
|
+
const tool = d.tool || String(it.title || it.id);
|
|
77
125
|
return {
|
|
78
|
-
callId: `inbox:${it.id}`, inboxItemId: it.id, approvalId
|
|
79
|
-
tool:
|
|
80
|
-
url: it.dashboard_url || null, shownAt: now, undescribable: !
|
|
126
|
+
callId: `inbox:${it.id}`, inboxItemId: it.id, approvalId,
|
|
127
|
+
tool, target: d.target, what: d.title, askedBy: d.by, askedAt: d.at, scope: null, risk: c.risk || null,
|
|
128
|
+
url: it.dashboard_url || null, shownAt: now, undescribable: !d.tool && !d.target,
|
|
129
|
+
facts: approvalFacts(it), // APPROVAL-WWWH: the exact command, hosts, chain, reason, time and read-only class
|
|
130
|
+
// only an item that names THIS conversation is ours; anything else came from another session and must not take the keyboard
|
|
131
|
+
foreign: !here,
|
|
132
|
+
origin: String(ob.agent || ob.agent_id || ob.session_name || ob.session || (ob.conversation_id ? `conversation ${String(ob.conversation_id).slice(0, 8)}` : approvalSourceLabel(it))),
|
|
133
|
+
privileged: isPrivileged({ tool, risk: c.risk, flag: c.privileged === true || c.elevated === true || it.payload?.privileged === true, title: it.title })
|
|
81
134
|
};
|
|
82
135
|
}
|
|
83
136
|
|
|
137
|
+
/** Where an inbox approval came from: its source label, else the payload source, else its type. */
|
|
138
|
+
export function approvalSourceLabel(it) {
|
|
139
|
+
const c = it?.payload?.approval_context || {};
|
|
140
|
+
return String(c.source_label || it?.payload?.source || it?.source || (c.requested_by && c.requested_by.agent_label) || it?.payload?.type || 'plugin or workflow');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* QUOXON-FW7: every open inbox approval, grouped for /approvals. 'this conversation' (names this conversation,
|
|
145
|
+
* or already a card here), 'other sessions' (raised on behalf of another conversation or agent), and
|
|
146
|
+
* 'plugins and workflows' (everything else, labelled by source). Empty groups are dropped.
|
|
147
|
+
*/
|
|
148
|
+
export function groupApprovals(items, { conversationId = null, cardIds = [] } = {}) {
|
|
149
|
+
const g = { 'decisions asked of you': [], 'this conversation': [], 'other sessions': [], 'plugins and workflows': [] };
|
|
150
|
+
for (const item of items || []) {
|
|
151
|
+
if (item?.payload?.type === 'decision') { g['decisions asked of you'].push({ item, label: String(item.payload.session || 'session') }); continue; } // quox ask
|
|
152
|
+
const ob = item?.payload?.approval_context?.on_behalf_of || {};
|
|
153
|
+
const label = approvalSourceLabel(item);
|
|
154
|
+
// A card that names a conversation belongs here only when it is THIS one (a card shown in this window for another
|
|
155
|
+
// conversation, e.g. a privileged hat.switch, is still "other sessions"). No conversation named: a card here counts.
|
|
156
|
+
const mine = ob.conversation_id ? Boolean(conversationId) && ob.conversation_id === conversationId : cardIds.includes(item.id);
|
|
157
|
+
if (mine) g['this conversation'].push({ item, label });
|
|
158
|
+
else if (ob.conversation_id || ob.agent || ob.agent_id || ob.session_name || ob.session) g['other sessions'].push({ item, label: String(ob.session_name || ob.session || ob.agent || ob.agent_id || `conversation ${String(ob.conversation_id).slice(0, 8)}`) });
|
|
159
|
+
else g['plugins and workflows'].push({ item, label });
|
|
160
|
+
}
|
|
161
|
+
return Object.entries(g).filter(([, v]) => v.length).map(([group, v]) => ({ group, items: v }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Privileged/elevated grants (hat switches and anything the server marks privileged or elevated) need an explicit confirm. */
|
|
165
|
+
export function isPrivileged({ tool = '', risk = null, flag = false, title = '' } = {}) {
|
|
166
|
+
if (flag === true) return true;
|
|
167
|
+
if (String(tool) === 'hat.switch' || /^hat[ .]switch/i.test(String(title))) return true;
|
|
168
|
+
return /privileged|elevated|critical/i.test(String(risk || ''));
|
|
169
|
+
}
|
|
170
|
+
|
|
84
171
|
let uid = 0;
|
|
85
172
|
const nid = () => ++uid;
|
|
86
173
|
|
|
@@ -101,6 +188,11 @@ function flush(s, force = false) {
|
|
|
101
188
|
return out.length || blocks.length !== s.blocks.length ? { ...s, history: [...s.history, ...out], blocks } : s;
|
|
102
189
|
}
|
|
103
190
|
|
|
191
|
+
/** Fold the turn's VOLT tool count and list into its AOCL summary (footer count, /aocl list). */
|
|
192
|
+
function withTools(sum, turn) {
|
|
193
|
+
const t = turn?.toolsOffer;
|
|
194
|
+
return t && t.offered != null ? { ...sum, toolsOffered: t.offered, toolsTotal: t.total, toolsList: t.list } : sum;
|
|
195
|
+
}
|
|
104
196
|
function note(s, text, tone = 'dim') {
|
|
105
197
|
return { ...s, history: [...s.history, { id: nid(), kind: 'note', text, tone }] };
|
|
106
198
|
}
|
|
@@ -133,6 +225,14 @@ function appendText(s00, content) {
|
|
|
133
225
|
return addBlock(s, { kind: 'text', text: content });
|
|
134
226
|
}
|
|
135
227
|
|
|
228
|
+
/** QUOXON-FW3: who a delegation targets. Server fields first; older servers only put agent_id=<id> in the summary. */
|
|
229
|
+
export function delegateInfo(ev) {
|
|
230
|
+
if (ev.tool !== 'delegate_to_agent') return {};
|
|
231
|
+
const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || null;
|
|
232
|
+
if (!id) return {};
|
|
233
|
+
return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}) };
|
|
234
|
+
}
|
|
235
|
+
|
|
136
236
|
function upsertTool(s, ev, patch) {
|
|
137
237
|
const i = s.blocks.findIndex(b => b.kind === 'tool' && b.callId === ev.call_id);
|
|
138
238
|
if (i >= 0) {
|
|
@@ -172,7 +272,8 @@ export function summarizeTurn(s, status, now) {
|
|
|
172
272
|
// Real server usage when it arrived; otherwise the estimate from streamed text; nothing when neither exists.
|
|
173
273
|
const tk = turnTokens(t);
|
|
174
274
|
if (tk > 0) parts.push(`${fmtTokens(tk)} tokens`);
|
|
175
|
-
|
|
275
|
+
// A labelled estimate (cost_source, e.g. the claude CLI's own figure) is never shown as this turn's price; /cost explains it.
|
|
276
|
+
const cost = t?.usage?.costSource || t?.viaCli ? null : fmtCost(t?.usage?.cost ?? normUsage(null, t?.doneCost).cost);
|
|
176
277
|
if (cost) parts.push(cost);
|
|
177
278
|
if (status === 'interrupted' || status === 'failed') {
|
|
178
279
|
if (t?.tools) parts.push(`${plural(finished, 'tool')} finished`);
|
|
@@ -215,15 +316,47 @@ function endTurn(s, status, now) {
|
|
|
215
316
|
function reduceInner(s, a) {
|
|
216
317
|
switch (a.type) {
|
|
217
318
|
case 'user': // a submitted prompt
|
|
218
|
-
return { ...s, history: [...s.history, { id: nid(), kind: 'user', text: a.text, queued: !!a.queued }] };
|
|
319
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'user', text: a.text, queued: !!a.queued, ...(a.echo ? { echo: true } : {}) }] };
|
|
219
320
|
case 'turn.begin':
|
|
220
|
-
return { ...s, turn: { id: a.turnId, status: 'running', startedAt: a.now, stage: null, activity: null, tools: 0, finishedTools: 0, approvals: 0, receipts: [], chars: 0, usage: null }, blocks: [], autoDrain: false };
|
|
321
|
+
return { ...s, lastTurnId: a.turnId, turn: { id: a.turnId, status: 'running', startedAt: a.now, stage: null, activity: null, tools: 0, finishedTools: 0, approvals: 0, receipts: [], chars: 0, usage: null }, blocks: [], autoDrain: false };
|
|
221
322
|
case 'note':
|
|
222
323
|
return note(s, a.text, a.tone);
|
|
324
|
+
// ASK-SETTLE (owner 2026-10-17): an open ask is a live card; settling it removes the card and leaves one line.
|
|
325
|
+
case 'ask.open': { // the same ask shown again (after a chat about it) replaces its card, never a second one
|
|
326
|
+
const asks = (s.asks || []).filter(x => x.text !== a.text);
|
|
327
|
+
return { ...s, asks: [...asks, { id: a.id, text: a.text, at: a.at ?? null }] };
|
|
328
|
+
}
|
|
329
|
+
case 'ask.settle': {
|
|
330
|
+
const asks = s.asks || [];
|
|
331
|
+
if (!asks.some(x => x.id === a.id)) return s;
|
|
332
|
+
if (a.silent) return { ...s, asks: asks.filter(x => x.id !== a.id) }; // a walkthrough card that ended normally: no leftover line
|
|
333
|
+
return note({ ...s, asks: asks.filter(x => x.id !== a.id) }, a.text, a.tone);
|
|
334
|
+
}
|
|
335
|
+
case 'slotmsg.settle': { // SLOT-RUN: an answered/expired run request collapses to one line in place
|
|
336
|
+
let hit = false;
|
|
337
|
+
const history = s.history.map(it => {
|
|
338
|
+
if (it.kind !== 'slotmsg' || !it.line?.run || it.line.run.askId !== a.askId || it.line.run.settled) return it;
|
|
339
|
+
hit = true;
|
|
340
|
+
return { ...it, line: { ...it.line, run: { ...it.line.run, settled: a.text, settledTone: a.tone } } };
|
|
341
|
+
});
|
|
342
|
+
return hit ? { ...s, history } : s;
|
|
343
|
+
}
|
|
344
|
+
case 'bang.out': // QUOXON-FW8: command output, shown as is (not note-painted); the transcript records only the action name
|
|
345
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'bang', text: a.text }] };
|
|
346
|
+
case 'slotmsg': // QUOXON-MESH: a slot chat line (slotChat.js slotLine), incoming or sent
|
|
347
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'slotmsg', line: a.line }] };
|
|
348
|
+
case 'tip': // a "did you know" tip (tips.js); text + the command to try
|
|
349
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'tip', text: a.text, cmd: a.cmd }] };
|
|
350
|
+
case 'bcard': // QUOXON-BROADCAST: the sender's framed delivery card (slash/broadcast.js broadcastCard), pure data
|
|
351
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'bcard', card: a.card }] };
|
|
352
|
+
case 'whatsnew': // the startup "What's new" box (whatsnew.js): [{ title, cmd }] + how many more
|
|
353
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'whatsnew', items: a.items, more: a.more, head: a.head, pad: a.pad }] };
|
|
223
354
|
case 'notify':
|
|
224
355
|
return { ...s, notify: { ...s.notify, ...a.patch } };
|
|
225
356
|
case 'welcome':
|
|
226
357
|
return { ...s, history: [...s.history, { id: nid(), kind: 'welcome', version: a.version }] };
|
|
358
|
+
case 'introShow': // /welcome: the first screen again (Q art + the greeting text), settled in history
|
|
359
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'intro', text: a.text, cols: a.cols }] };
|
|
227
360
|
case 'help':
|
|
228
361
|
return { ...s, history: [...s.history, { id: nid(), kind: 'help', model: a.model }] };
|
|
229
362
|
case 'object': // inline object view (objects/): pure data, rebuilt at render width
|
|
@@ -232,6 +365,8 @@ function reduceInner(s, a) {
|
|
|
232
365
|
return flush(addBlock(s, { kind: 'error', text: a.text }), false);
|
|
233
366
|
case 'verbose':
|
|
234
367
|
return { ...s, verbose: a.mode };
|
|
368
|
+
case 'aocl.table': // /aocl: the last turn's per-layer table, on demand
|
|
369
|
+
return { ...s, history: [...s.history, { id: nid(), kind: 'aocl', table: true, sum: s.lastAocl || null }] };
|
|
235
370
|
case 'history.add': // restored turns (/resume): ordinary history items, plus their answers for /copy
|
|
236
371
|
return { ...s, history: [...s.history, ...(a.items || []).map(i => ({ id: nid(), ...i }))], answers: [...(s.answers || []), ...(a.answers || [])].slice(-20) };
|
|
237
372
|
case 'history.reset': // /clear: a fresh screen; nothing on the server is touched
|
|
@@ -273,20 +408,47 @@ function reduceInner(s, a) {
|
|
|
273
408
|
}
|
|
274
409
|
case 'interrupt.requested':
|
|
275
410
|
return s.turn ? { ...s, turn: { ...s.turn, status: 'interrupting' } } : s;
|
|
411
|
+
case 'turn.deferred': // QUOXON-FW3: the send never reached the server; end the turn quietly (no failure summary, nothing ran)
|
|
412
|
+
return { ...s, turn: null, blocks: [], autoDrain: false };
|
|
276
413
|
case 'stopped.local': { // SSE aborted by us because the server has no interrupt route
|
|
277
414
|
let n = note(s, 'Stopped locally; work already sent may still finish. Verify before retrying.', 'warn');
|
|
278
415
|
return endTurn(n, 'interrupted', a.now);
|
|
279
416
|
}
|
|
280
417
|
case 'approval.found': { // pending approval seen by polling (covers provider paths that emit no approval.requested)
|
|
281
418
|
const card = cardFromInboxItem(a.item, { since: s.turn?.startedAt ?? a.since ?? 0, userId: a.userId, conversationId: s.ctx.conversationId, now: a.now || 0 });
|
|
419
|
+
if (card && card.approvalId) { // an event card raised without an inbox id learns it from the poll (else y answers a null id: 404)
|
|
420
|
+
const j = s.approvals.findIndex(x => !x.inboxItemId && x.approvalId === card.approvalId);
|
|
421
|
+
if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
|
|
422
|
+
}
|
|
423
|
+
if (card && card.approvalId) { // an event stub that could not be described learns its description from the inbox item
|
|
424
|
+
const j = s.approvals.findIndex(x => x.undescribable && (x.inboxItemId === card.inboxItemId || x.approvalId === card.approvalId));
|
|
425
|
+
if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, undescribable: false }; return { ...s, approvals }; }
|
|
426
|
+
}
|
|
282
427
|
if (!card || s.seenInbox[card.inboxItemId] || s.approvals.some(x => x.inboxItemId === card.inboxItemId || (card.approvalId && x.approvalId === card.approvalId))) return s;
|
|
283
|
-
|
|
428
|
+
if (card.foreign) { // another conversation's card: a compact notice, never the focused card
|
|
429
|
+
if (s.waiting.some(x => x.inboxItemId === card.inboxItemId)) return s;
|
|
430
|
+
return { ...s, waiting: [...s.waiting, card] };
|
|
431
|
+
}
|
|
432
|
+
return { ...s, approvals: [...s.approvals, { ...card, turnId: s.turn?.id ?? null }], seenInbox: { ...s.seenInbox, [card.inboxItemId]: true }, turn: s.turn ? { ...s.turn, approvals: s.turn.approvals + 1 } : s.turn };
|
|
433
|
+
}
|
|
434
|
+
case 'approval.gone': { // the server says it no longer knows this approval (404): someone already handled it
|
|
435
|
+
const hit = x => x.callId === a.callId || (a.inboxItemId && x.inboxItemId === a.inboxItemId);
|
|
436
|
+
const had = s.approvals.some(hit) || s.waiting.some(hit);
|
|
437
|
+
const n = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), approvals: s.approvals.filter(x => !hit(x)), waiting: s.waiting.filter(x => !hit(x)), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox };
|
|
438
|
+
return had ? note(n, 'approval already handled', 'accent') : n;
|
|
284
439
|
}
|
|
285
440
|
case 'approval.dismiss':
|
|
286
441
|
return { ...s, approvals: s.approvals.filter(x => x.callId !== a.callId) };
|
|
287
442
|
case 'approval.sent': { // the human answered from this terminal
|
|
288
|
-
const rest = { ...s, approvals: s.approvals.filter(x => x.callId !== a.callId) };
|
|
443
|
+
const rest = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox, approvals: s.approvals.filter(x => x.callId !== a.callId && !(a.inboxItemId && x.inboxItemId === a.inboxItemId)), waiting: s.waiting.filter(x => x.callId !== a.callId && x.inboxItemId !== a.callId) };
|
|
444
|
+
if (a.batch) return rest; // /approve N prints its own one-line summary
|
|
445
|
+
if (a.quiet && a.action === 'approve') return rest;
|
|
289
446
|
if (a.action !== 'approve') return note(rest, `denied: ${a.tool || 'approval'}${a.target ? ` on ${a.target}` : ''}`, 'warn');
|
|
447
|
+
if (a.foreign) return note(rest, `✔ Approved ${a.tool || 'approval'}${a.target ? ` on ${a.target}` : ''}; the plugin or agent will act on it (its outcome is not tracked here)`, 'ok');
|
|
448
|
+
if (a.tool === 'hat.switch') { // applied server-side by the inbox approve; nothing to wait for
|
|
449
|
+
const to = String(a.target || '').split('\u2192').pop().replace(/\(privileged\)/, '').trim() || 'the new hat';
|
|
450
|
+
return note(rest, `Hat switched to ${to}. Ask again and I'll carry on.`, 'ok');
|
|
451
|
+
}
|
|
290
452
|
const run = { callId: a.callId, approvalId: a.approvalId || null, tool: a.tool, target: a.target };
|
|
291
453
|
return note({ ...rest, running: [...rest.running.filter(x => x.callId !== a.callId), run] },
|
|
292
454
|
`approved; running (result arrives asynchronously)${a.tool ? `: ${a.tool}${a.target ? ` on ${a.target}` : ''}` : ''}`, 'ok');
|
|
@@ -298,12 +460,15 @@ function reduceInner(s, a) {
|
|
|
298
460
|
if (a.outcome === 'done') return note(n, `result: ${what}: ${a.text || 'completed'}`, 'ok');
|
|
299
461
|
if (a.outcome === 'failed') return note(n, `failed: ${what}: ${a.text || 'error'}`, 'warn');
|
|
300
462
|
if (a.outcome === 'denied') return note(n, `denied: ${what}`, 'warn');
|
|
301
|
-
|
|
463
|
+
// FW4: never silent and never a guess. Say what is unknown and where the authoritative answer is.
|
|
464
|
+
const check = run && run.approvalId ? `quox approvals result ${run.approvalId} (or /approvals)` : '/approvals (no approval id was returned, so it cannot be tracked from here)';
|
|
465
|
+
return note(n, `result not known yet: ${what}; approved, but the server has not said whether it ran. It may still be running or may have run. Check: ${check}`, 'warn');
|
|
302
466
|
}
|
|
303
467
|
case 'inbox.sync': { // open approval ids from a successful inbox refresh
|
|
304
468
|
const ids = new Set(a.ids);
|
|
305
469
|
const approvals = s.approvals.filter(x => !x.inboxItemId || ids.has(x.inboxItemId) || (a.now || 0) - x.shownAt < 5000);
|
|
306
|
-
|
|
470
|
+
const waiting = s.waiting.filter(x => ids.has(x.inboxItemId));
|
|
471
|
+
return approvals.length === s.approvals.length && waiting.length === s.waiting.length ? s : { ...s, approvals, waiting };
|
|
307
472
|
}
|
|
308
473
|
case 'event':
|
|
309
474
|
return reduceEvent(s, a.ev, a.now || 0);
|
|
@@ -315,6 +480,11 @@ function reduceInner(s, a) {
|
|
|
315
480
|
/** The first card is armed from the moment it is actually SHOWN, not from when it was received. */
|
|
316
481
|
export function reduce(s, a) {
|
|
317
482
|
let n = reduceInner(s, a);
|
|
483
|
+
// TUI-DIVIDER: the bottom-rule AOCL strip, moved only by real turn events (dividerAocl.js).
|
|
484
|
+
if (a.type === 'turn.begin' || a.type === 'event') {
|
|
485
|
+
const dv = dividerStep(s.divider ?? null, a, a.now || 0);
|
|
486
|
+
if (dv !== (n.divider ?? null)) n = { ...n, divider: dv };
|
|
487
|
+
}
|
|
318
488
|
// The honest activity word: only real events move it (see activity.js).
|
|
319
489
|
if (n.turn) {
|
|
320
490
|
const act = a.type === 'event' ? nextActivity(s.turn?.act, a.ev, a.now || 0, { hasText: Boolean(n.turn.chars) })
|
|
@@ -331,12 +501,15 @@ export function reduce(s, a) {
|
|
|
331
501
|
function reduceEvent(s, ev, now) {
|
|
332
502
|
switch (ev.type) {
|
|
333
503
|
case 'connected': return s;
|
|
334
|
-
//
|
|
504
|
+
// background.status is not in events v1 yet; handled so the line works the moment the server sends it (RESTORE-TUI: 4fbd3178c dropped it).
|
|
505
|
+
case 'background.status': return { ...s, background: Math.max(0, Number(ev.pending) || 0) };
|
|
335
506
|
case 'turn.started': {
|
|
336
507
|
const ctx = {
|
|
337
508
|
agent: ev.agent ?? s.ctx.agent, model: ev.model ?? s.ctx.model, provider: ev.provider ?? s.ctx.provider,
|
|
338
509
|
// B3: the host a self-hosted provider ran at; cleared when the server names none.
|
|
339
510
|
endpoint: ev.provider ? (ev.endpoint ?? null) : (s.ctx.endpoint ?? null),
|
|
511
|
+
// QUOXON-PARITY: which Claude path ran (subscription CLI vs API key); cleared like endpoint.
|
|
512
|
+
auth: ev.provider ? (ev.auth ?? null) : (s.ctx.auth ?? null),
|
|
340
513
|
hat: ev.hat ?? s.ctx.hat, conversationId: ev.conversation_id ?? s.ctx.conversationId
|
|
341
514
|
};
|
|
342
515
|
// The model THIS turn actually used, as the server reported it (never the profile's default).
|
|
@@ -378,13 +551,14 @@ function reduceEvent(s, ev, now) {
|
|
|
378
551
|
case 'replay.gap':
|
|
379
552
|
return addBlock(s, { kind: 'note', text: `Events ${ev.from} to ${ev.to} of this turn were not kept on the server (size cap); the rest follows.`, tone: 'warn' });
|
|
380
553
|
case 'tool.started':
|
|
381
|
-
return upsertTool(s, ev, { status: 'running', summary: ev.summary || ev.tool, startedAt: now });
|
|
554
|
+
return upsertTool(s, ev, { status: 'running', summary: ev.summary || ev.tool, startedAt: now, ...delegateInfo(ev) });
|
|
382
555
|
case 'tool.finished': {
|
|
383
556
|
const status = TOOL_STATUSES.includes(ev.status) ? ev.status : 'unknown'; // never green for a status we do not know
|
|
384
557
|
const first = !s.doneCalls[ev.call_id] && ['ok', 'failed', 'denied'].includes(status);
|
|
385
558
|
let withTool = upsertTool(s, ev, {
|
|
386
|
-
status, resultSummary: ev.result_summary, durationMs: ev.duration_ms,
|
|
387
|
-
envelopeId: ev.envelope_id, receipt: ev.receipt || null
|
|
559
|
+
status, resultSummary: toolResultText(ev.result_summary, s.verbose), rawSummary: ev.result_summary, durationMs: ev.duration_ms,
|
|
560
|
+
envelopeId: ev.envelope_id, receipt: ev.receipt || null,
|
|
561
|
+
...(Number.isFinite(ev.tools_used) ? { toolsUsed: ev.tools_used } : {})
|
|
388
562
|
});
|
|
389
563
|
if (first && withTool.turn) withTool = { ...withTool, turn: { ...withTool.turn, finishedTools: withTool.turn.finishedTools + 1 } };
|
|
390
564
|
if (ev.receipt && ev.receipt.verified === true && withTool.turn && ev.receipt.ward_seq != null) {
|
|
@@ -396,7 +570,9 @@ function reduceEvent(s, ev, now) {
|
|
|
396
570
|
const card = {
|
|
397
571
|
callId: ev.call_id, inboxItemId: ev.inbox_item_id, approvalId: ev.approval_id, tool: ev.tool,
|
|
398
572
|
target: ev.target, scope: ev.scope, risk: ev.risk, url: ev.dashboard_url, shownAt: now,
|
|
399
|
-
undescribable: ev.describable === false || !ev.target
|
|
573
|
+
undescribable: ev.describable === false || !ev.target,
|
|
574
|
+
turnId: s.turn?.id ?? ev.turn_id ?? null, // the turn this card arrived in (a stream event is always this conversation)
|
|
575
|
+
privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true })
|
|
400
576
|
};
|
|
401
577
|
if (s.approvals.some(x => x.callId === card.callId)) return s;
|
|
402
578
|
const dup = s.approvals.findIndex(x => (card.inboxItemId && x.inboxItemId === card.inboxItemId) || (card.approvalId && x.approvalId === card.approvalId));
|
|
@@ -404,13 +580,15 @@ function reduceEvent(s, ev, now) {
|
|
|
404
580
|
const approvals = s.approvals.slice(); approvals[dup] = { ...approvals[dup], callId: card.callId };
|
|
405
581
|
return { ...s, approvals };
|
|
406
582
|
}
|
|
583
|
+
const wi = card.inboxItemId ? s.waiting.findIndex(x => x.inboxItemId === card.inboxItemId) : -1;
|
|
584
|
+
if (wi >= 0) s = { ...s, waiting: s.waiting.filter((_, i) => i !== wi) }; // the stream says it is ours after all
|
|
407
585
|
if (card.inboxItemId && s.seenInbox[card.inboxItemId]) return s; // already shown and answered
|
|
408
586
|
return { ...s, seenInbox: card.inboxItemId ? { ...s.seenInbox, [card.inboxItemId]: true } : s.seenInbox, approvals: [...s.approvals, card], turn: s.turn ? { ...s.turn, approvals: s.turn.approvals + 1 } : s.turn };
|
|
409
587
|
}
|
|
410
588
|
case 'approval.resolved': {
|
|
411
589
|
const card = s.approvals.find(x => x.callId === ev.call_id);
|
|
412
590
|
const n = { ...s, approvals: s.approvals.filter(x => x.callId !== ev.call_id) };
|
|
413
|
-
return card || ev.decision ? note(n, `approval ${ev.decision || 'resolved'}${card ? `: ${card.tool} on ${card.target}` : ''}`, ev.decision === 'approved' ? 'ok' : 'warn') : n;
|
|
591
|
+
return card || ev.decision ? note(n, `${ev.decision === 'approved' ? '✔ approved by you' : `approval ${ev.decision || 'resolved'}`}${card ? `: ${card.tool} on ${card.target}` : ''}`, ev.decision === 'approved' ? 'ok' : 'warn') : n;
|
|
414
592
|
}
|
|
415
593
|
case 'steer.accepted': {
|
|
416
594
|
const i = s.steering.findIndex(x => x.text === ev.text);
|
|
@@ -418,10 +596,27 @@ function reduceEvent(s, ev, now) {
|
|
|
418
596
|
return addBlock({ ...s, steering }, { kind: 'note', text: `steered: ${ev.text}`, tone: 'accent' });
|
|
419
597
|
}
|
|
420
598
|
case 'trace':
|
|
599
|
+
// QUOXON-AOCL: per-layer AOCL rows fold into ONE line per turn (aocl.summary); /aocl shows the table.
|
|
600
|
+
if (ev.protocol === 'volt' && /tools offered/i.test(ev.label || '')) { // kept for the footer count and /aocl, shown only in verbose
|
|
601
|
+
const m = String(ev.label).match(/(\d+)\s+of\s+(\d+)/);
|
|
602
|
+
const list = String(ev.detail || '');
|
|
603
|
+
const toolsOffered = m ? Number(m[1]) : (list ? list.split(/,\s*/).filter(Boolean).length : null);
|
|
604
|
+
if (s.turn) s = { ...s, turn: { ...s.turn, toolsOffer: { offered: toolsOffered, total: m ? Number(m[2]) : null, list } } };
|
|
605
|
+
}
|
|
606
|
+
if (isLayerTrace(ev)) return s.turn ? { ...s, turn: { ...s.turn, aoclTraces: [...(s.turn.aoclTraces || []), { label: ev.label, detail: ev.detail }] } } : s;
|
|
421
607
|
if (s.verbose === 'off') return s;
|
|
422
608
|
return addBlock(s, { kind: 'trace', protocol: ev.protocol, label: ev.label, ref: ev.ref, detail: ev.detail });
|
|
609
|
+
case 'aocl.summary': {
|
|
610
|
+
const sum = withTools(normaliseSummary(ev), s.turn);
|
|
611
|
+
const n = { ...s, lastAocl: sum, turn: s.turn ? { ...s.turn, aoclSummary: true } : s.turn };
|
|
612
|
+
return addBlock(n, { kind: 'aocl', sum }); // the one-line footer shows at every verbosity
|
|
613
|
+
}
|
|
614
|
+
case 'aocl.verdict': {
|
|
615
|
+
const v = verdictNote(ev);
|
|
616
|
+
return v.tone === 'error' ? addBlock(s, { kind: 'error', text: v.text }) : addBlock(s, { kind: 'note', text: v.text, tone: v.tone });
|
|
617
|
+
}
|
|
423
618
|
case 'error': {
|
|
424
|
-
const n = addBlock(s, { kind: 'error', text: String(ev.error || 'stream error') });
|
|
619
|
+
const n = addBlock(s, { kind: 'error', text: isConsentError(ev) ? consentErrorText(ev) : String(ev.error || 'stream error') });
|
|
425
620
|
return n;
|
|
426
621
|
}
|
|
427
622
|
case 'turn.finished': case 'done': {
|
|
@@ -435,8 +630,16 @@ function reduceEvent(s, ev, now) {
|
|
|
435
630
|
// `done` only carries text/session; turn.finished (sent just after it) ends the turn. A legacy
|
|
436
631
|
// server never sends turn.finished: the app synthesises one when the stream closes.
|
|
437
632
|
// usage: `done` may carry cost_usd; turn.finished carries `usage` (input/output tokens, maybe cost_usd).
|
|
438
|
-
|
|
439
|
-
|
|
633
|
+
// cli_session_id on `done` = the claude CLI path: its cost is that CLI's own estimate even from a server that
|
|
634
|
+
// does not label it yet (cost_source 'claude-cli', R3 D5).
|
|
635
|
+
if (ev.type === 'done') return { ...n, turn: { ...n.turn, doneCost: ev.cost_usd, viaCli: Boolean(ev.cli_session_id) } };
|
|
636
|
+
// An older server sends per-layer AOCL traces and no aocl.summary: fold them into the one line here.
|
|
637
|
+
if (!n.turn.aoclSummary && n.turn.aoclTraces?.length) {
|
|
638
|
+
const sum = withTools(summaryFromTraces(n.turn.aoclTraces), n.turn);
|
|
639
|
+
n = { ...n, lastAocl: sum };
|
|
640
|
+
n = addBlock(n, { kind: 'aocl', sum });
|
|
641
|
+
}
|
|
642
|
+
n = { ...n, turn: { ...n.turn, usage: normUsage(n.turn.viaCli && ev.usage && !ev.usage.cost_source ? { ...ev.usage, cost_source: 'claude-cli' } : ev.usage, n.turn.doneCost) } };
|
|
440
643
|
return endTurn(n, ev.status || 'completed', now);
|
|
441
644
|
}
|
|
442
645
|
default:
|
|
@@ -456,3 +659,19 @@ export function approvalKeyAllowed({ now, shownAt, lastKeyAt, heldKey = null, ch
|
|
|
456
659
|
if (heldKey != null && ch === heldKey && (lastKeyAt == null || now - lastKeyAt < HELD_KEY_MS)) return false;
|
|
457
660
|
return true;
|
|
458
661
|
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* What an approval is for, without repeating who asked: titles often start "<who> wants to: ..." (the source label
|
|
665
|
+
* already says who), and a generic "Approval required" title falls back to the item's summary or description.
|
|
666
|
+
*/
|
|
667
|
+
export function approvalWhat(it) {
|
|
668
|
+
const w = it?.payload?.what;
|
|
669
|
+
if (w && typeof w === 'object' && typeof w.summary === 'string' && w.summary.trim()) { // APPROVAL-WHAT: the server says what will happen
|
|
670
|
+
const s = w.summary.trim(); const tg = typeof w.target === 'string' ? w.target.trim() : '';
|
|
671
|
+
return tg && !s.includes(tg) ? `${s} (target: ${tg})` : (!tg && w.target_none ? `${s} (${String(w.target_none).trim()})` : s);
|
|
672
|
+
}
|
|
673
|
+
let t = String(it?.title || it?.id || '').replace(/^[^:]{0,80}\bwants to:\s*/i, '').trim();
|
|
674
|
+
const more = it?.summary || it?.description || it?.detail || '';
|
|
675
|
+
if ((!t || /^approval (?:required|needed)\b/i.test(t)) && more) t = String(more).trim();
|
|
676
|
+
return t || String(it?.id || 'approval');
|
|
677
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /feedback: tell the Quox team something, through the public quox.ai feedback API
|
|
3
|
+
* (POST https://api.quox.ai/api/feedback, anonymous, 5 per hour per IP; lands in the quox.ai admin feedback view).
|
|
4
|
+
* /gripe stays the GitHub path. What is sent is type + message (the CLI version is appended to the message)
|
|
5
|
+
* and nothing else: no name, email, org, profile or token. The user sees the exact payload and says y first.
|
|
6
|
+
*/
|
|
7
|
+
export const FEEDBACK_URL = 'https://api.quox.ai/api/feedback';
|
|
8
|
+
const MIN = 5, MAX = 5000; // the endpoint refuses outside these (api/routes/feedback.js in quox-website)
|
|
9
|
+
const TIMEOUT_MS = 15000;
|
|
10
|
+
|
|
11
|
+
/** -> { type:'feedback', message } | null when the endpoint would refuse it. */
|
|
12
|
+
export function feedbackPayload(text, version) {
|
|
13
|
+
const body = String(text || '').trim();
|
|
14
|
+
if (body.length < MIN) return null;
|
|
15
|
+
const message = `${body}\n\n(sent with /feedback from quox CLI ${version || 'unknown'})`;
|
|
16
|
+
if (message.length > MAX) return null;
|
|
17
|
+
return { type: 'feedback', message };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The lines shown before asking: where it goes and exactly what is in the body. */
|
|
21
|
+
export function feedbackPreview(payload, url = FEEDBACK_URL) {
|
|
22
|
+
return [
|
|
23
|
+
`POST ${url} (the Quox team at quox.ai, not your Quox server)`,
|
|
24
|
+
'type: feedback',
|
|
25
|
+
'message:',
|
|
26
|
+
...String(payload.message).split('\n').map(l => ` ${l}`),
|
|
27
|
+
'Nothing else: no name, email, org or token.',
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** -> { ok:true, id, url } | { ok:false, message }. Status is checked before the body is trusted. */
|
|
32
|
+
export async function sendFeedback(payload, { fetchImpl = globalThis.fetch, url = FEEDBACK_URL, version = null, timeoutMs = TIMEOUT_MS } = {}) {
|
|
33
|
+
const ctl = new AbortController();
|
|
34
|
+
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetchImpl(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': `quox-cli/${version || 'unknown'}` },
|
|
39
|
+
body: JSON.stringify({ type: payload.type, message: payload.message }),
|
|
40
|
+
signal: ctl.signal,
|
|
41
|
+
});
|
|
42
|
+
let data = null;
|
|
43
|
+
try { data = await res.json(); } catch { data = null; }
|
|
44
|
+
if (!res.ok) return { ok: false, message: `HTTP ${res.status}: ${(data && data.error) || 'the feedback API refused it'}` };
|
|
45
|
+
if (!data || data.success !== true) return { ok: false, message: `HTTP ${res.status} but the reply did not confirm it was stored` };
|
|
46
|
+
return { ok: true, id: data.id ?? null, url };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, message: e?.name === 'AbortError' ? `no answer from ${url} within ${Math.round(timeoutMs / 1000)} s` : (e?.message || 'failed') };
|
|
49
|
+
} finally { clearTimeout(t); }
|
|
50
|
+
}
|
package/lib/quoxon/gallery.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `quox motion`: the product version of the animation lab (~/quoxon-work/intro/lab). Plays every Q motion
|
|
3
|
-
* with a caption: Orbit & Lock, Portal, Particle collapse, the
|
|
3
|
+
* with a caption: Orbit & Lock, Portal, Particle collapse, the perpetual status Q in its
|
|
4
4
|
* four states, the hello, the composer ring and the reveal effects. No server, no account, nothing sent.
|
|
5
5
|
*
|
|
6
6
|
* Scenes are pure: galleryScenes() returns { id, title, caption, ms, frame(t) -> ANSI lines }. runGallery()
|
|
7
7
|
* is the only part that touches a terminal (cursor, timers, signals).
|
|
8
8
|
*/
|
|
9
9
|
import {
|
|
10
|
-
labSize, orbitCellsAt, portalCellsAt, particleCellsAt,
|
|
11
|
-
INTRO_MS, PORTAL_MS,
|
|
10
|
+
labSize, orbitCellsAt, portalCellsAt, particleCellsAt, cellsToAnsi, qCells,
|
|
11
|
+
INTRO_MS, PORTAL_MS, SKY, VIOLET, AMBER,
|
|
12
12
|
} from './qart.js';
|
|
13
13
|
import { qGlyph, glyphAnsi, GLYPH_STATES } from './qglyph.js';
|
|
14
14
|
import { helloCells, helloAnsi, helloGradient, helloMs } from './qhello.js';
|
|
15
|
+
import { CLIPS, MOOD_CLIPS, clipCells, clipCaption } from './qdragonScenes.js';
|
|
16
|
+
import { isLight } from './theme.js';
|
|
15
17
|
|
|
16
18
|
const RESET = '\x1b[0m';
|
|
17
19
|
const fg = (rgb) => `\x1b[38;2;${rgb.map((v) => Math.max(0, Math.min(255, Math.round(v)))).join(';')}m`;
|
|
@@ -125,8 +127,12 @@ export const PERP_MS = 2600;
|
|
|
125
127
|
* The gallery's scenes for a terminal. narrow forces the 40-column layout (the compact particle Q).
|
|
126
128
|
* Each scene: { id, title, caption, ms, frame(t, { mono }) -> string[] }.
|
|
127
129
|
*/
|
|
128
|
-
export function galleryScenes({ cols = 100, rows = 30, narrow = false } = {}) {
|
|
130
|
+
export function galleryScenes({ cols = 100, rows = 30, narrow = false, theme = isLight() ? 'light' : 'dark' } = {}) {
|
|
129
131
|
const W = narrow ? 40 : cols;
|
|
132
|
+
// the dragon (qdragonScenes.js): eyes as quadrant blocks with a background colour, the lab's light palette on light terminals
|
|
133
|
+
const fit = (str) => (str.length > W ? str.slice(0, W - 1) + '…' : str);
|
|
134
|
+
const dragon = (name, t, o) => [...art(clipCells(name, t, { cols: W, theme, mono: o.mono })), '', paint(fit(clipCaption(name, t)), MUTED, o.mono)];
|
|
135
|
+
const MOOD_SPAN = MOOD_CLIPS.map((m) => CLIPS[m].dur + 400);
|
|
130
136
|
const size = narrow ? 'small' : labSize(W, rows);
|
|
131
137
|
const big = size === 'small' ? 'small' : size;
|
|
132
138
|
const art = (cells) => cellsToAnsi(cells);
|
|
@@ -138,8 +144,6 @@ export function galleryScenes({ cols = 100, rows = 30, narrow = false } = {}) {
|
|
|
138
144
|
frame: (t, o) => art(portalCellsAt(big, t, { color: !o.mono })) },
|
|
139
145
|
{ id: 'particle', title: 'Particle collapse', caption: 'density rises on a circle, then collapses into the compact Q', ms: 1100 + 500,
|
|
140
146
|
frame: (t, o) => art(particleCellsAt('small', t, { color: !o.mono })) },
|
|
141
|
-
{ id: 'eye', title: "The Dragon's Eye", caption: 'a slit-pupil eye opens in the bowl, blinks once, and dissolves back into the Q (/eye; rarely on launch)', ms: EYE_MS + 500,
|
|
142
|
-
frame: (t, o) => art(eyeCellsAt(big, t, { color: !o.mono })) },
|
|
143
147
|
{ id: 'hello', title: 'Hello', caption: 'the Q locks, then says hello: the line decodes in from katakana with a short afterglow', ms: INTRO_MS + helloMs(hello) + 600,
|
|
144
148
|
frame: (t, o) => {
|
|
145
149
|
const q = art(t < INTRO_MS ? orbitCellsAt(big, t, { color: !o.mono }) : qCells(big, { color: !o.mono }));
|
|
@@ -162,9 +166,19 @@ export function galleryScenes({ cols = 100, rows = 30, narrow = false } = {}) {
|
|
|
162
166
|
frame: (t, o) => ringLines(Math.min(W - 2, 72), ringState(t), o.mono) },
|
|
163
167
|
{ id: 'fx', title: 'Reveal effects', caption: 'tool row, answer drop-in, approval arming, receipt seal, ProofFlow, closing line', ms: FX_MS,
|
|
164
168
|
frame: (t, o) => fxLines(t, Math.min(W - 2, 78), o.mono) },
|
|
169
|
+
{ id: 'dragon', title: CLIPS.pick.title, caption: CLIPS.pick.caption, ms: CLIPS.pick.dur, frame: (t, o) => dragon('pick', t, o) },
|
|
170
|
+
{ id: 'dragon-fire', title: CLIPS.fire.title, caption: CLIPS.fire.caption, ms: CLIPS.fire.dur, frame: (t, o) => dragon('fire', t, o) },
|
|
171
|
+
{ id: 'dragon-moods', title: 'Dragon moods', caption: 'at the cinematic motion level the dragon reacts: wide eyes and an amber glint while it waits for you, a wink when done, a puff of smoke on failure, and a greeting on launch',
|
|
172
|
+
ms: MOOD_SPAN.reduce((a, b) => a + b, 0),
|
|
173
|
+
frame: (t, o) => {
|
|
174
|
+
let i = 0, s0 = 0;
|
|
175
|
+
while (i < MOOD_CLIPS.length - 1 && t >= s0 + MOOD_SPAN[i]) s0 += MOOD_SPAN[i++];
|
|
176
|
+
const m = MOOD_CLIPS[i];
|
|
177
|
+
return [...art(clipCells(m, t - s0, { cols: Math.min(W, 64), theme, mono: o.mono })), '', paint(fit(`${m}: ${CLIPS[m].caption.split(':')[1].replace(/\s*\(.*\)$/, '').trim()}`), MUTED, o.mono)];
|
|
178
|
+
} },
|
|
165
179
|
];
|
|
166
180
|
}
|
|
167
|
-
export const SCENE_IDS = ['orbit', 'portal', 'particle', '
|
|
181
|
+
export const SCENE_IDS = ['orbit', 'portal', 'particle', 'hello', 'perpetual', 'ring', 'fx', 'dragon', 'dragon-fire', 'dragon-moods'];
|
|
168
182
|
|
|
169
183
|
// ---- runner (the only impure part) ---------------------------------------------------------------------------
|
|
170
184
|
const visible = (s) => s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ''); // eslint-disable-line no-control-regex
|