quoxcli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -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/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -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 +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -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 +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -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 +120 -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 +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -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/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -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 +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- 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 +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- 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 +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- 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/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -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 +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- 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/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -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 +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -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/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -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 +124 -0
- package/lib/quoxon/commands.js +429 -39
- 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 +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -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 +266 -26
- package/lib/quoxon/extensions.js +276 -0
- 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/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -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 +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -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 +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- 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/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -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 +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -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/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -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 +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -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/evidenceCmd.js +34 -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 +22 -8
- 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/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- 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/todoCmd.js +122 -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 +90 -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/toolCards.js +30 -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 +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -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 +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/demo.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
1
2
|
import { DEMO_STEPS } from './demo.fixtures.js';
|
|
2
3
|
|
|
3
4
|
const sleep = (ms, signal) => new Promise((res) => {
|
|
@@ -7,7 +8,34 @@ const sleep = (ms, signal) => new Promise((res) => {
|
|
|
7
8
|
});
|
|
8
9
|
|
|
9
10
|
/** A fake api with the same surface as createApi(), driven by DEMO_STEPS. */
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* WALK-FIXES pty seam (demo only, never a real server): decisionsFile is a JSON state file
|
|
13
|
+
* { items: [decision inbox items], log: [], answerDelayMs?, approvalsDelayMs? }. approvals() lists the open ones,
|
|
14
|
+
* answerDecision() marks one answered and appends { id, key, note, at } to log, so a test in another process reads
|
|
15
|
+
* exactly what was submitted, and when.
|
|
16
|
+
*/
|
|
17
|
+
function decisionStore(file) {
|
|
18
|
+
const load = () => JSON.parse(readFileSync(file, 'utf8'));
|
|
19
|
+
const wait = (ms) => new Promise(r => setTimeout(r, ms || 0));
|
|
20
|
+
return {
|
|
21
|
+
// staleMs: an answered item is still listed for this long (a server that lags behind its own answers)
|
|
22
|
+
// failAfterAnswers: the server is down once anything was answered (the walkthrough's closing re-check)
|
|
23
|
+
async open() { const d = load(); await wait(d.approvalsDelayMs); if (d.failAfterAnswers && d.log.length) { const e = new Error('connect ECONNREFUSED'); e.status = 503; throw e; } return d.items.filter(x => x.status === 'open' || (d.staleMs && x.answeredAt && Date.now() - x.answeredAt < d.staleMs)); },
|
|
24
|
+
async answer(id, key, note) {
|
|
25
|
+
const d0 = load(); await wait(d0.answerDelayMs);
|
|
26
|
+
const d = load(); const it = d.items.find(x => x.id === id);
|
|
27
|
+
if (!it || it.status !== 'open') { const e = new Error('already handled'); e.status = 404; throw e; }
|
|
28
|
+
it.status = 'answered'; it.answeredAt = Date.now(); it.answer = { key, note: note || null };
|
|
29
|
+
d.log.push({ id, key, note: note || null, at: Date.now() });
|
|
30
|
+
if (d.arriveOnAnswer && !d.arrived) { d.items.push(...d.arriveOnAnswer); d.arrived = true; } // arriveOnAnswer: new items land while the owner is answering
|
|
31
|
+
writeFileSync(file, JSON.stringify(d));
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createDemoApi({ speed = 1, objects = false, decisionsFile = null } = {}) {
|
|
38
|
+
const decs = decisionsFile ? decisionStore(decisionsFile) : null;
|
|
11
39
|
let gate = null;
|
|
12
40
|
let decision = 'approve';
|
|
13
41
|
return {
|
|
@@ -26,10 +54,25 @@ export function createDemoApi({ speed = 1 } = {}) {
|
|
|
26
54
|
async steer() { return { ok: false, reason: 'unsupported' }; },
|
|
27
55
|
async interrupt() { return { ok: false }; },
|
|
28
56
|
async health() { return { ok: true, url: 'demo' }; },
|
|
29
|
-
|
|
57
|
+
// the open approval is in the inbox while the demo holds at the gate, so a poll never prunes its card
|
|
58
|
+
async approvals() { return [...(gate ? [{ id: 'demo-item', title: 'host_restart on web on edge-02', approval_id: 'demo-appr', created_at: new Date().toISOString() }] : []), ...(decs ? await decs.open() : [])]; },
|
|
59
|
+
...(decs ? { async answerDecision(id, key, { note } = {}) { return decs.answer(id, key, note); }, async approvalStats() { return { awaiting: (await decs.open()).length + (gate ? 1 : 0) }; } } : {}),
|
|
30
60
|
async skills() { return [{ id: 'sk1', name: 'daily-fleet-check', description: 'Check fleet health and summarise.', edition: 'stable' }]; },
|
|
31
61
|
async skill(id) { return { id, name: 'daily-fleet-check', description: 'Check fleet health and summarise.', edition: 'stable' }; },
|
|
32
62
|
async conversationExists() { return { exists: true }; },
|
|
33
|
-
|
|
63
|
+
// /proof works in the demo journey (main.js asks for it); the other object views stay unavailable offline
|
|
64
|
+
...(objects ? { objects: {
|
|
65
|
+
async proof(tool) {
|
|
66
|
+
const env = { id: tool.envelopeId, corr: 'demo-corr', payload: { success: true, tool_name: tool.tool } };
|
|
67
|
+
return { data: { tool, envelope: { ok: true, env },
|
|
68
|
+
aocl: { ok: true, list: [{ intent: 'aocl.identity.verified' }, { intent: 'aocl.policy.evaluated', payload: { allPassed: true } }] },
|
|
69
|
+
receipt: { ok: true, result: { status: 'WITNESSED', entry: { seq: 4069513 } } } } };
|
|
70
|
+
},
|
|
71
|
+
...Object.fromEntries(['skill', 'agent', 'workflowRun', 'crystal', 'memory', 'rules', 'reconcile'].map((k) => [k, async () => ({ error: 'Object views are not available in the demo.' })]))
|
|
72
|
+
} } : {}),
|
|
73
|
+
async respond(_id, action) {
|
|
74
|
+
if (decs && _id !== 'demo-item') return decs.answer(_id, action === 'reject' ? 'n' : 'y', null); // a yes/no decision answered as an approval (the bulk card)
|
|
75
|
+
decision = action === 'reject' ? 'reject' : 'approve'; const g = gate; gate = null; g?.(); return { ok: true };
|
|
76
|
+
}
|
|
34
77
|
};
|
|
35
78
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a dev observer wired to this install? /observe and /note exist only for the developer whose
|
|
3
|
+
* window a QUOXON observer session watches; a customer has no observer, so for them these commands
|
|
4
|
+
* are hidden (help, palette, autocomplete) and refuse with a plain line.
|
|
5
|
+
*
|
|
6
|
+
* The switch is config driven, in the same style as the dev-transcripts flag: QUOX_DEV_OBSERVER=1
|
|
7
|
+
* turns it on for one shell, QUOX_DEV_OBSERVER=0 forces it off, otherwise the flag file
|
|
8
|
+
* ~/.config/quox/dev-observer decides (touch it on a dev box; it survives new shells).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const DEV_OBSERVER_REFUSAL = 'No dev observer is set up on this install, so there is nothing to send this to.';
|
|
15
|
+
|
|
16
|
+
export function devObserverOn(env = process.env, { home = homedir(), exists = existsSync } = {}) {
|
|
17
|
+
if (env.QUOX_DEV_OBSERVER === '0') return false;
|
|
18
|
+
if (env.QUOX_DEV_OBSERVER === '1') return true;
|
|
19
|
+
try { return exists(join(home, '.config', 'quox', 'dev-observer')); } catch { return false; }
|
|
20
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-DIVIDER: the composer's bottom rule (`──────── anthropic claude-sonnet-5-5 ─`) as a live AOCL strip.
|
|
3
|
+
*
|
|
4
|
+
* Owner design (2026-10-17 22:09, after the first cut): a PIPELINE, like the dashboard command screen's
|
|
5
|
+
* LayerStackMini. One compact chain of 11 dots, one per AOCL layer, each in its own layer colour, sits just left of
|
|
6
|
+
* the model label: `────────•─•─●─●─◆─•─•─•─•─•─• anthropic claude-sonnet-5-5 ─`. On Enter the dots light left to
|
|
7
|
+
* right as the real layer events arrive; when they reach the model, only the label animates (katakana shimmer).
|
|
8
|
+
* Motion lives in one place at a time (the newest dot, or the label), never across the row. The finished state is
|
|
9
|
+
* the label green (red on an error or a block, with the blocking dot marked) for ~1.6 s, then the normal rule.
|
|
10
|
+
*
|
|
11
|
+
* HONESTY: only REAL state moves this. A pip lights only when that layer's `aocl.layer` event arrives (or a
|
|
12
|
+
* pre-stream refusal's `aocl_layers` record), in the colour of its real result. A bypassed layer (not evaluated
|
|
13
|
+
* on this path) stays unlit, and so does every layer of a server that sends no `aocl.layer`. The "model is
|
|
14
|
+
* working" phase starts on a real signal: the last pre-model layer (L4) reported, or the first model output.
|
|
15
|
+
* Dots light one step apart for readability (STEP_MS), but never before their own event.
|
|
16
|
+
*
|
|
17
|
+
* Pure: `dividerStep` folds actions into state, `dividerFrame` draws one frame (or null = draw the normal rule).
|
|
18
|
+
* Nothing here runs a timer; the app asks the clock for frames only while `dividerLive` says so.
|
|
19
|
+
*/
|
|
20
|
+
import stringWidth from 'string-width';
|
|
21
|
+
import { wordFrame, wordColor } from './indicator.js';
|
|
22
|
+
import { LAYER_NAMES } from './aocl.js';
|
|
23
|
+
|
|
24
|
+
export const ORDER = ['L0', 'L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10'];
|
|
25
|
+
const PRE_LAST = 'L4';
|
|
26
|
+
export const GREEN_MS = 1600; // the label holds green this long after a completed reply (under the app's 2 s idle guarantee, motion-app.test)
|
|
27
|
+
export const RED_MS = 1600; // and red this long after an error or a block (the error itself stays in the transcript)
|
|
28
|
+
export const MIN_COLS = 70; // narrower than this: the plain rule, no animation
|
|
29
|
+
export const STEP_MS = 120; // dots light at most one per step, left to right (never before their event)
|
|
30
|
+
export const CURRENT_MS = 700; // the newest dot blinks this long after it lights; then it is just lit
|
|
31
|
+
export const TAG_MS = 1200; // the newest layer's name shows beside the chain this long
|
|
32
|
+
export const LIT_RESULTS = new Set(['pass', 'gated', 'blocked']);
|
|
33
|
+
|
|
34
|
+
// Real model work only. `thinking` frames are mostly server staging (routing, processing, provider notes) sent
|
|
35
|
+
// before turn.started; only the model's own reasoning counts.
|
|
36
|
+
const MODEL_SIGNALS = new Set(['chunk', 'text.delta', 'tool.started']);
|
|
37
|
+
const isModelSignal = (ev) => MODEL_SIGNALS.has(ev.type) || (ev.type === 'thinking' && ev.stage === 'model_reasoning');
|
|
38
|
+
|
|
39
|
+
/** A fresh strip for a turn the user just sent. */
|
|
40
|
+
export const dividerStart = (turnId, now) => ({ turnId, startedAt: now, pips: {}, seen: {}, order: [], modelAt: null, phase: 'gates', endedAt: null, blockedAt: null });
|
|
41
|
+
|
|
42
|
+
function light(d, l, now) {
|
|
43
|
+
const id = String(l?.id || '');
|
|
44
|
+
if (!ORDER.includes(id) || d.pips[id]) return d; // unknown layer or already lit: nothing changes
|
|
45
|
+
const result = String(l.result || '');
|
|
46
|
+
if (!LIT_RESULTS.has(result)) { // bypass: a real event, but not evaluated: the pipeline steps over it, unlit
|
|
47
|
+
return result === 'bypass' && !(d.seen || {})[id] ? { ...d, seen: { ...(d.seen || {}), [id]: { result, at: now } } } : d;
|
|
48
|
+
}
|
|
49
|
+
let n = { ...d, pips: { ...d.pips, [id]: { result, at: now } }, seen: { ...(d.seen || {}), [id]: { result, at: now } }, order: [...d.order, id] };
|
|
50
|
+
if (result === 'blocked') n = { ...n, phase: 'error', blockedAt: id, endedAt: n.endedAt ?? now };
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fold one reducer action into the strip. `a` is the quoxon reducer action ({type:'turn.begin'} or
|
|
56
|
+
* {type:'event', ev}); unknown actions return `d` unchanged (same object).
|
|
57
|
+
*/
|
|
58
|
+
export function dividerStep(d, a, now) {
|
|
59
|
+
if (!a) return d;
|
|
60
|
+
if (a.type === 'turn.begin') return dividerStart(a.turnId, now);
|
|
61
|
+
if (a.type !== 'event' || !d || d.endedAt != null && a.ev?.type !== 'aocl.layer') return d;
|
|
62
|
+
const ev = a.ev || {};
|
|
63
|
+
if (ev.type === 'aocl.layer') {
|
|
64
|
+
if (d.endedAt != null && d.phase !== 'error') return d; // a finished green strip never changes
|
|
65
|
+
let n = light(d, ev, now);
|
|
66
|
+
const pre = ev.phase === 'pre' || ORDER.indexOf(String(ev.id)) <= ORDER.indexOf(PRE_LAST);
|
|
67
|
+
if (n.phase === 'gates' && (String(ev.id) === PRE_LAST || !pre)) n = { ...n, phase: 'model', modelAt: n.modelAt ?? now };
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
if (isModelSignal(ev)) return d.phase === 'gates' ? { ...d, phase: 'model', modelAt: now } : d;
|
|
71
|
+
if (ev.type === 'aocl.verdict' && ev.verdict === 'blocked') {
|
|
72
|
+
const id = ORDER.includes(ev.layer) ? ev.layer : 'L8';
|
|
73
|
+
return { ...d, phase: 'error', blockedAt: d.blockedAt || id, endedAt: d.endedAt ?? now };
|
|
74
|
+
}
|
|
75
|
+
if (ev.type === 'error') return { ...d, phase: 'error', endedAt: now };
|
|
76
|
+
if (ev.type === 'turn.finished') {
|
|
77
|
+
const ok = (ev.status || 'completed') === 'completed';
|
|
78
|
+
return ok && d.phase !== 'error' ? { ...d, phase: 'done', endedAt: now } : { ...d, phase: 'error', endedAt: d.endedAt ?? now };
|
|
79
|
+
}
|
|
80
|
+
return d;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True while the strip needs frames: a turn in flight, or the green/red hold after it. */
|
|
84
|
+
export function dividerLive(d, now) {
|
|
85
|
+
if (!d) return false;
|
|
86
|
+
if (d.endedAt == null) return true;
|
|
87
|
+
return now - d.endedAt < (d.phase === 'done' ? GREEN_MS : RED_MS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Each layer's own colour, from the dashboard (src/lib/aee/envelope.js AOCL_LAYERS, which LayerStackMini uses).
|
|
91
|
+
// L0 and L10 are grey there (#6b7280); the TUI has no grey text, so they take sky and violet.
|
|
92
|
+
export const LAYER_HEX = {
|
|
93
|
+
L0: '#38bdf8', L1: '#3b82f6', L2: '#3b82f6', L3: '#f59e0b', L4: '#8b5cf6', L5: '#8b5cf6',
|
|
94
|
+
L6: '#6366f1', L7: '#f97316', L8: '#14b8a6', L9: '#22c55e', L10: '#a855f7',
|
|
95
|
+
};
|
|
96
|
+
const hexRgb = (h) => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
|
|
97
|
+
const mixRgb = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
|
|
98
|
+
const DARK = [30, 36, 48];
|
|
99
|
+
export const layerRgb = (id) => hexRgb(LAYER_HEX[id]);
|
|
100
|
+
export const dimRgb = (id) => mixRgb(layerRgb(id), DARK, 0.6); // pending / bypassed: the layer's own hue, dimmed (not grey)
|
|
101
|
+
|
|
102
|
+
export const RGB = { done: [34, 197, 94], blocked: [239, 68, 68], model: [56, 189, 248] };
|
|
103
|
+
const LIT = { pass: '●', gated: '◆', blocked: '✖' };
|
|
104
|
+
const PENDING = '•';
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* When each layer the server reported was reached in the pipeline: in layer order, one STEP_MS apart, never
|
|
108
|
+
* before its own event. Bypassed layers are reached (the chain passes them) but stay unlit.
|
|
109
|
+
*/
|
|
110
|
+
export function layerReachTimes(d) {
|
|
111
|
+
const out = {};
|
|
112
|
+
if (!d) return out;
|
|
113
|
+
let t = d.startedAt - STEP_MS;
|
|
114
|
+
for (const id of ORDER) {
|
|
115
|
+
const s = (d.seen || {})[id] || d.pips[id];
|
|
116
|
+
if (!s) continue;
|
|
117
|
+
t = Math.max(t + STEP_MS, s.at);
|
|
118
|
+
out[id] = t;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** When the pipeline hands over to the model (the label starts to shimmer): every pre-model layer seen was reached. */
|
|
124
|
+
export function modelReachedAt(d) {
|
|
125
|
+
if (!d || d.modelAt == null) return null;
|
|
126
|
+
const r = layerReachTimes(d);
|
|
127
|
+
const pre = ORDER.slice(0, ORDER.indexOf(PRE_LAST) + 1).map(id => r[id]).filter(x => x != null);
|
|
128
|
+
return Math.max(d.modelAt, ...pre, d.startedAt) + (pre.length ? STEP_MS : 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The chain of 11 dots joined by rule: cells [{ t, rgb, bold }]. Motion only on the newest dot. */
|
|
132
|
+
export function chainCells(d, now, { still = false } = {}) {
|
|
133
|
+
const reach = layerReachTimes(d);
|
|
134
|
+
const reached = ORDER.filter(id => reach[id] != null && reach[id] <= now);
|
|
135
|
+
const newest = reached.filter(id => d.pips[id]).sort((a, b) => reach[b] - reach[a])[0] || null;
|
|
136
|
+
const cells = [];
|
|
137
|
+
ORDER.forEach((id, i) => {
|
|
138
|
+
if (i) cells.push({ t: '─' });
|
|
139
|
+
const p = d.pips[id];
|
|
140
|
+
if (p && reach[id] <= now) {
|
|
141
|
+
// only the newest dot blinks, only while the turn runs, and a blocked dot never dims (it stays red)
|
|
142
|
+
const blinkOff = !still && d.endedAt == null && p.result !== 'blocked' && id === newest && now - reach[id] < CURRENT_MS && Math.floor((now - reach[id]) / 175) % 2 === 1;
|
|
143
|
+
const rgb = p.result === 'blocked' ? RGB.blocked : layerRgb(id);
|
|
144
|
+
cells.push({ t: LIT[p.result], rgb: blinkOff ? dimRgb(id) : rgb, bold: !blinkOff });
|
|
145
|
+
} else cells.push({ t: PENDING, rgb: dimRgb(id) });
|
|
146
|
+
});
|
|
147
|
+
return { cells, newest, newestAt: newest ? reach[newest] : null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One frame of the rule: `{ cells: [{ t, rgb?, bold?, label? }] }` whose widths sum to exactly `cols`, or null
|
|
152
|
+
* when the normal rule should draw (no strip, the hold is over, NO_COLOR, reduced motion, too narrow, no label).
|
|
153
|
+
* Cells without `rgb` take the rule's own colour; `label: true` takes the label's own colour.
|
|
154
|
+
* `still` (a slow redraw): no blink and no katakana, but every lit dot and the end colours stay.
|
|
155
|
+
*/
|
|
156
|
+
export function dividerFrame({ cols, label, extra = null, d, now, mono = false, reduced = false, still = false, seed = 0 }) {
|
|
157
|
+
if (mono || reduced || !d || cols < MIN_COLS || !label || !dividerLive(d, now)) return null;
|
|
158
|
+
const lab = ` ${label} `;
|
|
159
|
+
const lw = stringWidth(lab);
|
|
160
|
+
const chain = chainCells(d, now, { still });
|
|
161
|
+
const cw = chain.cells.length;
|
|
162
|
+
const ew = extra ? stringWidth(` ${extra.text} `) : 0;
|
|
163
|
+
const left = cols - lw - 1 - cw - ew;
|
|
164
|
+
if (left < 4) return null;
|
|
165
|
+
// the newest lit layer names itself to the left of the chain, in its colour, while it is new
|
|
166
|
+
const tag = chain.newest && now - chain.newestAt < TAG_MS ? ` ${chain.newest} ${LAYER_NAMES[chain.newest] || ''} ` : '';
|
|
167
|
+
const fill = Math.max(1, left - (tag && tag.length + 1 <= left - 1 ? tag.length : 0));
|
|
168
|
+
const leftCells = Array.from({ length: fill }, () => ({ t: '─' }));
|
|
169
|
+
if (fill < left) {
|
|
170
|
+
const rgb = d.pips[chain.newest].result === 'blocked' ? RGB.blocked : layerRgb(chain.newest);
|
|
171
|
+
leftCells.splice(fill - 1, 0, ...[...tag].map(ch => ({ t: ch, rgb, bold: true })));
|
|
172
|
+
}
|
|
173
|
+
while (leftCells.length < left) leftCells.push({ t: '─' });
|
|
174
|
+
let labelCells;
|
|
175
|
+
if (d.phase === 'done') labelCells = [{ t: lab, rgb: RGB.done, bold: true }];
|
|
176
|
+
else if (d.phase === 'error') labelCells = [{ t: lab, rgb: RGB.blocked, bold: true }];
|
|
177
|
+
else {
|
|
178
|
+
const at = modelReachedAt(d);
|
|
179
|
+
if (at != null && now >= at) {
|
|
180
|
+
if (still) labelCells = [{ t: lab, rgb: RGB.model, bold: true }];
|
|
181
|
+
else {
|
|
182
|
+
const fr = wordFrame(label, now, { since: at, seed });
|
|
183
|
+
const n = fr.length;
|
|
184
|
+
labelCells = [{ t: ' ' }, ...fr.map((c, i) => ({ t: c.ch, rgb: wordColor('working', i, n, now, c.flash ? 'flash' : c.glyph ? 'glyph' : 'letter') })), { t: ' ' }];
|
|
185
|
+
}
|
|
186
|
+
} else labelCells = [{ t: lab, label: true }];
|
|
187
|
+
}
|
|
188
|
+
return { cells: [...leftCells, ...chain.cells, ...labelCells, ...(extra ? [{ t: ` ${extra.text} `, hex: extra.hex, bold: true }] : []), { t: '─' }] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Plain text of a frame (tests and pty proofs). */
|
|
192
|
+
export const frameText = (f) => (f ? f.cells.map(c => c.t).join('') : null);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-DIVIDER: the composer's bottom rule drawn from a dividerAocl frame (pips, bullet, katakana label, green/red).
|
|
3
|
+
* Draws ONE pre-coloured string per colour run (as the ring does), so a frame is a single row of exactly `cols`
|
|
4
|
+
* cells and Ink tokenises a handful of escapes, not one per cell.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { Text } from 'ink';
|
|
9
|
+
import { dividerFrame } from './dividerAocl.js';
|
|
10
|
+
|
|
11
|
+
const h = React.createElement;
|
|
12
|
+
const RUN_Q = 8; // quantise shimmer colours so neighbouring cells share a run
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per turn, a frame gap this long while the strip is live marks the redraw as slow: the strip then drops its motion
|
|
16
|
+
* (no bullet, no katakana) for the rest of that turn but keeps every lit pip. `slow` is a ref object owned by the caller.
|
|
17
|
+
*/
|
|
18
|
+
export const SLOW_GAP_MS = 400;
|
|
19
|
+
export function noteFrameGap(slow, d, now) {
|
|
20
|
+
if (!slow || !d) return false;
|
|
21
|
+
if (slow.turnId !== d.turnId) { slow.turnId = d.turnId; slow.last = now; slow.slow = false; return false; }
|
|
22
|
+
if (slow.last != null && now - slow.last > SLOW_GAP_MS && d.endedAt == null) slow.slow = true;
|
|
23
|
+
slow.last = now;
|
|
24
|
+
return slow.slow;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The frame as one string of chalk runs; `ruleColor`/`labelColor` are hex or undefined (theme roles resolved). */
|
|
28
|
+
export function paintFrame(f, { ruleColor, labelColor }) {
|
|
29
|
+
let out = '', run = '', key = null, style = null;
|
|
30
|
+
const paint = (s) => {
|
|
31
|
+
if (!s) return '';
|
|
32
|
+
let p = chalk;
|
|
33
|
+
if (style.rgb) p = p.rgb(...style.rgb);
|
|
34
|
+
else if (style.hex) p = p.hex(style.hex);
|
|
35
|
+
if (style.bold) p = p.bold;
|
|
36
|
+
return p(s);
|
|
37
|
+
};
|
|
38
|
+
const flush = () => { if (run) out += paint(run); run = ''; };
|
|
39
|
+
for (const c of f.cells) {
|
|
40
|
+
const rgb = c.rgb ? c.rgb.map(v => Math.max(0, Math.min(255, Math.round(v / RUN_Q) * RUN_Q))) : null;
|
|
41
|
+
const st = { rgb, hex: rgb ? null : (c.hex || (c.label ? labelColor : ruleColor)) || null, bold: Boolean(c.bold) };
|
|
42
|
+
const k = `${rgb ? rgb.join(',') : st.hex}|${st.bold}`;
|
|
43
|
+
if (k !== key) { flush(); key = k; style = st; }
|
|
44
|
+
run += c.t;
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The bottom rule as a strip element, or null when the caller should draw its normal Rule (a plain call, not a component). */
|
|
51
|
+
export function dividerRuleEl({ cols, label, extra = null, divider, now, ruleColor, labelColor, mono = false, reduced = false, slow = null }) {
|
|
52
|
+
if (!divider) return null;
|
|
53
|
+
const isSlow = noteFrameGap(slow, divider, now);
|
|
54
|
+
const f = dividerFrame({ cols, label, extra, d: divider, now, mono, reduced, still: isSlow }); // slow: keep the lit pips, drop the motion
|
|
55
|
+
if (!f) return null;
|
|
56
|
+
return h(Text, { wrap: 'truncate-end' }, paintFrame(f, { ruleColor, labelColor }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dragon companion for Quoxon (Lucid, Hyperion, Seer, Sentinel, Drago, Cipher). The controller owns attach state and feeds REAL stream
|
|
3
|
+
* events to dragonlab's pure view (integration/companions-v1/view.mjs: the same reducer and renderer the CLI
|
|
4
|
+
* uses). No art, state logic or timers are duplicated here; this file only decides, translates and lays out.
|
|
5
|
+
* Decisions: docs/dragons/COMPANION-UX.md. The reducer never overwrites pending approvals or failures, but the host only SHOWS the dragon during a dwell (attach, key moment, invoke).
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { Box, Text } from 'ink';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import { cellNodes, dragonActive, clipRoom } from './qmotion.js';
|
|
12
|
+
import { useFrame } from './motion/frame.js';
|
|
13
|
+
import { loadCompanionView } from '../dragon/companionView.js';
|
|
14
|
+
import { ART_MIN_COLS as SLOT_MIN_COLS, ART_MIN_ROWS as SLOT_MIN_ROWS } from '../dragon/frames.js';
|
|
15
|
+
import { companionPlan, motionPolicy, normaliseMode, COMPANION_MODES, VENDORED_DRAGONLAB, dragonForAgent, companionMode } from '../dragon/select.js';
|
|
16
|
+
import { createMomentEngine, createRosterWatch, LONG_TASK_MS, SUMMON_COOLDOWN_MS } from './dragonMoments.js';
|
|
17
|
+
import { moodForEvent } from './motion/level.js';
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
const AGES_OUT = 10 * 60 * 1000; // an attach that is not a greeting starts with its entrance already over
|
|
21
|
+
/** How long the dragon stays after any appearance (attach, key moment, invoke). The one dwell constant. */
|
|
22
|
+
export const DRAGON_DWELL_MS = 10000;
|
|
23
|
+
/** A keypress inside the first part of a dwell does not send it away: invoking with Enter and then typing must not kill it. */
|
|
24
|
+
export const KEY_GRACE_MS = 2000;
|
|
25
|
+
/** A turn at least this long counts as a "long task" when it finishes (defined with the moment engine). */
|
|
26
|
+
export { LONG_TASK_MS };
|
|
27
|
+
/** Art that is still being drawn when its dwell would end is waited for this long (from the appearance) before giving up. */
|
|
28
|
+
export const FIRST_FRAME_GRACE_MS = 20000;
|
|
29
|
+
|
|
30
|
+
/** Art needs room; below this the companion is one plain text line. */
|
|
31
|
+
export const ART_MIN_COLS = SLOT_MIN_COLS, ART_MIN_ROWS = SLOT_MIN_ROWS;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether this /attach greets. Every explicit, typed /attach does (even a repeat of the same agent in one session); only an
|
|
35
|
+
* automatic re-attach (after a restart, or resuming a conversation) is quiet. The quiet flag is read AND cleared in one step
|
|
36
|
+
* at the start of the command, so an attach that bails early (a slot, a busy turn) can never leave it armed for the next typed one.
|
|
37
|
+
*/
|
|
38
|
+
export function takeGreeting(flags) { const quiet = Boolean(flags.noGreeting); flags.noGreeting = false; return !quiet; }
|
|
39
|
+
|
|
40
|
+
/** Quoxon stream/poll facts -> the reducer's event vocabulary. Unknown shapes pass through (the reducer ignores them). */
|
|
41
|
+
export function toCompanionEvent(action) {
|
|
42
|
+
if (!action) return null;
|
|
43
|
+
if (action.type === 'turn.begin') return { type: 'turn.started', turn_id: action.turnId };
|
|
44
|
+
if (action.type === 'approval.found') {
|
|
45
|
+
const key = action.item?.id || action.item?.approval_id || action.item?.approvalId;
|
|
46
|
+
return key ? { type: 'approval.requested', call_id: String(key) } : null;
|
|
47
|
+
}
|
|
48
|
+
if (action.type === 'event') return action.ev || null;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createCompanionController({ settings = () => ({}), env = process.env, isTTY = true, exists = existsSync, now = Date.now, importer = null, makeView = loadCompanionView, jobs = null, typing = () => false } = {}) {
|
|
53
|
+
let noticed = false, showUntil = 0, shownAt = 0, unpainted = false, waitingFrame = false, trigger = 'attach', forced = false, timer = null, job = null;
|
|
54
|
+
let summoned = null, summonCaption = null; const lastSummon = new Map(); const rosterWatch = createRosterWatch(); // a domain moment's dragon (not the attached one) and its truthful caption
|
|
55
|
+
const moments = createMomentEngine({ now });
|
|
56
|
+
let agent = null, plan = { enabled: false, why: 'no agent attached' }, view = null, state = null, token = 0, greet = true;
|
|
57
|
+
const listeners = new Set();
|
|
58
|
+
const notify = () => listeners.forEach(f => f());
|
|
59
|
+
const replan = (as = null) => companionPlan({ agent: as ?? agent, settings: settings(), env: forced ? { ...env, QUOX_DRAGON_COMPANION: 'greeting' } : env, isTTY, exists, bundled: VENDORED_DRAGONLAB });
|
|
60
|
+
|
|
61
|
+
/** (Re)start the dwell from shownAt; a timer wakes the renderer at expiry. */
|
|
62
|
+
function arm() {
|
|
63
|
+
showUntil = shownAt + DRAGON_DWELL_MS;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
timer = setTimeout(() => { if (summoned && now() >= showUntil) restore(); else notify(); }, Math.max(0, showUntil - now()) + 50); timer.unref?.();
|
|
66
|
+
}
|
|
67
|
+
/** Show for one dwell, then leave on its own. The dwell and the keypress grace start when the first frame is actually drawn (see snapshot). */
|
|
68
|
+
function appear(why = 'attach') {
|
|
69
|
+
if (!view) return;
|
|
70
|
+
trigger = why; shownAt = now(); unpainted = true; waitingFrame = false;
|
|
71
|
+
arm();
|
|
72
|
+
readJob();
|
|
73
|
+
notify();
|
|
74
|
+
}
|
|
75
|
+
/** The dragon's one job: a real read (cached 30 s inside `jobs`), asked only when the dragon is about to be shown. Its caption replaces the generic one when it lands. */
|
|
76
|
+
function readJob() {
|
|
77
|
+
const mine = token, id = plan.id;
|
|
78
|
+
if (!jobs || !id) return;
|
|
79
|
+
const first = jobs.peek?.(id);
|
|
80
|
+
if (first) job = first;
|
|
81
|
+
Promise.resolve().then(() => jobs.run(id)).then((r) => { if (mine === token && r) { job = r; notify(); } }, () => { /* a failed job read leaves the generic caption: never a made-up one */ });
|
|
82
|
+
}
|
|
83
|
+
/** Still on screen: inside the dwell, or waiting for art that has not been drawn yet (the dwell then counts from the first picture). */
|
|
84
|
+
const live = () => now() < showUntil || (waitingFrame && unpainted && now() < shownAt + FIRST_FRAME_GRACE_MS);
|
|
85
|
+
/** Key moments only in mode `on`; `greeting` and `off` never show for approvals or failures. */
|
|
86
|
+
const keyMoment = () => { if (view && companionMode(settings(), env) === 'on') appear('moment'); };
|
|
87
|
+
|
|
88
|
+
/** A summoned dragon's dwell is over (or it was dismissed): drop it and put the attached agent's dragon back, quietly. */
|
|
89
|
+
function restore() {
|
|
90
|
+
summoned = null; summonCaption = null;
|
|
91
|
+
if (agent) load({ quiet: true }); else { token++; view?.dispose?.(); view = null; state = null; showUntil = 0; plan = replan(); notify(); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function load({ show = false, persist = null, why = 'attach', as = null, caption = null, quiet = false } = {}) {
|
|
95
|
+
const mine = ++token;
|
|
96
|
+
view?.dispose?.(); view = null; state = null; showUntil = 0; job = null;
|
|
97
|
+
summoned = as; summonCaption = caption;
|
|
98
|
+
plan = replan(as);
|
|
99
|
+
if (!plan.enabled) return notify();
|
|
100
|
+
try {
|
|
101
|
+
const opts = { still: plan.still, mono: plan.mono, persist: persist ?? plan.persist };
|
|
102
|
+
let v;
|
|
103
|
+
if (importer) v = await (await importer(pathToFileURL(plan.viewFile).href)).loadDragonView(plan.id, opts);
|
|
104
|
+
else {
|
|
105
|
+
// The playback path: frames come from a worker through a cache (dragon/frames.js). If the worker cannot start, the
|
|
106
|
+
// art still shows through dragonlab's own view (slower, inline), as before.
|
|
107
|
+
try { v = await makeView(plan.root, plan.id, { ...opts, persist: true, onFrame: notify }); } // persist: this host's dwell (showUntil) decides how long it stays, not the entrance length (Lucid's is 3 s)
|
|
108
|
+
catch { v = await (await import(pathToFileURL(plan.viewFile).href)).loadDragonView(plan.id, opts); }
|
|
109
|
+
}
|
|
110
|
+
if (mine !== token) { v.dispose?.(); return; }
|
|
111
|
+
view = v; state = v.initial(now() - (greet && !quiet ? 0 : AGES_OUT));
|
|
112
|
+
if (!quiet && (show || greet)) appear(why);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (mine !== token) return;
|
|
115
|
+
plan = { ...plan, enabled: false, why: `could not load dragon art: ${e.message}` };
|
|
116
|
+
}
|
|
117
|
+
notify();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
subscribe(f) { listeners.add(f); return () => listeners.delete(f); },
|
|
122
|
+
/** Attach: entrance when `greet` (explicit attach), none when re-attaching after a restart. */
|
|
123
|
+
attach(a, { greet: g = true } = {}) { agent = a || null; greet = g; forced = false; moments.reset(); return load(); },
|
|
124
|
+
detach() { agent = null; token++; view?.dispose?.(); view = null; state = null; showUntil = 0; job = null; forced = false; clearTimeout(timer); plan = replan(); notify(); },
|
|
125
|
+
/** Re-read the saved mode (after /dragon companion on|off|greeting). An enable greets. */
|
|
126
|
+
refresh() { greet = true; forced = false; return load(); },
|
|
127
|
+
/** Explicit /dragon: show the attached agent's dragon for one dwell, in any mode (even off). false = no dragon agent attached. */
|
|
128
|
+
async invoke() {
|
|
129
|
+
if (!agent || !dragonForAgent(agent)) return false;
|
|
130
|
+
// The view hides itself once its arrival is over unless it persists, and `greeting`/`off` (or a resumed attach) do not persist:
|
|
131
|
+
// an explicit call reloads it persistent so the whole dwell shows, whatever mode and however long ago it attached.
|
|
132
|
+
if (!view || !plan.persist) { forced = true; await load({ show: true, persist: true, why: 'invoke' }); return Boolean(view); }
|
|
133
|
+
appear('invoke'); return true;
|
|
134
|
+
},
|
|
135
|
+
/** Send it away now. */
|
|
136
|
+
dismiss() { if (showUntil) { showUntil = 0; if (summoned) restore(); else notify(); } },
|
|
137
|
+
/** A keypress: sends it away, but not during the first KEY_GRACE_MS of a dwell (the Enter that invoked it is followed by typing). */
|
|
138
|
+
keypress() { if (showUntil && now() - shownAt >= KEY_GRACE_MS) this.dismiss(); },
|
|
139
|
+
/**
|
|
140
|
+
* A domain moment calls a dragon: companion mode `on` only, never while the user is typing, never while another dragon is on
|
|
141
|
+
* screen, and not the same dragon again inside SUMMON_COOLDOWN_MS. Works with nothing attached. The caption is the moment's own
|
|
142
|
+
* truthful line. -> true when it showed.
|
|
143
|
+
*/
|
|
144
|
+
async summon(dragon, caption) {
|
|
145
|
+
if (companionMode(settings(), env) !== 'on' || typing() || live()) return false;
|
|
146
|
+
const last = lastSummon.get(dragon);
|
|
147
|
+
if (last != null && now() - last < SUMMON_COOLDOWN_MS) return false;
|
|
148
|
+
lastSummon.set(dragon, now());
|
|
149
|
+
if (agent && dragonForAgent(agent) === dragon && view && plan.persist) { summonCaption = caption; appear('summon'); return true; }
|
|
150
|
+
await load({ show: true, as: dragon, caption, persist: true, why: 'summon' });
|
|
151
|
+
return Boolean(view);
|
|
152
|
+
},
|
|
153
|
+
/** Roster rows (every refresh) -> the first objective/loop that just finished or failed summons its dragon. */
|
|
154
|
+
watchRoster(rows) { const m = rosterWatch.observe(rows)[0]; return m ? this.summon(m.dragon, m.caption) : Promise.resolve(false); },
|
|
155
|
+
/** True while the dragon is on screen and moving: the host asks the shared clock for frames only then. */
|
|
156
|
+
animating() { return Boolean(view && state && !plan.still && live()); },
|
|
157
|
+
/** Feed a reducer action (turn.begin / event / approval.found). Cheap no-op when no companion is active. */
|
|
158
|
+
tap(action) {
|
|
159
|
+
if (!view) return;
|
|
160
|
+
const ev = toCompanionEvent(action);
|
|
161
|
+
if (ev) { state = view.reduce(state, ev, now()); jobs?.observe?.(ev); }
|
|
162
|
+
if (action?.type === 'turn.begin') moments.turnStarted();
|
|
163
|
+
const m = moments.observe(ev);
|
|
164
|
+
if (m?.companion) keyMoment();
|
|
165
|
+
},
|
|
166
|
+
/** True while an attached companion shows key moments (mode on): it owns them, so the Q dragon stays quiet. */
|
|
167
|
+
ownsMoments() { return Boolean(view && companionMode(settings(), env) === 'on'); },
|
|
168
|
+
/** The Q dragon's mood for a state change, or null when the companion owns the moment (one dragon per moment). */
|
|
169
|
+
moodFor(prev, action, next) { return this.ownsMoments() ? null : moodForEvent(prev, action, next); },
|
|
170
|
+
plan: () => plan,
|
|
171
|
+
/** Why the art cannot show (mode on/greeting but art missing or unloadable), or null. Independent of the attached agent. */
|
|
172
|
+
artProblem() {
|
|
173
|
+
if (companionMode(settings(), env) === 'off') return null;
|
|
174
|
+
if (/dragon art/.test(plan.why || '')) return plan.why;
|
|
175
|
+
const p = companionPlan({ agent: 'lucid', settings: settings(), env, isTTY: true, exists, bundled: VENDORED_DRAGONLAB });
|
|
176
|
+
return !p.enabled && /dragon art/.test(p.why || '') ? p.why : null;
|
|
177
|
+
},
|
|
178
|
+
/** One line, once per session, when a dragon agent attaches but the art cannot show. Otherwise null. */
|
|
179
|
+
takeArtNotice() {
|
|
180
|
+
if (noticed || !agent || !dragonForAgent(agent) || plan.enabled || !/dragon art/.test(plan.why || '')) return null;
|
|
181
|
+
noticed = true;
|
|
182
|
+
return `Dragon companion is ${plan.mode} but its art could not be loaded, so no dragon will show (${plan.why}). /dragon companion status`;
|
|
183
|
+
},
|
|
184
|
+
mode: () => plan.mode,
|
|
185
|
+
/** -> null | { kind:'art', frame, caption, name } | { kind:'label', text } */
|
|
186
|
+
snapshot(t, { cols, rows }) {
|
|
187
|
+
const out = this.snapshotNow(t, { cols, rows });
|
|
188
|
+
if (out && unpainted) { unpainted = false; shownAt = now(); arm(); } // first picture on screen: the dwell and the keypress grace count from here, not from when the art finished loading
|
|
189
|
+
return out;
|
|
190
|
+
},
|
|
191
|
+
snapshotNow(t, { cols, rows }) {
|
|
192
|
+
if (summoned && view && !live()) { restore(); return null; } // a summoned dragon's dwell ended: back to the attached one (never while its art is still loading: a frame drawn then cancelled the summon)
|
|
193
|
+
if (!view || !state || !live()) return null;
|
|
194
|
+
const big = cols >= ART_MIN_COLS && rows >= ART_MIN_ROWS;
|
|
195
|
+
let v;
|
|
196
|
+
const mood = job?.mood === 'concerned' && !state.approvals.length ? 'concerned' : null;
|
|
197
|
+
try { v = view.view(state, t, { cols, rows: big ? rows : 1, mood }); } catch { return null; }
|
|
198
|
+
if (v.hidden) return null;
|
|
199
|
+
// One caption per trigger: "Attached." only on an attach; an invoke says who is here; an approval always says so.
|
|
200
|
+
let caption = v.caption;
|
|
201
|
+
const summonedCaption = summonCaption;
|
|
202
|
+
if (state.approvals.length) caption = 'Waiting for your approval.';
|
|
203
|
+
else if (job?.caption && !state.issue) caption = job.caption; // the dragon's job: a real read (jobs.js); an approval or a recorded failure still comes first
|
|
204
|
+
else if (state.phase === 'connecting') caption = trigger === 'attach' ? 'Attached.' : trigger === 'invoke' ? `${view.name} is here.` : v.caption;
|
|
205
|
+
if (summonedCaption && now() < showUntil) caption = summonedCaption; // a domain moment states its own fact
|
|
206
|
+
if (!big) return { kind: 'label', text: `${view.name}: ${caption}` };
|
|
207
|
+
if (!v.frame) { waitingFrame = true; return null; } // the first frame is still being drawn off the main thread: draw nothing rather than a gap
|
|
208
|
+
return { kind: 'art', frame: v.frame, caption, name: view.name };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** "/dragon companion <arg>" -> { text, tone, save?, refresh? }. Pure. */
|
|
214
|
+
export function companionCommand(arg, { settings = {}, plan = null, artProblem = null } = {}) {
|
|
215
|
+
// `invoke: true` = the host also plays the attached agent's dragon for a dwell (a status check is a call for the dragon too).
|
|
216
|
+
const w = String(arg || '').trim().toLowerCase();
|
|
217
|
+
const cur = normaliseMode(settings.dragonCompanion);
|
|
218
|
+
if (!w || w === 'status') {
|
|
219
|
+
const mode = cur || plan?.mode || 'on';
|
|
220
|
+
const now = plan?.enabled ? `showing ${plan.id}` : `not showing (${plan?.why || 'no agent attached'})`;
|
|
221
|
+
if (artProblem && mode !== 'off') return { text: `Dragon companion: ${mode}, but the art can't be loaded so no dragon will show (${artProblem}). /dragon companion on|greeting|off.`, tone: 'warn' };
|
|
222
|
+
return { text: `Dragon companion: ${mode}, ${now}. /dragon companion on|greeting|off. Only Lucid, Hyperion, Seer, Sentinel, Drago and Cipher have dragons.`, invoke: true };
|
|
223
|
+
}
|
|
224
|
+
const m = normaliseMode(w);
|
|
225
|
+
if (!m) return { text: `Usage: /dragon companion ${COMPANION_MODES.join('|')}|status`, tone: 'warn' };
|
|
226
|
+
const what = { on: 'appears on attach and at key moments such as a new approval, a failure or a long task finishing, then leaves after a few seconds', greeting: 'appears on attach only, then leaves', off: 'never appears on its own' }[m];
|
|
227
|
+
if (artProblem && m !== 'off') return { text: `Dragon companion: ${m} saved, but the art can't be loaded so no dragon will show (${artProblem}).`, tone: 'warn', save: m, refresh: true };
|
|
228
|
+
return { text: `Dragon companion: ${m} (${what}). /dragon always calls it. Saved to this profile.`, tone: 'ok', save: m, refresh: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The live block. Re-renders on the shared clock; draws nothing when there is no companion. */
|
|
232
|
+
export function DragonCompanionLive({ ctl, cols, rows }) {
|
|
233
|
+
const t = useFrame();
|
|
234
|
+
const [, bump] = React.useState(0);
|
|
235
|
+
React.useEffect(() => ctl.subscribe(() => bump(x => x + 1)), [ctl]);
|
|
236
|
+
const snap = ctl.snapshot(t, { cols, rows });
|
|
237
|
+
if (!snap) return null;
|
|
238
|
+
if (dragonActive()) return null; // an explicit /dragon <clip> owns the canvas: the companion steps aside (both stacked overflow a 34-row terminal and Ink wipes the screen, 2026-10-25)
|
|
239
|
+
if (snap.kind === 'label') return h(Box, { paddingLeft: 2 }, h(Text, { wrap: 'truncate' }, snap.text));
|
|
240
|
+
return h(Box, { flexDirection: 'row', marginTop: 1 },
|
|
241
|
+
h(Box, { flexDirection: 'column', flexShrink: 0 }, ...snap.frame.map((r, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(r, `dc${k}-`)))),
|
|
242
|
+
h(Box, { flexDirection: 'column', paddingLeft: 2 },
|
|
243
|
+
h(Text, { bold: true }, snap.name.toUpperCase()),
|
|
244
|
+
h(Text, { wrap: 'wrap' }, snap.caption)));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* /detach back to Q: with dragons on (mode on or greeting) the Q dragon gets one greeting, the way an attach shows the agent's.
|
|
249
|
+
* -> null | { clip:'pick' } | { note } (the window is too small: say so in one line). Pure.
|
|
250
|
+
*/
|
|
251
|
+
export function detachGreeting({ settings = {}, env = process.env, rows = null, chromeH = 10, hadAgent = true } = {}) {
|
|
252
|
+
if (!hadAgent || companionMode(settings, env) === 'off' || motionPolicy(env).off) return null;
|
|
253
|
+
const room = clipRoom('dragon', rows, chromeH);
|
|
254
|
+
return room.ok ? { clip: 'pick' } : { note: room.note };
|
|
255
|
+
}
|