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