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/render.js
CHANGED
|
@@ -2,25 +2,36 @@
|
|
|
2
2
|
* QUOXON view layer (Ink 6, React.createElement). Width-aware everywhere,
|
|
3
3
|
* horizontal rules only (no side borders), semantic colour, mono-safe.
|
|
4
4
|
*/
|
|
5
|
+
import { toolCardLines } from './toolCards.js';
|
|
5
6
|
import React from 'react';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import { Box, Text, useStdout } from 'ink';
|
|
8
|
-
import
|
|
9
|
+
import stringWidth from 'string-width';
|
|
10
|
+
import { graphemes, lineCol, selection, composerFrom } from './textBuffer.js';
|
|
9
11
|
|
|
10
12
|
const h = React.createElement;
|
|
11
13
|
/** Coloured runs (from theme.paintLine/paintRuns) as Text children; the characters are unchanged. */
|
|
12
14
|
const runs = (rs) => rs.map((r, i) => (r.role ? h(Text, { key: i, color: c(r.role) }, r.text) : r.text));
|
|
13
15
|
|
|
14
|
-
import { COLORS, MODE_COLOR, MIN_COLS, mono, c, clampCols, clip, paintLine, paintRuns, agentColor, proto } from './theme.js';
|
|
16
|
+
import { COLORS, MODE_COLOR, MIN_COLS, mono, c, clampCols, clip, paintLine, paintRuns, agentColor, proto, toneText, stripMarks, providerOf, providerColor } from './theme.js';
|
|
15
17
|
import { reducedMotion, motionAllowed } from './motion/clock.js';
|
|
16
18
|
import { ruleColors, ringStep, ringMode } from './motion/ring.js';
|
|
17
19
|
import { useFrame } from './motion/frame.js';
|
|
18
20
|
import { rgbStr, hexRgb, mix, clamp01, mixOk, pulse01 } from './motion/color.js';
|
|
19
21
|
import { revealCells, TIMING } from './motion/reveal.js';
|
|
22
|
+
import { handoffFrame } from './motion/level.js';
|
|
23
|
+
import { dividerRuleEl } from './dividerRule.js';
|
|
20
24
|
import * as themeNs from './theme.js';
|
|
21
25
|
import { qArt, qArtFrame } from '../core/banner.js';
|
|
22
|
-
import {
|
|
26
|
+
import { cmd } from './palette.js';
|
|
27
|
+
import { themeRole, layoutRows, expandLines, limitGrid } from './rows.js';
|
|
28
|
+
import { wrapHang } from './objects/model.js';
|
|
29
|
+
import { Markdown, wrapCell, markdownRows, wrapRows, WText, Heading } from './markdown.js';
|
|
30
|
+
import { parseAsk, keysLabel, askText } from './ask.js';
|
|
31
|
+
import { parseAsideNote } from './asideView.js';
|
|
32
|
+
import { formatSlotBody, inlineable, codeRows } from './slotFormat.js';
|
|
23
33
|
import { termRows } from './term.js';
|
|
34
|
+
import { cellWidth, ambiguousWide } from './cells.js';
|
|
24
35
|
import { welcomeModel } from './welcome.js';
|
|
25
36
|
import { Q_GLYPH, qColor, stateWord, indicatorState, fmtTokens, wordFrame, wordColor } from './indicator.js';
|
|
26
37
|
|
|
@@ -28,7 +39,12 @@ import { qSize, qDims, orbitFrame, qLines, labSize, introPlan, qCells, cellsToAn
|
|
|
28
39
|
import { glyphStill, glyphMode, colorDepth } from './qglyph.js';
|
|
29
40
|
export { INTRO_MS };
|
|
30
41
|
import { QGlyph, cellNodes } from './qmotion.js';
|
|
42
|
+
import { CLIPS as DRAGON_CLIPS, clipCells } from './qdragonScenes.js';
|
|
43
|
+
import { dragonLayout } from './qdragon.js';
|
|
31
44
|
import { ObjectView } from './objects/view.js';
|
|
45
|
+
import { aoclLineSegs, aoclTableLines } from './aocl.js';
|
|
46
|
+
import { cardLines, wrapText, loopPinned } from './loopCard.js';
|
|
47
|
+
import { orgLabel } from './orgLabel.js';
|
|
32
48
|
export { COLORS, MODE_COLOR, MIN_COLS, clampCols, clip, reducedMotion };
|
|
33
49
|
|
|
34
50
|
// ---- working indicator (the one animated line) -------------------------------------------------
|
|
@@ -50,7 +66,7 @@ export const STATIC_RAIN = 'クオック'.slice(0, RAIN);
|
|
|
50
66
|
export function describeActivity(state) {
|
|
51
67
|
const running = state.blocks.filter(b => b.kind === 'tool' && b.status === 'running');
|
|
52
68
|
if (state.approvals.length) return 'Waiting for approval';
|
|
53
|
-
if (running.length)
|
|
69
|
+
if (running.length) { const r = running[running.length - 1]; return r.agentName ? `Delegating to ${r.agentName}` : `Running ${r.tool}`; }
|
|
54
70
|
if (state.turn?.status === 'interrupting') return 'Stopping';
|
|
55
71
|
return state.turn?.stage || 'Working';
|
|
56
72
|
}
|
|
@@ -60,7 +76,7 @@ export function describeActivity(state) {
|
|
|
60
76
|
* is on, once a second otherwise) and passes `now`. `motion` false = static word and colour (reduced motion);
|
|
61
77
|
* colour is separately off under NO_COLOR.
|
|
62
78
|
*/
|
|
63
|
-
export const Working = ({ activity, since, interrupting, escLabel, state, failing = false, tokens = 0, act = null, now: nowProp, motion = !reducedMotion() }) => {
|
|
79
|
+
export const Working = ({ activity, since, interrupting, escLabel, state, failing = false, tokens = 0, act = null, now: nowProp, motion = !reducedMotion(), handoff = false }) => {
|
|
64
80
|
const now = useFrame(nowProp);
|
|
65
81
|
const quiet = !motion;
|
|
66
82
|
const t = Math.floor(now / 120); // the indicator's colour periods are in 120 ms ticks
|
|
@@ -77,7 +93,12 @@ export const Working = ({ activity, since, interrupting, escLabel, state, failin
|
|
|
77
93
|
// clock, ~30% scrambled at a time, cascades of 2..4 half-width katakana resolving with a flash. Reduced
|
|
78
94
|
// motion: a still, coloured, plain word. NO_COLOR: the motion stays, the colour goes. Elapsed, tokens and
|
|
79
95
|
// hints are never scrambled.
|
|
80
|
-
|
|
96
|
+
// Silent wait (FW1): after 3 s on one activity with no answer text, say what it is waiting on, live.
|
|
97
|
+
const actSecs = act ? Math.max(0, Math.floor((now - act.since) / 1000)) : 0;
|
|
98
|
+
const longWait = Boolean(act) && st === 'working' && actSecs >= 3 && act.word !== 'Writing';
|
|
99
|
+
const waitTool = longWait && act.tool ? act.tool : null;
|
|
100
|
+
const stale = longWait && !act.tool && (act.word === 'Sending' || act.word === 'Routing');
|
|
101
|
+
const plain = clip(stateWord(st, stale ? 'Thinking' : (act ? act.word : activity)), 40);
|
|
81
102
|
const scramble = st === 'working' && !quiet;
|
|
82
103
|
const cst = st === 'working' && (act ? act.word : activity) === 'Writing' ? 'writing' : st;
|
|
83
104
|
const cells = scramble ? wordFrame(plain, now, { since: seenRef.current.at }) : [...plain].map(ch => ({ ch, glyph: false, flash: false }));
|
|
@@ -87,14 +108,18 @@ export const Working = ({ activity, since, interrupting, escLabel, state, failin
|
|
|
87
108
|
color: mono() ? undefined : `rgb(${wordColor(cst, i, n, now, g.flash ? 'flash' : g.glyph ? 'glyph' : 'letter', quiet).join(',')})`
|
|
88
109
|
}, g.ch));
|
|
89
110
|
const ell = st === 'failing' ? null : h(Text, { key: 'e', color: mono() ? undefined : `rgb(${wordColor(cst, n, n + 1, now, 'letter', quiet).join(',')})` }, '…');
|
|
90
|
-
const detail = act?.detail && st !== 'failing' ? clip(act.detail, 28) : '';
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
const detail = waitTool ? clip(`waiting on ${waitTool}`, 40) : (act?.detail && st !== 'failing' ? clip(act.detail, 28) : '');
|
|
112
|
+
// Expressive: while delegating, a dot travels from Quox to the agent it is handing to (motion/level.js).
|
|
113
|
+
const delegating = handoff && !quiet && st === 'working' && (act ? act.word : activity) === 'Delegating';
|
|
114
|
+
const track = delegating ? h(Text, { key: 'ho' }, ' ', ...handoffFrame(now, 5, { mono: mono() }).map((cl, i) => h(Text, { key: i, color: cl.rgb ? rgbStr(cl.rgb) : undefined }, cl.ch))) : null;
|
|
115
|
+
const word = h(Text, { wrap: 'truncate-end' }, h(Text, null, ...wordNodes, ell), track,
|
|
116
|
+
detail ? h(Text, { color: c('dim') }, ` ${delegating ? '' : '· '}${detail}`) : '');
|
|
93
117
|
const { stdout: so } = useStdout();
|
|
94
118
|
const narrow = (so?.columns || 100) < 60; // keep the line on one row: short hint, tokens still shown
|
|
95
119
|
const tok = tokens > 0 ? ` · ↓ ${fmtTokens(tokens)} tokens` : '';
|
|
96
|
-
const
|
|
97
|
-
|
|
120
|
+
const sep = st === 'failing' ? '' : (detail ? ' · ' : ' '); // the separator lives in the tail so a squeezed word never swallows it
|
|
121
|
+
const tail = st === 'failing' ? '' : `${sep}${waitTool ? actSecs : secs}s${tok} · ${interrupting ? 'stopping; waiting for the server to confirm' : (narrow ? 'Esc' : (escLabel || 'Esc to stop'))}`;
|
|
122
|
+
return h(Box, { paddingLeft: 2 }, q, h(Text, null, ' '), word, h(Text, { color: c('dim'), wrap: 'truncate-end' }, tail));
|
|
98
123
|
};
|
|
99
124
|
|
|
100
125
|
// ---- drop-in reveals (motion/reveal.js) ------------------------------------------------------------
|
|
@@ -197,26 +222,60 @@ export function fitResult(summary, max) {
|
|
|
197
222
|
return ''; // not even one whole pair fits: show none rather than a value without its key
|
|
198
223
|
}
|
|
199
224
|
|
|
200
|
-
|
|
225
|
+
/**
|
|
226
|
+
* The ignition glow's colour (expressive motion): `hex` flared by g 0..1. Dark terminals flare towards a warm
|
|
227
|
+
* white, light ones towards amber (white would vanish on a light background). g 0 returns hex untouched.
|
|
228
|
+
*/
|
|
229
|
+
export function igniteColor(col, g, light = themeNs.isLight()) {
|
|
230
|
+
const rgb = Array.isArray(col) ? col : typeof col === 'string' && /^#[0-9a-f]{6}$/i.test(col) ? hexRgb(col) : null;
|
|
231
|
+
if (!(g > 0) || !rgb) return Array.isArray(col) ? rgbStr(col) : col;
|
|
232
|
+
return rgbStr(mix(rgb, light ? [217, 119, 6] : [255, 244, 214], Math.min(1, g) * (light ? 0.7 : 0.85)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Pure: the delegation row text. body while running names the target and the task; the result names the outcome. */
|
|
236
|
+
export function delegationText(b) {
|
|
237
|
+
const who = b.agentName;
|
|
238
|
+
const body = b.status === 'running' ? `Delegating to ${who}${b.taskGist ? ` \u00b7 ${b.taskGist}` : ''}` : `${who}${b.taskGist ? ` \u00b7 ${b.taskGist}` : ''}`;
|
|
239
|
+
if (b.status === 'running') return { body, result: '', warn: false };
|
|
240
|
+
if (b.status === 'ok' && Number.isFinite(b.toolsUsed)) {
|
|
241
|
+
return b.toolsUsed === 0
|
|
242
|
+
? { body, result: `${who} did nothing (0 tools)`, warn: true }
|
|
243
|
+
: { body, result: `${who} finished \u00b7 used ${b.toolsUsed} tool${b.toolsUsed === 1 ? '' : 's'}`, warn: false };
|
|
244
|
+
}
|
|
245
|
+
return { body, result: '', warn: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function ToolRow({ b, cols, fx = null, pulse = null, ignite = 0 }) {
|
|
201
249
|
const glyph = STATUS_GLYPH[b.status] || '◐';
|
|
202
250
|
const tone = STATUS_TONE[b.status] || 'sky';
|
|
203
251
|
const dur = fmtDuration(b.durationMs);
|
|
204
252
|
const { family, verb } = toolLabel(b.tool);
|
|
205
253
|
const name = clip(family, 10);
|
|
206
|
-
|
|
254
|
+
// QUOXON-FW3: a delegation names WHO ran it, what it was for, and whether it did anything.
|
|
255
|
+
const deleg = b.agentName ? delegationText(b) : null;
|
|
256
|
+
const body0 = deleg ? deleg.body : b.summary && b.summary !== b.tool ? b.summary : verb;
|
|
207
257
|
const room = Math.max(4, cols - 2 - 2 - gw(name) - 3); // indent, glyph + space, family, ' │ '
|
|
208
258
|
const running = b.status === 'running';
|
|
209
259
|
// the facts after the result: duration, then witnessing state
|
|
210
260
|
let after = [];
|
|
261
|
+
let envExtra = null; // an id never shares a clipped line: when it does not fit it wraps onto its own line(s)
|
|
211
262
|
if (!running && b.receipt) {
|
|
212
263
|
const v = b.receipt.verified;
|
|
213
264
|
after = [dur, v === true ? 'witnessed' : v === false ? 'unverified' : 'verification pending', `receipt ward:${b.receipt.ward_seq}`].filter(Boolean);
|
|
265
|
+
if (b.envelopeId) envExtra = `envelope ${b.envelopeId}`;
|
|
214
266
|
} else if (!running) {
|
|
215
|
-
after = [dur, b.envelopeId ? `envelope ${
|
|
267
|
+
after = [dur, b.envelopeId ? `envelope ${b.envelopeId}` : '', b.envelopeId ? 'witnessing pending' : ''].filter(Boolean);
|
|
216
268
|
}
|
|
217
|
-
const result = running ? '' : (b.resultSummary ? `${b.status === 'unknown' ? 'unknown: ' : ''}${b.resultSummary}` : b.status);
|
|
269
|
+
const result = running ? '' : deleg && deleg.result ? deleg.result : (b.resultSummary ? `${b.status === 'unknown' ? 'unknown: ' : ''}${b.resultSummary}` : b.status);
|
|
218
270
|
const bodyStr = clipWords(body0, running || !result ? room : Math.max(6, Math.min(gw(body0), Math.floor(room / 2))));
|
|
219
271
|
const tailRoom = room - gw(bodyStr) - 3;
|
|
272
|
+
if (!running && !b.receipt && b.envelopeId && gw(after.join(' · ')) + 3 > Math.max(0, tailRoom)) {
|
|
273
|
+
envExtra = `envelope ${b.envelopeId} · witnessing pending`;
|
|
274
|
+
after = after.filter(f => !/^envelope /.test(f) && f !== 'witnessing pending');
|
|
275
|
+
}
|
|
276
|
+
const extra = envExtra ? wrapHang(envExtra, Math.max(12, cols - 6), 2).map((l, i) => h(Text, { key: `e${i}`, color: c('muted') }, ` ${l}`)) : [];
|
|
277
|
+
if (!running) extra.push(...toolCardLines(b, cols).map((l, i) => h(Text, { key: `c${i}`, color: c(l.head ? 'muted' : 'prose'), wrap: 'truncate-end' }, ` ${l.text}`))); // HITL-INBOX-BAR: a list answer as a table
|
|
278
|
+
const withExtra = (node) => (extra.length ? h(Box, { flexDirection: 'column' }, node, ...extra) : node);
|
|
220
279
|
let tail = '';
|
|
221
280
|
if (!running && tailRoom > 0) {
|
|
222
281
|
const facts = after.join(' · ');
|
|
@@ -227,17 +286,21 @@ export function ToolRow({ b, cols, fx = null, pulse = null }) {
|
|
|
227
286
|
const verified = b.receipt?.verified === true;
|
|
228
287
|
const glyphColor = b.receipt && !running ? (verified ? 'ok' : 'warn') : tone;
|
|
229
288
|
// Running: one glyph whose colour pulses sky <-> violet in OKLab (never a spinner of different glyphs).
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
289
|
+
const runRgb = running && pulse != null && !mono() ? mixOk(hexRgb(COLORS.sky), hexRgb(COLORS.violet), pulse01(pulse, 1200)) : null;
|
|
290
|
+
const runColor = runRgb ? rgbStr(runRgb) : null;
|
|
291
|
+
const tailRuns = tail ? paintRuns(tail, deleg && deleg.warn ? 'warn' : tone === 'ok' ? 'value' : tone).map(r => (/^(witnessed)$/.test(r.text.trim()) ? { ...r, role: 'ok' } : r)) : [];
|
|
292
|
+
// Expressive: the row ignites as the tool starts (a short warm flare on the glyph and the family, motion/level.js).
|
|
293
|
+
const lit = running && ignite > 0 && !mono();
|
|
294
|
+
const glyphC = lit ? igniteColor(runRgb || c(glyphColor) || COLORS.sky, ignite) : runColor || glyphColor;
|
|
295
|
+
const head = [{ t: ' ' }, { t: glyph, color: glyphC, bold: lit && ignite > 0.5 }, { t: ' ' }, { t: name, color: lit ? igniteColor(agentColor(name), ignite * 0.7) : agentColor(name), bold: true }, { t: ' │ ', color: 'muted' }, { t: bodyStr, color: 'text' }];
|
|
233
296
|
const tailSegs = tail ? [{ t: ' │ ', color: 'muted' }, ...tailRuns.map(r => ({ t: r.text, color: r.role }))] : [];
|
|
234
297
|
// Settle (running -> result): the glyph flashes once and the result decodes in behind it.
|
|
235
298
|
if (fx?.settle != null && !running) {
|
|
236
299
|
const flash = fx.settle < 0.12 && !mono();
|
|
237
300
|
if (flash) head[1] = { t: glyph, color: '#ffffff', bold: true };
|
|
238
|
-
return h(Box, null, ...fxNodes(head, null), ...fxNodes(tailSegs, { e: fx.settle, timing: TIMING.settle }, 7));
|
|
301
|
+
return withExtra(h(Box, null, ...fxNodes(head, null), ...fxNodes(tailSegs, { e: fx.settle, timing: TIMING.settle }, 7)));
|
|
239
302
|
}
|
|
240
|
-
return h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3));
|
|
303
|
+
return withExtra(h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3)));
|
|
241
304
|
}
|
|
242
305
|
|
|
243
306
|
/** Compact protocol detail: drop the quox.chat./quox. intent prefix and keep only the tail of long correlation ids, so the line fits instead of clipping. */
|
|
@@ -271,6 +334,37 @@ function protoIconColor(protocol, { pulse = null, fx = null } = {}) {
|
|
|
271
334
|
return rgbStr(col);
|
|
272
335
|
}
|
|
273
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Pure: a protocol row body wrapped into at most `max` lines of `room` cells, breaking only at spaces (a word
|
|
339
|
+
* longer than a whole line is the one thing ever cut). Overflow ends the last line with " …" after a whole word.
|
|
340
|
+
*/
|
|
341
|
+
export function wrapTrace(body, room, max = 4) {
|
|
342
|
+
const words = String(body || '').split(' ').filter(Boolean);
|
|
343
|
+
const w = (s) => [...s].length;
|
|
344
|
+
const lines = [];
|
|
345
|
+
let cur = '';
|
|
346
|
+
let i = 0;
|
|
347
|
+
for (; i < words.length && lines.length < max; i++) {
|
|
348
|
+
let word = words[i];
|
|
349
|
+
if (!cur && w(word) > room) { // longer than a line: the only hard break
|
|
350
|
+
while (w(word) > room && lines.length < max) { lines.push([...word].slice(0, room).join('')); word = [...word].slice(room).join(''); }
|
|
351
|
+
if (lines.length >= max) { i++; break; }
|
|
352
|
+
cur = word; continue;
|
|
353
|
+
}
|
|
354
|
+
if (!cur) { cur = word; continue; }
|
|
355
|
+
if (w(cur) + 1 + w(word) <= room) { cur += ' ' + word; continue; }
|
|
356
|
+
lines.push(cur); cur = '';
|
|
357
|
+
i--; // place this word on the next line
|
|
358
|
+
}
|
|
359
|
+
if (cur && lines.length < max) lines.push(cur);
|
|
360
|
+
if (i < words.length) { // more than fits: end on a whole word plus an ellipsis
|
|
361
|
+
let last = lines.length ? lines[lines.length - 1] : '';
|
|
362
|
+
while (last && w(last) + 2 > room) last = last.includes(' ') ? last.slice(0, last.lastIndexOf(' ')) : [...last].slice(0, Math.max(0, room - 2)).join('');
|
|
363
|
+
lines[Math.max(0, lines.length - 1)] = `${last} …`;
|
|
364
|
+
}
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
|
|
274
368
|
export function TraceRow({ b, cols, fx = null, pulse = null }) {
|
|
275
369
|
const [g, label] = PROTO[b.protocol] || ['·', String(b.protocol || 'TRACE').toUpperCase()];
|
|
276
370
|
const ref = b.ref != null ? ` · ${String(b.ref).slice(0, 8)}` : '';
|
|
@@ -286,25 +380,38 @@ export function TraceRow({ b, cols, fx = null, pulse = null }) {
|
|
|
286
380
|
if (!fx) return h(Box, null, h(Text, null, ' ', icon, labelNode, h(Text, { color: c('muted') }, afterIcon.slice(labelSeg.length) + body)));
|
|
287
381
|
return h(Box, null, h(Text, null, ' '), icon, ...fxNodes([{ t: labelSeg, color: 'proto:' + b.protocol }, { t: afterIcon.slice(labelSeg.length) + body, color: 'muted' }], fx, 5));
|
|
288
382
|
}
|
|
289
|
-
// Too long even when compacted: wrap onto indented lines (at most 4) rather than hiding the end.
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
if (chars.length > room * 4) lines[3] = clip(lines[3], [...head].length + room);
|
|
383
|
+
// Too long even when compacted: wrap at word boundaries onto indented lines (at most 4) rather than hiding the end.
|
|
384
|
+
const pad = ' '.repeat([...head].length);
|
|
385
|
+
const lines = wrapTrace(body, room, 4).map((l, i) => (i ? pad : afterIcon) + l);
|
|
293
386
|
return h(Box, { flexDirection: 'column' }, ...lines.map((l, i) => (i === 0
|
|
294
387
|
? h(Text, { key: i }, ' ', icon, labelNode, h(Text, { color: c('muted') }, l.slice(labelSeg.length)))
|
|
295
388
|
: h(Text, { key: i, color: c('muted') }, l))));
|
|
296
389
|
}
|
|
297
390
|
|
|
298
391
|
export function HelpView({ model, cols }) {
|
|
392
|
+
// TUI-UX #6: the key column is sized from the usage text (capped), always followed by a two-space gutter. A usage that
|
|
393
|
+
// does not fit its column goes on its own line with the description underneath, so the two never run together.
|
|
299
394
|
const keyW = Math.min(24, Math.max(...model.filter(m => m.key).map(m => [...m.key].length), 8) + 2);
|
|
300
395
|
return h(Box, { flexDirection: 'column', marginTop: 1, width: cols }, ...model.map((m, i) => {
|
|
301
396
|
if (m.kind === 'gap') return h(Text, { key: i }, ' ');
|
|
397
|
+
if (m.kind === 'subhead') return h(Heading, { key: i, text: m.text });
|
|
302
398
|
if (m.kind === 'head') return m.dim ? h(Text, { key: i, bold: true, color: c('muted') }, m.text) : h(Heading, { key: i, text: m.text });
|
|
303
399
|
if (m.kind === 'row' || (m.kind === 'dim' && m.key)) {
|
|
304
400
|
const dim = m.kind === 'dim';
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
401
|
+
const keyRole = (r) => (r.role === 'muted' || r.role === 'key' ? r : { ...r, role: /^\s*(just type|paste|status line)\s*$/.test(m.key) ? 'label' : 'key' });
|
|
402
|
+
const keyNode = (txt) => h(Text, { color: c('muted') }, ...(dim ? [txt] : runs(paintRuns(txt, 'key').map(keyRole))));
|
|
403
|
+
const descNode = (txt) => h(Text, { color: c(dim ? 'muted' : 'text') }, ...(dim ? [txt] : runs(paintRuns(txt, 'text'))));
|
|
404
|
+
// Both columns are wrapped here, by words, so no continuation line starts with a stray space and none collides.
|
|
405
|
+
const descLines = wrapCell(m.text, Math.max(10, cols - keyW - 4));
|
|
406
|
+
if ([...m.key].length + 2 <= keyW) { // usage plus a two-space gutter fits the key column
|
|
407
|
+
return h(Box, { key: i, flexDirection: 'column' }, ...descLines.map((d, k) => h(Box, { key: k },
|
|
408
|
+
h(Box, { width: keyW + 2, flexShrink: 0 }, k === 0 ? h(Text, null, ' ', keyNode(m.key)) : h(Text, null, ' ')),
|
|
409
|
+
h(Box, { flexGrow: 1, flexShrink: 1 }, descNode(d)))));
|
|
410
|
+
}
|
|
411
|
+
const keyLines = wrapCell(m.key, Math.max(10, cols - 8)).map((l, k) => (k ? ' ' : ' ') + l);
|
|
412
|
+
return h(Box, { key: i, flexDirection: 'column' },
|
|
413
|
+
...keyLines.map((l, k) => h(Text, { key: `k${k}` }, keyNode(l))),
|
|
414
|
+
...descLines.map((d, k) => h(Box, { key: `d${k}`, paddingLeft: keyW + 2 }, descNode(d))));
|
|
308
415
|
}
|
|
309
416
|
return h(Text, { key: i, color: c(m.kind === 'dim' ? 'muted' : 'text') }, ...runs(paintRuns(m.text, m.kind === 'dim' ? 'muted' : 'text')));
|
|
310
417
|
}));
|
|
@@ -337,23 +444,38 @@ export const introAnimates = (isTTY) => motionAllowed(isTTY);
|
|
|
337
444
|
/**
|
|
338
445
|
* Text rows of the Q for a terminal size and progress (0..1 of the plan; 1 = settled). Lab sizing (labSize: the
|
|
339
446
|
* compact particle Q under 60 cols or 12 rows) and the lab timeline (introPlan: Orbit & Lock in INTRO_MS, then
|
|
340
|
-
* the
|
|
447
|
+
* the dragon greeting when `dragon`). Exported so tests can assert on frames.
|
|
341
448
|
*/
|
|
342
|
-
export function introArt(cols, rows, progress = 1, color = !mono(), {
|
|
449
|
+
export function introArt(cols, rows, progress = 1, color = !mono(), { dragon = false, theme = themeNs.isLight() ? 'light' : 'dark' } = {}) {
|
|
343
450
|
const size = labSize(cols, rows);
|
|
344
|
-
const plan = introPlan(size, {
|
|
345
|
-
const
|
|
346
|
-
|
|
451
|
+
const plan = introPlan(size, { color });
|
|
452
|
+
const ms = dragon ? INTRO_MS + DRAGON_CLIPS.greeting.dur + 300 : plan.ms;
|
|
453
|
+
const t = progress >= 1 ? Infinity : Math.max(0, progress) * ms;
|
|
454
|
+
if (dragon && t !== Infinity) {
|
|
455
|
+
// Cinematic: the settled Q opens its eyes, looks at you and winks (the dragon greeting, qdragonScenes.js). The
|
|
456
|
+
// dragon is drawn at the Q's OWN size (the compact dragon under 60 cols) on the Q's left edge, and the box keeps
|
|
457
|
+
// the dragon's height and width for the whole sequence (Orbit & Lock frames are padded), so nothing jumps.
|
|
458
|
+
const L = dragonLayout(qDims(size).cols + 6, { size, lm: 0 }), w = Math.max(qDims(size).cols, L.cols);
|
|
459
|
+
if (t >= INTRO_MS) return { size, w, ms, lines: cellsToAnsi(clipCells('greeting', t - INTRO_MS, { cols: L.cols, size, lm: 0, theme, mono: !color, label: false })) };
|
|
460
|
+
const q = cellsToAnsi(plan.at(Math.min(t, plan.ms)));
|
|
461
|
+
return { size, w, ms, lines: [...q, ...Array.from({ length: Math.max(0, L.rows - q.length) }, () => '')] };
|
|
462
|
+
}
|
|
463
|
+
return { size, w: qDims(size).cols, ms, lines: cellsToAnsi(t === Infinity ? qCells(size, { color }) : plan.at(Math.min(t, plan.ms))) };
|
|
347
464
|
}
|
|
348
465
|
|
|
349
|
-
export
|
|
466
|
+
export const INTRO_HEAD = "Hi, I'm Quox!";
|
|
467
|
+
export const INTRO_LINES = ['Tell me what you want. Watch me work.', '/help for commands, or just ask me anything.'];
|
|
468
|
+
|
|
469
|
+
export function Intro({ item, cols, progress = 1, rows, dragon = false }) {
|
|
350
470
|
const { stdout } = useStdout();
|
|
351
|
-
const art = introArt(cols, rows ?? termRows(stdout) ?? 40, progress, !mono(), {
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
h(
|
|
471
|
+
const art = introArt(cols, rows ?? termRows(stdout) ?? 40, progress, !mono(), { dragon });
|
|
472
|
+
// Beside the art only when the longest line fits there; otherwise the text goes under the art, left-aligned and wrapped, never clipped.
|
|
473
|
+
const need = Math.max(...[INTRO_HEAD, ...INTRO_LINES, item.text].map(l => [...l].length));
|
|
474
|
+
const wide = cols >= 60 && cols - 2 - art.w - 2 >= need;
|
|
475
|
+
const info = h(Box, { flexDirection: 'column', justifyContent: 'center', marginLeft: wide ? 2 : 0, marginTop: wide ? 0 : 1, flexShrink: 1, ...(wide ? {} : { width: Math.max(10, cols - 4) }) },
|
|
476
|
+
h(Grad, { text: INTRO_HEAD }),
|
|
477
|
+
...INTRO_LINES.map((l, k) => h(Text, { key: k, color: c('text'), wrap: 'wrap' }, l)),
|
|
478
|
+
h(Text, { color: c('label'), wrap: 'wrap' }, item.text)); // version/build line: quieter (normal weight, soft brand tint), never grey
|
|
357
479
|
return h(Box, { flexDirection: wide ? 'row' : 'column', marginTop: 1, marginBottom: 2, marginLeft: 2 }, h(Box, { flexDirection: 'column' }, ...art.lines.map((l, k) => h(Text, { key: k, wrap: 'truncate' }, l))), info);
|
|
358
480
|
}
|
|
359
481
|
|
|
@@ -368,10 +490,58 @@ export function revealTimingFor(item) {
|
|
|
368
490
|
}
|
|
369
491
|
}
|
|
370
492
|
|
|
371
|
-
|
|
493
|
+
// QUOXON-AOCL: ONE line per turn ("▤ AOCL 11/11 ✔ · 9 passed · 2 bypassed · 34 ms"), red naming the layer
|
|
494
|
+
// on a reject; with table:true, the /aocl per-layer table. White on dark, never grey (owner rule).
|
|
495
|
+
const AOCL_TONE = { ok: 'ok', fail: 'fail', warn: 'warn', text: 'input', error: 'fail', accent: 'sky' };
|
|
496
|
+
export function AoclRow({ item, cols }) {
|
|
497
|
+
const icon = h(Text, { color: protoIconColor('aocl') }, '▤');
|
|
498
|
+
const label = h(Text, { color: proto('aocl'), bold: true }, ' AOCL ');
|
|
499
|
+
if (item.table) {
|
|
500
|
+
const rows = aoclTableLines(item.sum);
|
|
501
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...rows.map(([t, tone], i) => h(Text, { key: i, color: c(AOCL_TONE[tone] || 'input') }, clip(i === 0 ? t : ' ' + t, cols - 4))));
|
|
502
|
+
}
|
|
503
|
+
let room = Math.max(10, cols - 10);
|
|
504
|
+
const segs = [];
|
|
505
|
+
for (const sg of aoclLineSegs(item.sum).segs) {
|
|
506
|
+
if (room <= 0) break;
|
|
507
|
+
const t = clip(sg.t, room);
|
|
508
|
+
room -= [...t].length;
|
|
509
|
+
segs.push(h(Text, { key: segs.length, color: c(AOCL_TONE[sg.tone] || 'input') }, t));
|
|
510
|
+
}
|
|
511
|
+
return h(Box, { width: cols }, h(Text, null, ' ', icon, label, ...segs));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Visual rhythm, decided in ONE place (the history renderer), never per producer. Blank rows BEFORE an item given
|
|
516
|
+
* the item above it (an item never carries a trailing blank, so two blanks in a row cannot happen):
|
|
517
|
+
* - a blank row between EVERY distinct block (user message, tool group, answer, notice group, AOCL footer, tip
|
|
518
|
+
* frame, bang block); tight only inside a block: a run of tool rows, a run of notices, the AOCL + Thought
|
|
519
|
+
* footer lines. A blank follows the prompt, separates the answer from the footer, and follows the footer.
|
|
520
|
+
* - items that draw their own leading blank (intro, welcome, help, object cards) add none here
|
|
521
|
+
* - under 20 terminal rows nothing is padded: only a new turn keeps its blank
|
|
522
|
+
*/
|
|
523
|
+
const SELF_SPACED = new Set(['intro', 'welcome', 'help', 'object']);
|
|
524
|
+
const GROUP = { tool: 'tools', trace: 'tools', note: 'notice', error: 'notice', aocl: 'footer', summary: 'footer' };
|
|
525
|
+
const FOOTER = new Set(['aocl', 'summary']);
|
|
526
|
+
export function historyGap(prev, item, rows = null) {
|
|
527
|
+
if (!item || SELF_SPACED.has(item.kind)) return 0;
|
|
528
|
+
if (item.kind === 'loopcard' && loopPinned(item.card)) return 0; // drawn above the composer, not here
|
|
529
|
+
if (item.kind === 'user') return 1;
|
|
530
|
+
if (!prev) return item.kind === 'text' ? 1 : 0;
|
|
531
|
+
if (rows != null && rows < 20) return 0;
|
|
532
|
+
if (FOOTER.has(item.kind) && FOOTER.has(prev.kind)) return 0; // the AOCL line and the Thought line are one footer block
|
|
533
|
+
const g = GROUP[item.kind];
|
|
534
|
+
if (g && g === GROUP[prev.kind]) return 0; // a run of tool rows, or of notices, is one block
|
|
535
|
+
return 1; // every other pair of distinct blocks (each tip frame is its own block)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function HistoryItem({ item, cols, live = false, fx = null, pulse = null, ignite = 0, gap }) {
|
|
539
|
+
if (gap !== undefined && gap > 0 && !['user', 'text'].includes(item.kind)) {
|
|
540
|
+
return h(Box, { marginTop: gap, flexDirection: 'column' }, h(HistoryItem, { item, cols, live, fx, pulse, ignite, gap: 0 }));
|
|
541
|
+
}
|
|
372
542
|
switch (item.kind) {
|
|
373
543
|
case 'intro': return h(Intro, { item, cols });
|
|
374
|
-
case 'user': return h(Box, { marginTop: 1, width: cols }, // gutter box + wrapping text box = hanging indent
|
|
544
|
+
case 'user': return h(Box, { marginTop: gap ?? 1, width: cols }, // gutter box + wrapping text box = hanging indent
|
|
375
545
|
h(Box, { width: 2, flexShrink: 0 }, h(Text, { color: c('sky'), bold: true }, item.queued ? '» ' : '› ')),
|
|
376
546
|
h(Box, { flexGrow: 1, flexShrink: 1 }, h(WText, { color: c('input') }, item.text)));
|
|
377
547
|
case 'welcome': return h(WelcomeView, { version: item.version, cols });
|
|
@@ -384,11 +554,18 @@ export function HistoryItem({ item, cols, live = false, fx = null, pulse = null
|
|
|
384
554
|
? h(Text, { key: i }, ...fxNodes(segs(l), { e: fx.e - i * 0.05, timing: fx.timing }, 11 + i))
|
|
385
555
|
: h(Text, { key: i, color: c('dim') }, ...runs(paintRuns(l, 'muted'))))));
|
|
386
556
|
}
|
|
387
|
-
case 'text': return h(Box, { marginTop: 1, paddingLeft: 2, flexDirection: 'column' }, h(Markdown, { text: item.text, cols: cols - 2, live }));
|
|
388
|
-
case 'tool': return h(ToolRow, { b: item, cols, fx, pulse });
|
|
557
|
+
case 'text': return h(Box, { marginTop: gap ?? 1, paddingLeft: 2, flexDirection: 'column' }, h(Markdown, { text: item.text, cols: cols - 2, live }));
|
|
558
|
+
case 'tool': return h(ToolRow, { b: item, cols, fx, pulse, ignite });
|
|
389
559
|
case 'trace': return h(TraceRow, { b: item, cols, fx, pulse });
|
|
390
|
-
case '
|
|
560
|
+
case 'aocl': return h(AoclRow, { item, cols });
|
|
561
|
+
case 'error': return h(ErrorView, { item, cols, fx });
|
|
391
562
|
case 'note': return h(NoteView, { item, cols, fx });
|
|
563
|
+
case 'slotmsg': return h(SlotMsgView, { item, cols });
|
|
564
|
+
case 'loopcard': return loopPinned(item.card) ? null : h(LoopCardView, { item, cols });
|
|
565
|
+
case 'tip': return h(TipView, { item, cols });
|
|
566
|
+
case 'whatsnew': return h(WhatsNewView, { item, cols });
|
|
567
|
+
case 'bcard': return h(BroadcastCardView, { item, cols });
|
|
568
|
+
case 'bang': return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...String(item.text).split('\n').map((l, i) => h(Text, { key: i, color: c('text') }, l || ' ')));
|
|
392
569
|
default: return null;
|
|
393
570
|
}
|
|
394
571
|
}
|
|
@@ -398,26 +575,224 @@ const SEP = ' · ';
|
|
|
398
575
|
/** Lines of a note at `width`: one line when it fits; a ' · ' list breaks only at separators. */
|
|
399
576
|
export function noteLines(text, width) {
|
|
400
577
|
const t = String(text ?? '');
|
|
401
|
-
if (!t.includes(SEP) || t.includes('\n') || wrapRows(t, width) === 1) return [t];
|
|
578
|
+
if (!t.includes(SEP) || t.includes('\n') || wrapRows(stripMarks(t), width) === 1) return [t];
|
|
402
579
|
const lines = [];
|
|
403
580
|
let cur = '';
|
|
404
581
|
for (const seg of t.split(SEP)) {
|
|
405
582
|
const next = cur ? cur + SEP + seg : seg;
|
|
406
|
-
if (!cur || graphemes(next).length <= width) cur = next;
|
|
583
|
+
if (!cur || graphemes(stripMarks(next)).length <= width) cur = next;
|
|
407
584
|
else { lines.push(cur); cur = seg; }
|
|
408
585
|
}
|
|
409
586
|
lines.push(cur);
|
|
410
587
|
return lines;
|
|
411
588
|
}
|
|
412
|
-
|
|
589
|
+
/** A note line that is one bare URL longer than the row: never hard-wrapped (a break inside a token breaks copy and click). */
|
|
590
|
+
const longUrl = (l, width) => { const t = stripMarks(l); return /^https?:\/\/\S+$/.test(t) && graphemes(t).length > width; };
|
|
591
|
+
/** A note line that opens with a status glyph and wraps hangs under its text, not under the glyph (the glyph keeps its own 2 cells). */
|
|
592
|
+
const GLYPH_LEAD = /^(?:▶|✔|✖|⚠|∞) /u;
|
|
593
|
+
const hangs = (l, width) => GLYPH_LEAD.test(l) && !longUrl(l, width) && wrapRows(stripMarks(l), width) > 1;
|
|
594
|
+
export const noteRows = (text, cols, tone) => noteLines(toneText(text, tone), cols - 2).reduce((n, l) => n + (longUrl(l, cols - 2) ? 1 : hangs(l, cols - 2) ? wrapRows(stripMarks(l).slice(2), cols - 4) : wrapRows(stripMarks(l), cols - 2)), 0);
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Z2H-7B3: a failure message can be several lines (what ran, what did not, the advice). Every line shows,
|
|
598
|
+
* wrapped at the row width, never clipped to one: the first line leads with a red ✖, the rest indent under it.
|
|
599
|
+
*/
|
|
600
|
+
export const errorRows = (text, cols) => String(text ?? '').split('\n').reduce((n, l) => n + Math.max(1, wrapRows(stripMarks(l), Math.max(10, cols - 4))), 0);
|
|
601
|
+
function ErrorView({ item, cols, fx }) {
|
|
602
|
+
const raw = String(item.text ?? '');
|
|
603
|
+
const lines = raw.split('\n');
|
|
604
|
+
const paint = (l) => paintRuns(l, 'prose').map(r => ({ t: r.text, color: r.role || 'prose' }));
|
|
605
|
+
if (lines.length === 1 && wrapRows(stripMarks(raw), Math.max(10, cols - 4)) === 1) {
|
|
606
|
+
const parts = [{ t: '✖ ', color: 'error' }, ...paint(raw)];
|
|
607
|
+
return h(Box, { paddingLeft: 2 }, fx ? h(Text, null, ...fxNodes(parts, fx, 13)) : h(Text, null, ...parts.map((p, i) => h(Text, { key: i, color: c(p.color) }, p.t))));
|
|
608
|
+
}
|
|
609
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols },
|
|
610
|
+
...lines.map((l, i) => h(Box, { key: i },
|
|
611
|
+
h(Box, { width: 2, flexShrink: 0 }, h(Text, { color: c('error') }, i === 0 ? '✖ ' : ' ')),
|
|
612
|
+
h(Box, { flexGrow: 1, flexShrink: 1 }, h(WText, { color: c('prose') }, ...runs(paintRuns(l || ' ', 'prose')))))));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* /aside: a read-only side answer, drawn apart from the conversation: a violet ┃ bar down the left, the question
|
|
617
|
+
* as its header, the answer as markdown, a quiet read-only footer. Rows: margin + header + body + footer (asideRows).
|
|
618
|
+
*/
|
|
619
|
+
function AsideView({ item, cols }) {
|
|
620
|
+
const { head, body, foot } = parseAsideNote(item.text);
|
|
621
|
+
const w = Math.max(10, cols - 5);
|
|
622
|
+
return h(Box, { marginLeft: 2, marginTop: 1, flexDirection: 'column', width: cols - 2, borderStyle: 'bold', borderTop: false, borderRight: false, borderBottom: false, borderLeftColor: c('violet'), paddingLeft: 1 },
|
|
623
|
+
h(Text, { color: c('violet'), bold: true }, clip(head, w)),
|
|
624
|
+
h(Markdown, { text: body.join('\n') || '(no answer)', cols: w }),
|
|
625
|
+
...(foot ? [h(Text, { key: 'f', color: c('quiet') }, clip(foot, w))] : []));
|
|
626
|
+
}
|
|
627
|
+
export const asideRows = (text, cols) => { const { body, foot } = parseAsideNote(text); return 2 + Math.max(1, markdownRows(body.join('\n') || '(no answer)', Math.max(10, cols - 5))) + (foot ? 1 : 0); };
|
|
413
628
|
|
|
414
629
|
function NoteView({ item, cols, fx = null }) {
|
|
415
|
-
|
|
416
|
-
|
|
630
|
+
if (item.tone === 'ask') return h(AskCard, { text: item.text, cols }); // QUOXON-ASK: every ask is the approval card
|
|
631
|
+
if (item.tone === 'aside') return h(AsideView, { item, cols });
|
|
632
|
+
// RECO-HIGHLIGHT: the recommended option of a decision card: bold, accent colour, never grey.
|
|
633
|
+
if (item.tone === 'reco') return h(Box, { paddingLeft: 2, width: cols }, h(WText, { color: c('sky'), bold: true }, stripMarks(item.text)));
|
|
634
|
+
// Tone shows as a glyph (⚠ ✖ ✔) in front; the sentence stays prose so commands and values keep their own colours.
|
|
635
|
+
const color = c('prose');
|
|
636
|
+
const text = toneText(item.text, item.tone);
|
|
417
637
|
// While it reveals, a line that fits decodes in; a line that wraps keeps plain wrapping (never mis-measured).
|
|
418
|
-
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...noteLines(
|
|
419
|
-
? h(
|
|
420
|
-
:
|
|
638
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...noteLines(text, cols - 2).map((l, i) => longUrl(l, cols - 2)
|
|
639
|
+
? h(Box, { key: i, width: graphemes(stripMarks(l)).length, flexShrink: 0 }, h(Text, { color: c('link'), wrap: 'truncate-end' }, stripMarks(l)))
|
|
640
|
+
: hangs(l, cols - 2)
|
|
641
|
+
? h(Box, { key: i }, h(Box, { width: 2, flexShrink: 0 }, h(Text, null, ...runs(paintLine(l.slice(0, 2), item.tone)))), h(Box, { flexGrow: 1, flexShrink: 1 }, h(WText, { color, bold: item.tone === 'ask' }, ...runs(paintLine(l.slice(2), item.tone)))))
|
|
642
|
+
: (fx && wrapRows(stripMarks(l), cols - 2) === 1
|
|
643
|
+
? h(Text, { key: i }, ...fxNodes(paintLine(l, item.tone).map(r => (typeof r === 'string' ? { t: r, color: 'prose' } : { t: r.text, color: r.role || 'prose' })), { e: fx.e - i * 0.05, timing: fx.timing }, 17 + i))
|
|
644
|
+
: h(WText, { key: i, color, bold: item.tone === 'ask' }, ...runs(paintLine(l, item.tone))))));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* QUOXON-MESH: a slot chat line. Incoming "◆ quox2 (Adam) 04:56 hello" (violet: another session speaking),
|
|
649
|
+
* outgoing "→ quox2 04:56 hello ✔" (sky: you). A file shows its name, three lines and a /slot log pointer;
|
|
650
|
+
* /slot log prints the whole file (`full`).
|
|
651
|
+
*/
|
|
652
|
+
/** QLOOP-LIVE: the live loop card. The card object is updated in place by loopCards.js; this reads it on every draw. */
|
|
653
|
+
export const loopCardLines = (card, cols, now = Date.now(), hint = false) => cardLines(card.state, { cols, now, frame: Math.floor(now / 250), expanded: card.expanded, focused: card.focused, confirm: card.confirm, hint });
|
|
654
|
+
const cardMemo = new WeakMap(); // card -> { key, lines }: re-laid-out only when what it draws changed
|
|
655
|
+
export function LoopCardView({ item, cols, pinned = false }) {
|
|
656
|
+
const card = item.card, now = Date.now(), st = card.state;
|
|
657
|
+
const key = [cols, pinned, card.focused, card.expanded, card.confirm, Math.floor(now / 250), Math.floor(now / 1000), st, st.observed?.length, st.spool?.length].map(String).join('|');
|
|
658
|
+
let hit = cardMemo.get(card);
|
|
659
|
+
if (!hit || hit.st !== st || hit.key !== key) { hit = { st, key, lines: loopCardLines(card, cols, now, pinned) }; cardMemo.set(card, hit); }
|
|
660
|
+
const lines = hit.lines;
|
|
661
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 2, width: cols },
|
|
662
|
+
...lines.map((rs, i) => h(Text, { key: i }, ...rs.map((r, j) => h(Text, { key: j, color: c(r.role), bold: r.bold || undefined }, r.t)))));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function SlotMsgView({ item, cols }) {
|
|
666
|
+
const l = item.line || {};
|
|
667
|
+
if (l.roomLine) { // QUOXON-ROOMS: `#group-chat QUOXON: text`, a stable colour per sender
|
|
668
|
+
return h(Box, { paddingLeft: 2, width: cols },
|
|
669
|
+
h(WText, null,
|
|
670
|
+
h(Text, { color: c('label') }, `#${l.room} `),
|
|
671
|
+
h(Text, { color: c(l.color || 'sky'), bold: true }, `${l.who}: `),
|
|
672
|
+
h(Text, { color: c('text') }, String(l.text ?? ''))));
|
|
673
|
+
}
|
|
674
|
+
const role = l.out ? 'sky' : l.urgent ? 'error' : 'violet';
|
|
675
|
+
const body = String(l.text ?? '');
|
|
676
|
+
// SLOT-RUN A (owner 2026-10-16 "messy needs a format"): one tidy header line (sender, no-reply marker, time);
|
|
677
|
+
// a short plain line stays on it, anything longer or with markdown renders below as formatted rows.
|
|
678
|
+
const time = l.time ? ` ${l.time}` : '';
|
|
679
|
+
const headLen = graphemes(`${l.glyph || '◆'} ${l.who || ''}${!l.out && l.noReply ? ' (no reply address)' : ''}${time} `).length;
|
|
680
|
+
const run = !l.out && l.run ? l.run : null;
|
|
681
|
+
const inline = !run && (Boolean(l.file) || l.out || inlineable(body, cols - 2 - headLen - 2));
|
|
682
|
+
const segRow = (segs, key) => h(Box, { key, paddingLeft: 4, width: cols }, segs.length ? h(Text, null, ...segs.map((sg, i) => h(Text, { key: i, color: c(sg.role || 'text'), bold: sg.bold || undefined }, sg.t))) : h(Text, null, ' '));
|
|
683
|
+
const out = [h(Box, { key: 'h', paddingLeft: 2, width: cols },
|
|
684
|
+
h(WText, null,
|
|
685
|
+
h(Text, { color: c(role), bold: true }, `${l.glyph || '◆'} ${l.who || ''}`),
|
|
686
|
+
!l.out && l.noReply ? h(Text, { color: c('label') }, ' (no reply address)') : null,
|
|
687
|
+
h(Text, { color: c('label') }, `${time} `),
|
|
688
|
+
inline ? h(Text, { color: c(l.urgent ? 'error' : 'text') }, body) : null,
|
|
689
|
+
l.out && l.delivered ? h(Text, { color: c('ok') }, ' ✔') : null))];
|
|
690
|
+
if (!inline && !l.file && !run && body.trim()) formatSlotBody(body, cols - 6, { role: l.urgent ? 'error' : 'text' }).forEach((segs, i) => out.push(segRow(segs, `b${i}`)));
|
|
691
|
+
// SLOT-RUN B: a run request shows the shared ask, the FULL command (never clipped), and its answer line.
|
|
692
|
+
// ASK-SETTLE: an answered or expired run request is one line (what was asked and the answer), never a live card.
|
|
693
|
+
if (run && run.settled) out.push(h(Box, { key: 'run', paddingLeft: 4, width: cols }, h(WText, { color: c('prose') }, ...runs(paintLine(toneText(run.settled, run.settledTone), run.settledTone)))));
|
|
694
|
+
else if (run) out.push(h(Box, { key: 'run' }, h(NeedsYouCard, { // QUOXON-ASK: a run request is the approval card too
|
|
695
|
+
cols, title: `⚠ ${run.who} asks you to run this here`, rows: run.why ? [['why', run.why]] : [], block: codeRows([run.cmd], cols - 4),
|
|
696
|
+
notes: run.runnable ? [] : [`✖ Not offered: ${run.refuse}. Copy it yourself only if you trust it.`],
|
|
697
|
+
footer: run.danger ? `▶ ${run.danger}. Type run to run it, n to skip? run · n no · c chat` : '▶ Run it? y yes · n no · c chat',
|
|
698
|
+
arming: run.runnable ? null : h(Text, null, '') })));
|
|
699
|
+
if (l.file && !l.out) {
|
|
700
|
+
const lines = l.full ? String(l.file.body).split('\n') : l.file.preview;
|
|
701
|
+
lines.forEach((t, i) => out.push(h(Box, { key: `f${i}`, paddingLeft: 6, width: cols }, h(Text, { color: c('text') }, clip(t || ' ', cols - 8)))));
|
|
702
|
+
if (!l.full && l.file.lines > lines.length) out.push(h(Box, { key: 'more', paddingLeft: 6 }, h(Text, { color: c('prose') }, `… ${l.file.lines - lines.length} more line${l.file.lines - lines.length === 1 ? '' : 's'}: `), h(Text, { color: c('command') }, '/slot log')));
|
|
703
|
+
else if (!l.full) out.push(h(Box, { key: 'more', paddingLeft: 6 }, h(Text, { color: c('prose') }, 'full file: '), h(Text, { color: c('command') }, '/slot log')));
|
|
704
|
+
}
|
|
705
|
+
return h(Box, { flexDirection: 'column' }, ...out);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export const TIP_HINT = `${cmd('/tip')} for another · ${cmd('/tip-settings off')} to stop`;
|
|
709
|
+
/** Word-wrap plain text to `width` columns (a word longer than the width is hard-split). */
|
|
710
|
+
function wrapPlain(text, width) {
|
|
711
|
+
const out = []; let line = '';
|
|
712
|
+
for (const w0 of String(text ?? '').split(/\s+/).filter(Boolean)) {
|
|
713
|
+
let w = w0;
|
|
714
|
+
while ([...w].length > width) { if (line) { out.push(line); line = ''; } out.push([...w].slice(0, width).join('')); w = [...w].slice(width).join(''); }
|
|
715
|
+
if (line && [...line].length + 1 + [...w].length > width) { out.push(line); line = w; } else line = line ? `${line} ${w}` : w;
|
|
716
|
+
}
|
|
717
|
+
if (line) out.push(line);
|
|
718
|
+
return out.length ? out : [''];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* A tip in a rounded brand-accent frame: width min(cols - 4, 76), 1-col padding, wrapped text, "Try:" on its own
|
|
723
|
+
* line in the command colour, the /tip hint as the last line inside. Under 40 cols or NO_COLOR: a plain "Tip:" block.
|
|
724
|
+
*/
|
|
725
|
+
function TipView({ item, cols }) {
|
|
726
|
+
if (mono() || cols < 40) {
|
|
727
|
+
const w = Math.max(10, cols - 2);
|
|
728
|
+
const lines = [...wrapPlain(`Tip: ${item.text}`, w), `Try: ${item.cmd}`, ...wrapPlain(stripMarks(TIP_HINT), w)];
|
|
729
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...lines.map((l, i) => h(Text, { key: i }, l)));
|
|
730
|
+
}
|
|
731
|
+
const W = Math.min(cols - 4, 76), inner = W - 4, edge = { color: c('brand') };
|
|
732
|
+
const row = (key, kids, len) => h(Box, { key }, h(Text, edge, '│ '), h(Text, null, ...kids), h(Text, null, ' '.repeat(Math.max(0, inner - len))), h(Text, edge, ' │'));
|
|
733
|
+
const body = wrapPlain(item.text, inner).map((l, i) => row(`t${i}`, [h(Text, { key: 0, color: c('prose') }, l)], [...l].length));
|
|
734
|
+
const cmdText = clip(item.cmd, inner - 5);
|
|
735
|
+
const tryRow = row('try', [h(Text, { key: 0, color: c('prose'), bold: true }, 'Try: '), h(Text, { key: 1, color: c('command') }, cmdText)], 5 + [...cmdText].length);
|
|
736
|
+
const hintRow = row('hint', runs(paintRuns(TIP_HINT, 'quiet')), [...stripMarks(TIP_HINT)].length);
|
|
737
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column' },
|
|
738
|
+
h(Text, edge, '╭─ ', h(Text, { bold: true }, 'Tip'), ' ', '─'.repeat(W - 8), '╮'),
|
|
739
|
+
...body, tryRow, hintRow,
|
|
740
|
+
h(Text, edge, '╰', '─'.repeat(W - 2), '╯'));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* The startup "What's new since you last ran Quox" box: same frame as a tip, one title row (+ a "Try:" row) per entry,
|
|
745
|
+
* then ALWAYS a "Try: /whatsnew" footer ("+N more. Try: /whatsnew" when more are unseen), so the way to the full list
|
|
746
|
+
* never depends on which entries happen to be newest. The compact head variant (item.head, the new-commands line)
|
|
747
|
+
* already ends "/whatsnew for more" and gets no footer. Under 40 cols or NO_COLOR: plain lines, no frame.
|
|
748
|
+
*/
|
|
749
|
+
function WhatsNewView({ item, cols }) {
|
|
750
|
+
const HEAD = item.head || "What's new since you last ran Quox";
|
|
751
|
+
const lead = item.more > 0 ? `+${item.more} more. ` : '';
|
|
752
|
+
const more = item.head ? '' : `${lead}Try: /whatsnew`;
|
|
753
|
+
if (mono() || cols < 40) {
|
|
754
|
+
const w = Math.max(10, cols - 2);
|
|
755
|
+
const lines = [HEAD, ...item.items.flatMap(it => [`- ${it.title}`, ...(it.cmd ? [` Try: ${it.cmd}`] : [])]), ...(more ? [more] : [])].flatMap(l => wrapPlain(l, w));
|
|
756
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...lines.map((l, i) => h(Text, { key: i }, l)));
|
|
757
|
+
}
|
|
758
|
+
const W = Math.min(cols - 4, 76), inner = W - 4, edge = { color: c('brand') };
|
|
759
|
+
const row = (key, kids, len) => h(Box, { key }, h(Text, edge, '│ '), h(Text, null, ...kids), h(Text, null, ' '.repeat(Math.max(0, inner - len))), h(Text, edge, ' │'));
|
|
760
|
+
const rows = item.items.flatMap((it, i) => {
|
|
761
|
+
const t = clip(it.title, inner - 2);
|
|
762
|
+
const out = [row(`t${i}`, [h(Text, { key: 0, color: c('prose'), bold: true }, `• ${t}`)], 2 + [...t].length)];
|
|
763
|
+
if (it.cmd) { const k = clip(it.cmd, inner - 7); out.push(row(`c${i}`, [h(Text, { key: 0, color: c('prose') }, ' Try: '), h(Text, { key: 1, color: c('command') }, k)], 7 + [...k].length)); }
|
|
764
|
+
return out;
|
|
765
|
+
});
|
|
766
|
+
if (more) rows.push(row('more', [h(Text, { key: 0, color: c('prose') }, lead), h(Text, { key: 1, color: c('prose'), bold: true }, 'Try: '), h(Text, { key: 2, color: c('command') }, '/whatsnew')], [...more].length));
|
|
767
|
+
if (item.pad) { rows.unshift(row('p0', [], 0)); rows.push(row('p1', [], 0)); }
|
|
768
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column' },
|
|
769
|
+
h(Text, edge, '╭─ ', h(Text, { bold: true }, HEAD), ' ', '─'.repeat(Math.max(1, W - 5 - HEAD.length)), '╮'),
|
|
770
|
+
...rows,
|
|
771
|
+
h(Text, edge, '╰', '─'.repeat(W - 2), '╯'));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* QUOXON-BROADCAST: the sender's delivery card. Rounded frame in the card's tone (green all in, gold pending, red a
|
|
776
|
+
* real failure), one row per fact, the id and the status hint in the bottom edge. Widths are measured in cells so the
|
|
777
|
+
* emoji does not skew the edge. Under 50 cols: the model's one-line form.
|
|
778
|
+
*/
|
|
779
|
+
function BroadcastCardView({ item, cols }) {
|
|
780
|
+
const m = item.card;
|
|
781
|
+
if (cols < 50) return h(Box, { paddingLeft: 1, width: cols }, h(WText, { color: c('prose') }, m.narrow));
|
|
782
|
+
const W = Math.min(cols - 4, 76), inner = W - 4, edge = { color: c(m.tone) };
|
|
783
|
+
const cut = (t, room) => { let o = ''; for (const ch of [...t]) { if (stringWidth(o + ch) > room - 1) return o + '…'; o += ch; } return o; };
|
|
784
|
+
const fit = (t, room) => (stringWidth(t) > room ? cut(t, room) : t);
|
|
785
|
+
const rows = m.rows.map((segs, i) => {
|
|
786
|
+
let left = inner;
|
|
787
|
+
const kids = segs.map(([t, role], k) => { const x = fit(t, left); left -= stringWidth(x); return h(Text, { key: k, color: c(role) }, x); });
|
|
788
|
+
return h(Box, { key: i }, h(Text, edge, '│ '), h(Text, null, ...kids), h(Text, null, ' '.repeat(Math.max(0, left))), h(Text, edge, ' │'));
|
|
789
|
+
});
|
|
790
|
+
const title = fit(m.title, W - 5);
|
|
791
|
+
const foot = fit(m.foot, W - 5);
|
|
792
|
+
return h(Box, { paddingLeft: 2, flexDirection: 'column' },
|
|
793
|
+
h(Text, edge, '╭─ ', h(Text, { bold: true, color: c('prose') }, title), ' ', '─'.repeat(Math.max(1, W - 5 - stringWidth(title))), '╮'),
|
|
794
|
+
...rows,
|
|
795
|
+
h(Text, edge, '╰─ ', h(Text, { color: c('quiet') }, foot), ' ', '─'.repeat(Math.max(1, W - 5 - stringWidth(foot))), '╯'));
|
|
421
796
|
}
|
|
422
797
|
|
|
423
798
|
/** Rows a live block takes as HistoryItem renders it (sizes the live region; keep in step with HistoryItem). */
|
|
@@ -425,8 +800,12 @@ export function liveRows(blocks, cols) {
|
|
|
425
800
|
return blocks.reduce((n, b) => {
|
|
426
801
|
switch (b.kind) {
|
|
427
802
|
case 'text': return n + 1 + markdownRows(b.text, cols - 2, true);
|
|
428
|
-
case 'note': return n + noteRows(b.text, cols);
|
|
803
|
+
case 'note': return n + (b.tone === 'ask' ? askCardRows(b.text, cols) : b.tone === 'aside' ? asideRows(b.text, cols) : noteRows(b.text, cols, b.tone));
|
|
429
804
|
case 'summary': return n + noteLines(b.text, cols - 4).length;
|
|
805
|
+
case 'error': return n + errorRows(b.text, cols);
|
|
806
|
+
case 'slotmsg': return n + (b.line?.run && !b.line.run.settled ? 9 + Math.ceil(String(b.line.run.cmd || '').length / Math.max(10, cols - 8)) : 1); // ASK-SETTLE: an armed run card is held live
|
|
807
|
+
case 'tool': return n + 1 + (b.status === 'running' ? 0 : toolCardLines(b, cols).length);
|
|
808
|
+
case 'loopcard': return n + (loopPinned(b.card) ? 0 : 1 + loopCardLines(b.card, cols).length);
|
|
430
809
|
default: return n + 1;
|
|
431
810
|
}
|
|
432
811
|
}, 0);
|
|
@@ -442,11 +821,54 @@ export const Grad = ({ text }) => {
|
|
|
442
821
|
}));
|
|
443
822
|
};
|
|
444
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Pure: the model as the composer's bottom-rule label (`──── anthropic claude-sonnet-5-5 ─`). Degrades to fit,
|
|
826
|
+
* never wraps: full, then a shortened provider (`anth claude-sonnet-5-5`), then the model alone, then the
|
|
827
|
+
* model clipped. Leaves at least four rule cells on the left. Below 30 cols, or no model: ''.
|
|
828
|
+
*/
|
|
829
|
+
export function modelRuleLabel(cols, model, reserve = 0) {
|
|
830
|
+
const full = String(model || '').trim();
|
|
831
|
+
if (!full || cols < 30) return '';
|
|
832
|
+
const room = cols - 7 - reserve; // ' label ' + one trailing dash + four leading dashes (+ the approvals indicator)
|
|
833
|
+
if (reserve && room < 6) return '';
|
|
834
|
+
const fits = (t) => cellWidth(t) <= room; // cells under the ambiguous-width policy (cells.js)
|
|
835
|
+
if (fits(full)) return full;
|
|
836
|
+
const segs = full.split(' · ');
|
|
837
|
+
const tailSeg = segs[segs.length - 1];
|
|
838
|
+
const words = (segs.length > 1 ? segs[0] : full).split(' ');
|
|
839
|
+
const name = segs.length > 1 ? tailSeg : words.slice(1).join(' ') || words[0];
|
|
840
|
+
const provider = segs.length > 1 ? segs[0] : words.length > 1 ? words[0] : '';
|
|
841
|
+
if (provider) { const short = `${provider.slice(0, 4)} ${name}`; if (fits(short)) return short; }
|
|
842
|
+
if (fits(name)) return name;
|
|
843
|
+
return reserve ? '' : clip(name, room); // with the approvals indicator the model is dropped before the indicator is cut
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Pure: the permanent approvals indicator on the bottom rule. `a` = { count, error, signedOut }.
|
|
848
|
+
* Never a false 0: signed out, an unreachable inbox, or a count not read yet is `approvals ?` (warn).
|
|
849
|
+
*/
|
|
850
|
+
export function approvalsIndicator(a) {
|
|
851
|
+
if (!a) return null;
|
|
852
|
+
const n = a.count;
|
|
853
|
+
if (a.signedOut || a.error || typeof n !== 'number') return { text: 'approvals ?', role: 'warn' };
|
|
854
|
+
if (n === 0) return { text: '✔ nothing waiting', role: 'ok' };
|
|
855
|
+
return { text: `⏸ ${n} approval${n === 1 ? '' : 's'} waiting`, role: 'warn' };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Pure: the labels for this width. The indicator sits top-left, the model bottom-right: independent of each other. */
|
|
859
|
+
export function barLabels(cols, model, a) {
|
|
860
|
+
const ind = cols >= 30 ? approvalsIndicator(a) : null;
|
|
861
|
+
if (ind && cols - cellWidth(` ${ind.text} `) - 5 < 4) return { model: modelRuleLabel(cols, model), ind: null }; // no room for the whole indicator
|
|
862
|
+
return { model: modelRuleLabel(cols, model), ind };
|
|
863
|
+
}
|
|
864
|
+
|
|
445
865
|
/** Pure: the rule split around a right-aligned session name: `───── name ─`. Below 30 cols, or no name: plain. */
|
|
446
866
|
export function ruleParts(cols, name) {
|
|
447
|
-
|
|
867
|
+
// TUI-FOOTER: '─' is East Asian Ambiguous; where the terminal draws it two cells wide, half as many fill the row
|
|
868
|
+
const d = cellWidth('─');
|
|
869
|
+
if (!name || cols < 30) return { left: '─'.repeat(Math.floor(cols / d)), label: '', right: '' };
|
|
448
870
|
const label = ` ${clip(name, cols - 8)} `;
|
|
449
|
-
return { left: '─'.repeat(Math.max(1, cols -
|
|
871
|
+
return { left: '─'.repeat(Math.max(1, Math.floor((cols - cellWidth(label) - d) / d))), label, right: '─' };
|
|
450
872
|
}
|
|
451
873
|
|
|
452
874
|
/**
|
|
@@ -470,26 +892,46 @@ function ringString(str, rgbs, from = 0) {
|
|
|
470
892
|
* A composer rule. `ring` (motion only, never under NO_COLOR) = { state, top }: the rule becomes half of the
|
|
471
893
|
* light ring (motion/ring.js); the session name stays bright on top of it.
|
|
472
894
|
*/
|
|
473
|
-
export const Rule = ({ cols, color, name, ring = null }) => {
|
|
895
|
+
export const Rule = ({ cols, color, name, ring = null, labelColor = null, extra = null }) => {
|
|
474
896
|
const p = ruleParts(cols, name);
|
|
897
|
+
// `labelColor` (the model on the bottom rule): its own colour, not bold, so it reads as context, not a title
|
|
898
|
+
const lab = labelColor ? { color: labelColor } : null;
|
|
899
|
+
if (extra) { // TUI-BAR: the approvals indicator at the LEFT, the session name (if any) at the RIGHT: `─ ✔ nothing waiting ──── name ─`
|
|
900
|
+
const d = cellWidth('─');
|
|
901
|
+
const el = ` ${extra.text} `;
|
|
902
|
+
const nm = name ? ` ${name} ` : '';
|
|
903
|
+
const fill = '─'.repeat(Math.max(1, Math.floor((cols - cellWidth(nm) - cellWidth(el) - 2 * d) / d)));
|
|
904
|
+
const ex = { color: c(extra.role), bold: true };
|
|
905
|
+
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
906
|
+
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
907
|
+
const fl = [...fill].length, ln = [...el].length;
|
|
908
|
+
return h(Text, null, ringString('─', rgbs), h(Text, ex, el), ringString(fill, rgbs, 1 + ln), nm ? h(Text, lab || { color: c('violet'), bold: true }, nm) : null, ringString('─', rgbs, 1 + ln + fl + [...nm].length));
|
|
909
|
+
}
|
|
910
|
+
return h(Text, null, h(Text, { color: c(color) }, '─'), h(Text, ex, el), h(Text, { color: c(color) }, fill), nm ? h(Text, lab || { color: c('violet'), bold: true }, nm) : null, h(Text, { color: c(color) }, '─'));
|
|
911
|
+
}
|
|
475
912
|
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
476
913
|
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
477
914
|
if (!p.label) return h(Text, null, ringString(p.left, rgbs));
|
|
478
915
|
const ls = [...p.left].length, lb = [...p.label].length;
|
|
479
|
-
return h(Text, null, ringString(p.left, rgbs), h(Text, { color: c('violet'), bold: true }, p.label), ringString(p.right, rgbs, ls + lb));
|
|
916
|
+
return h(Text, null, ringString(p.left, rgbs), h(Text, lab || { color: c('violet'), bold: true }, p.label), ringString(p.right, rgbs, ls + lb));
|
|
480
917
|
}
|
|
481
918
|
if (!p.label) return h(Text, { color: c(color) }, p.left);
|
|
482
|
-
return h(Text, null, h(Text, { color: c(color) }, p.left), h(Text, { color: c('label'), bold: true }, p.label), h(Text, { color: c(color) }, p.right));
|
|
919
|
+
return h(Text, null, h(Text, { color: c(color) }, p.left), h(Text, lab || { color: c('label'), bold: true }, p.label), h(Text, { color: c(color) }, p.right));
|
|
483
920
|
};
|
|
484
921
|
|
|
485
922
|
/** Split the buffer into visible lines with an inverse cursor cell. */
|
|
486
923
|
export function composerLines(buf, maxLines = 8) {
|
|
487
924
|
const lines = buf.text.split('\n');
|
|
488
925
|
const { line: cl, col } = lineCol(buf.text, buf.cursor);
|
|
489
|
-
|
|
490
|
-
|
|
926
|
+
const from = composerFrom(buf, maxLines);
|
|
927
|
+
const sel = selection(buf);
|
|
928
|
+
const starts = []; { let o = 0; for (const l of lines) { starts.push(o); o += l.length + 1; } }
|
|
491
929
|
return lines.slice(from, from + maxLines).map((text, i) => {
|
|
492
930
|
const idx = from + i;
|
|
931
|
+
if (sel) { // QUOXON-MOUSE: the selected span is drawn inverse; no cursor cell while a selection shows
|
|
932
|
+
const a = Math.max(0, Math.min(text.length, sel.from - starts[idx])); const b = Math.max(0, Math.min(text.length, sel.to - starts[idx]));
|
|
933
|
+
return a < b ? { key: idx, pre: text.slice(0, a), sel: text.slice(a, b), post: text.slice(b) } : { text, cursor: null, key: idx };
|
|
934
|
+
}
|
|
493
935
|
if (idx !== cl) return { text, cursor: null, key: idx };
|
|
494
936
|
const gs = graphemes(text);
|
|
495
937
|
return { key: idx, before: gs.slice(0, col).map(g => g.text).join(''), at: gs[col] ? gs[col].text : ' ', after: gs.slice(col + 1).map(g => g.text).join('') };
|
|
@@ -503,24 +945,50 @@ export function composerLines(buf, maxLines = 8) {
|
|
|
503
945
|
export function RingComposer({ ring: ringIn, ...props }) {
|
|
504
946
|
const now = useFrame();
|
|
505
947
|
const ring = ringIn ? (ringIn.ref.current = ringStep(ringIn.ref.current, { active: ringIn.active, mode: ringIn.mode, now })) : null;
|
|
506
|
-
return h(Composer, { ...props, ring });
|
|
948
|
+
return h(Composer, { ...props, ring, now });
|
|
507
949
|
}
|
|
508
950
|
|
|
509
951
|
/** `ring` = a ring state from motion/ring.js, or null for the static rules. Hook-free (pure render). */
|
|
510
|
-
|
|
952
|
+
// TUI-DIVIDER: `divider` = { d, slow } (the AOCL strip state and the per-turn slow-redraw ref), drawn only on the
|
|
953
|
+
// ring path (motion + colour), so NO_COLOR, reduced motion and the lite TUI keep the plain rule.
|
|
954
|
+
/** Does the block directly above the composer close with a rule? Then no spacer and no second rule (one rule only). */
|
|
955
|
+
export function endsInRule({ card = null, asks = [], background = 0, pending = [], steering = [], countdown = false }) {
|
|
956
|
+
if (background > 0 || pending.length || steering.length || countdown) return false;
|
|
957
|
+
if (asks.length) return !askCardModel(asks[asks.length - 1].text).plain && askCardRows(asks[asks.length - 1].text, 80) > 0;
|
|
958
|
+
return Boolean(card);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function Composer({ buf, cols, mode, busy, placeholder, name, model = null, ring = null, divider = null, now = 0, topRule = true, approvals = null }) {
|
|
962
|
+
const bar = barLabels(cols, model, approvals);
|
|
963
|
+
// TUI-BAR (owner 2026-10-23): the indicator once, top-left; the session name top-right (it drops before the indicator
|
|
964
|
+
// is cut); the model bottom-right. The top rule is drawn whenever the indicator is, even above a card's closing rule.
|
|
965
|
+
const topExtra = bar.ind;
|
|
966
|
+
const nameRoom = topExtra ? cols - 4 - cellWidth(` ${topExtra.text} `) - 4 : cols - 8;
|
|
967
|
+
const topName = name && topExtra ? (nameRoom >= 6 ? clip(name, nameRoom) : '') : name;
|
|
511
968
|
const empty = buf.text === '';
|
|
512
969
|
const lines = composerLines(buf);
|
|
513
970
|
const body = empty
|
|
514
971
|
? [h(Box, { key: 'p' }, h(Text, { color: c('sky'), bold: true }, '› '), h(Text, { inverse: true }, ' '), h(Text, { color: c('placeholder') }, clip(placeholder, cols - 5)))]
|
|
515
972
|
: lines.map((l, i) => h(Box, { key: l.key },
|
|
516
|
-
h(Text, { color: c('sky'), bold: true }, i === 0 ? '› ' : ' '),
|
|
517
|
-
l.
|
|
973
|
+
h(Box, { flexShrink: 0 }, h(Text, { color: c('sky'), bold: true }, i === 0 ? '› ' : ' ')), // COMPOSER-EDIT: never squeezed, so wrapped rows are cols - 2 wide
|
|
974
|
+
l.sel !== undefined
|
|
975
|
+
? h(Text, { color: c('input') }, l.pre, h(Text, { inverse: true }, l.sel), l.post)
|
|
976
|
+
: l.at === undefined
|
|
518
977
|
? h(Text, { color: c('input') }, l.text || ' ')
|
|
519
978
|
: h(Text, { color: c('input') }, l.before, h(Text, { inverse: true }, l.at), l.after)));
|
|
520
979
|
return h(Box, { flexDirection: 'column', width: cols },
|
|
521
|
-
h(Rule, { cols, color: MODE_COLOR[mode] || 'sky', name, ring: ring ? { state: ring, top: true } : null }),
|
|
980
|
+
(topRule || topExtra) ? h(Rule, { cols, color: MODE_COLOR[mode] || 'sky', name: topName, extra: topExtra, labelColor: topExtra ? c('violet') : null, ring: ring ? { state: ring, top: true } : null }) : null,
|
|
522
981
|
...body,
|
|
523
|
-
|
|
982
|
+
(ring && divider && dividerRuleEl({ cols, label: bar.model, extra: null, divider: divider.d, now, ruleColor: c('dim'), labelColor: modelColor(model), mono: mono(), reduced: cellWidth('─') !== 1, slow: divider.slow }))
|
|
983
|
+
|| h(Rule, { cols, color: 'dim', name: bar.model, labelColor: modelColor(model), ring: ring ? { state: ring, top: false } : null }));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** The model label's colour: its provider's brand colour, else the label role (never a grey). */
|
|
987
|
+
function modelColor(model) {
|
|
988
|
+
if (mono()) return undefined;
|
|
989
|
+
const [prov, ...rest] = String(model || '').split(' ');
|
|
990
|
+
const pc = providerOf(prov, rest.join(' ')) ? providerColor(prov, rest.join(' ')) : null;
|
|
991
|
+
return pc || c('label');
|
|
524
992
|
}
|
|
525
993
|
|
|
526
994
|
export function Pending({ pending, steering, cols }) {
|
|
@@ -541,81 +1009,227 @@ function DrawRule({ cols, color, e, dur = 0.25 }) {
|
|
|
541
1009
|
|
|
542
1010
|
export const ARM_BAR = 6;
|
|
543
1011
|
/** `motionE` = seconds since the card appeared, when motion is on: the rule draws in, the title decodes, the arming bar fills. */
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1012
|
+
/** Approvals raised by ANOTHER session: a notice line only. It never takes focus or keys; answer through /approvals. */
|
|
1013
|
+
export function WaitingNotice({ items = [], cols }) {
|
|
1014
|
+
if (!items.length) return null;
|
|
1015
|
+
const from = [...new Set(items.map(i => i.origin || 'another session'))].slice(0, 2).join(', ');
|
|
1016
|
+
const n = items.length;
|
|
1017
|
+
return h(Text, { color: c('ask'), bold: true }, ' ' + clip(`▶ ${n} approval${n === 1 ? '' : 's'} waiting from ${from}${items.some(i => i.privileged) ? ' (privileged)' : ''}: /approvals`, cols - 2));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* QUOXON-ASK (owner 2026-10-16 "every ask should look like an approval card"): THE card frame for anything that needs
|
|
1022
|
+
* the human. Approval cards and every other ask render through this one function, so they cannot drift apart:
|
|
1023
|
+
* a rule, a bold title saying who is asking, label/value rows (what a yes does, in full, and why), then the
|
|
1024
|
+
* "▶ question? keys" footer in the ask colour, and a closing rule. `arming` replaces the footer until keys are live.
|
|
1025
|
+
*/
|
|
1026
|
+
/** The "▶ question? keys" line: the glyph keeps its own 2 cells and a wrapped question hangs under its text, not under the glyph. */
|
|
1027
|
+
const HANG_ASK = /^▶ /;
|
|
1028
|
+
export function AskFoot({ footer }) {
|
|
1029
|
+
const f = String(footer ?? '');
|
|
1030
|
+
if (!HANG_ASK.test(f)) return h(Text, { color: c('prose'), bold: true, wrap: 'wrap' }, ' ', ...runs(paintLine(f, 'ask')));
|
|
1031
|
+
return h(Box, { paddingLeft: 2 }, h(Box, { width: 2, flexShrink: 0 }, h(Text, { bold: true }, ...runs(paintLine('▶ ', 'ask')))), h(Box, { flexGrow: 1, flexShrink: 1 }, h(Text, { color: c('prose'), bold: true, wrap: 'wrap' }, ...runs(paintLine(f.slice(2), 'ask')))));
|
|
1032
|
+
}
|
|
1033
|
+
const footRows = (footer, cols) => (HANG_ASK.test(footer) ? wrapRows(footer.slice(2), cols - 4) : wrapRows(footer, cols - 2));
|
|
1034
|
+
export function NeedsYouCard({ cols, title, rows = [], block = [], notes = [], footer, arming = null, motionE = null }) {
|
|
1035
|
+
const e = motionE;
|
|
1036
|
+
return h(Box, { flexDirection: 'column', width: cols, marginTop: 1 },
|
|
1037
|
+
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }),
|
|
1038
|
+
e == null ? h(Text, { color: c('warn'), bold: true, wrap: 'wrap' }, title) : h(Text, null, ...fxNodes([{ t: title, color: 'warn', bold: true }], { e: e - 0.1, timing: TIMING.title }, 23)),
|
|
1039
|
+
// APPROVAL-WHAT: wrap at word boundaries under the label instead of cutting the action or target off
|
|
1040
|
+
...rows.flatMap(([k, v], ri) => wrapPlain(String(v), Math.max(10, cols - 10)).slice(0, 8).map((line, i) => h(Text, { key: `${ri}${k}${i}`, color: c('text') }, ' ', h(Text, { color: c('label') }, (i ? '' : k).padEnd(7)), h(Text, { color: c(k === 'risk' ? 'warn' : 'value') }, line)))),
|
|
1041
|
+
// block: pre-shaped segment rows shown verbatim (a command to run: never word-wrapped, never clipped)
|
|
1042
|
+
...block.map((segs, i) => h(Box, { key: `b${i}`, paddingLeft: 2, width: cols }, h(Text, null, ...segs.map((sg, j) => h(Text, { key: j, color: c(sg.role || 'text'), bold: sg.bold || undefined }, sg.t))))),
|
|
1043
|
+
...notes.map((n, i) => h(Text, { key: `n${i}`, color: c('warn'), bold: true, wrap: 'wrap' }, ` ${n}`)),
|
|
1044
|
+
arming || h(AskFoot, { footer }),
|
|
1045
|
+
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** The card's fields for an ask line (from askText): who asks, what a yes does, why, and the footer. */
|
|
1049
|
+
export function askCardModel(text) {
|
|
1050
|
+
const a = parseAsk(text);
|
|
1051
|
+
const rows = [['do', a.action], ['why', a.reason]].filter(r => r[1]);
|
|
1052
|
+
if (a.plain) return { plain: true, title: '', rows: [], footer: String(text).trim() }; // one quiet line, no box (the walkthrough's prompt)
|
|
1053
|
+
return { title: a.title ? `⚠ ${a.title}` : `⚠ ${a.who || 'This terminal'} asks: ${a.question}`, rows, footer: `▶ ${a.question} ${keysLabel(a.keys)}`.trim() };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Any ask (y/N/c and friends) as the approval card frame. `text` is the askText line. */
|
|
1057
|
+
export function AskCard({ text, cols }) {
|
|
1058
|
+
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return null; // an empty ask draws nothing: no rules, no blank line
|
|
1059
|
+
const m = askCardModel(text);
|
|
1060
|
+
if (m.plain) return h(Box, { width: cols, marginTop: 1, flexDirection: 'column' }, h(AskFoot, { footer: m.footer }));
|
|
1061
|
+
return h(NeedsYouCard, { cols, title: m.title, rows: m.rows, footer: m.footer });
|
|
1062
|
+
}
|
|
1063
|
+
/** Rows an AskCard takes (sizes the live region). */
|
|
1064
|
+
export const askCardRows = (text, cols) => {
|
|
1065
|
+
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return 0;
|
|
1066
|
+
const m = askCardModel(text);
|
|
1067
|
+
if (m.plain) return 1 + footRows(m.footer, cols);
|
|
1068
|
+
return 4 + wrapRows(m.title, cols) + m.rows.reduce((n, [, v]) => n + Math.min(8, wrapPlain(String(v), Math.max(10, cols - 10)).length), 0) + footRows(m.footer, cols);
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
export function ApprovalCard({ card, org, cols, armed, draftHint = null, motionE = null, confirm = false, position = null }) {
|
|
1072
|
+
const when = card.askedAt ? String(card.askedAt).replace('T', ' ').slice(0, 16) + ' UTC' : null;
|
|
1073
|
+
const rows = card.undescribable ? [['what', card.what], ['from', card.askedBy || card.origin], ['asked', when], ['org', org]].filter(r => r[1]) : [
|
|
1074
|
+
['what', card.what && card.what !== card.tool ? card.what : null], ['tool', card.tool], ['target', card.target], ['scope', card.scope], ['risk', card.risk], ['from', card.foreign ? card.origin : null], ['org', card.plan && (card.plan.orgName || card.plan.orgId) ? orgLabel(card.plan.orgName, card.plan.orgId) : org]
|
|
547
1075
|
].filter(r => r[1]);
|
|
1076
|
+
if (card.plan) { // the plan itself, so the approver sees what they are approving
|
|
1077
|
+
if (card.plan.lines.length) card.plan.lines.forEach((l, i) => rows.push([i ? '' : 'plan', l]));
|
|
1078
|
+
else rows.push(['plan', card.plan.loaded ? 'this objective has no steps recorded' : 'could not load the plan steps, deny or check `quox objective get` first']);
|
|
1079
|
+
}
|
|
1080
|
+
if (card.privileged) rows.push(['grants', `PRIVILEGED: ${[card.tool, card.target].filter(Boolean).join(' ')}`]);
|
|
1081
|
+
// APPROVAL-WWWH: what exactly (verbatim), where, who, why, when, read-only or write. The command is a block: never word-wrapped.
|
|
1082
|
+
const f = card.facts || null; const block = [];
|
|
1083
|
+
if (f && !card.undescribable) {
|
|
1084
|
+
if (f.where && /^\d+ (hosts|targets):/.test(f.where)) { for (const k of ['target', 'what']) { const i = rows.findIndex(r => r[0] === k && (k === 'target' || String(r[1]).includes(f.hostList[1]))); if (i >= 0) rows.splice(i, 1); } }
|
|
1085
|
+
if (f.command) { block.push([{ t: f.command.kind === 'input' ? 'input:' : 'command:', role: 'label' }]); block.push(...codeRows(f.command.shown.split('\n'), cols - 4)); }
|
|
1086
|
+
for (const r of [['where', f.where], ['who', f.who], ['why', f.why], ['asked', [f.asked && `"${f.asked}"`, f.when].filter(Boolean).join(' · ') || null], ['class', f.access]]) if (r[1]) rows.push(r);
|
|
1087
|
+
}
|
|
548
1088
|
const e = motionE;
|
|
549
|
-
const title =
|
|
1089
|
+
const title = `⚠ Approval needed${position && position.n > 1 ? ` (${position.i} of ${position.n})` : ''}: execution is held`;
|
|
550
1090
|
const filled = e == null ? 0 : Math.min(ARM_BAR, Math.floor(clamp01(e / 0.3) * ARM_BAR));
|
|
551
1091
|
const arming = e == null || mono()
|
|
552
1092
|
? h(Text, { color: c('dim') }, e == null ? ' arming…' : ` arming… ${'▰'.repeat(filled)}${'▱'.repeat(ARM_BAR - filled)}`)
|
|
553
1093
|
: h(Text, null, h(Text, { color: c('dim') }, ' arming… '), h(Text, { color: c('warn') }, '▰'.repeat(filled)), h(Text, { color: c('faint') }, '▱'.repeat(ARM_BAR - filled)));
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
draftHint === 'focus' ? h(Text, { color: c('warn') }, ' card focused (Esc returns to your draft)') : null,
|
|
561
|
-
!armed ? arming : h(Text, { color: c('muted') }, ...runs(paintLine(card.undescribable ? ' type the word approve to allow · d deny (no default)'
|
|
562
|
-
: ' a approve · d deny · o open in dashboard (no default)'))),
|
|
563
|
-
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }));
|
|
1094
|
+
const footer = card.privileged && confirm ? `▶ Grant ${[card.tool, card.target].filter(Boolean).join(' ')}? press y again to confirm, any other key cancels`
|
|
1095
|
+
: card.undescribable && confirm ? '▶ Quox could not describe this action. Approve it anyway? press y again to confirm, any other key cancels'
|
|
1096
|
+
: draftHint === 'focus' ? askText('Approve it?', { keys: 'y approve · n deny · card focused (Esc returns to your draft)' }) // RESTORE-TUI: every y/n goes through askText (same text)
|
|
1097
|
+
: draftHint === 'draft' ? askText('Approve it?', { keys: 'y/n answer once your draft is empty · Tab to answer the approval card (your draft is kept)' })
|
|
1098
|
+
: '▶ Approve it? y approve · n deny · c chat · Tab later · /approvals all';
|
|
1099
|
+
return h(NeedsYouCard, { cols, title, rows, block, notes: [...(card.undescribable && !confirm ? ['Quox could not describe this action'] : []), ...(position && position.others > 0 ? [`other sessions: ${position.others} waiting · /approvals`] : [])], footer, arming: armed ? null : arming, motionE: e });
|
|
564
1100
|
}
|
|
565
1101
|
|
|
566
|
-
/** Columns the status line's Q takes: 2 for the Braille glyph, 1 for the Ⓠ fallback. */
|
|
567
|
-
export const statusGlyphWidth = () => glyphStill('resting', { mono: mono(), mode: glyphMode(process.env) }).
|
|
1102
|
+
/** Columns the status line's Q takes: 2 for the Braille glyph, 1 for the Ⓠ fallback (2 where Ambiguous is wide). */
|
|
1103
|
+
export const statusGlyphWidth = () => cellWidth(glyphStill('resting', { mono: mono(), mode: glyphMode(process.env) }).text);
|
|
1104
|
+
|
|
1105
|
+
/** The composer mode in plain words on the status line's context row. */
|
|
1106
|
+
export const MODE_WORDS = { ask: 'ask first', auto: 'auto, acts without asking', plan: 'plan only' };
|
|
568
1107
|
|
|
569
1108
|
/**
|
|
570
|
-
*
|
|
571
|
-
*
|
|
1109
|
+
* The status as ONE continuous line that wraps (owner 2026-10-17 19:47: "should just make it one long line that wraps
|
|
1110
|
+
* to second or third line if needed ... so much space, looks wrong"; "should say agent: none, when nothing attached").
|
|
1111
|
+
* `● agent X · ⬢ slot S · ⬢ groups a 2, b · profile P · org O · mode ask first · rules · ↻ auto-restart`, wrapped
|
|
1112
|
+
* onto as few rows as it needs, evened out so no item sits alone, broken only between items, every further row at
|
|
1113
|
+
* the same left edge (never inside an item; a
|
|
1114
|
+
* single value too long for a row is split across rows). The key hints are separate: always their own row at that
|
|
1115
|
+
* same left edge, so nothing is right-aligned and no row carries a gap. The model sits on the composer's bottom rule
|
|
1116
|
+
* (modelRuleLabel). Widths are terminal cells under the ambiguous-width policy (cells.js).
|
|
1117
|
+
* Pure: rows of { text, segs, hint, gap, lead } where segs are [text, kind, extra].
|
|
572
1118
|
*/
|
|
573
|
-
export function
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1119
|
+
export function statusRows({ cols, slot, agent, groups = [], parts, hint, qw = statusGlyphWidth(), wide = ambiguousWide() }) {
|
|
1120
|
+
const len = (t) => cellWidth(t, { wide });
|
|
1121
|
+
const W = Math.max(12, cols - 1);
|
|
1122
|
+
const lead = 1 + qw + len(' ✦ '); // ' ' + Q + ' ✦ '
|
|
1123
|
+
const room = Math.max(4, W - lead);
|
|
1124
|
+
// a token: segs to draw, joined to the previous token on the same row by `pre`
|
|
1125
|
+
const tokens = [];
|
|
1126
|
+
tokens.push(agent
|
|
1127
|
+
? { pre: ' · ', segs: [['● ', 'agentDot', agent], ['agent ', 'label'], [agent, 'agent', agent]] }
|
|
1128
|
+
: { pre: ' · ', segs: [['○ ', 'icon'], ['agent: ', 'label'], ['none', 'value']] });
|
|
1129
|
+
tokens.push({ pre: ' · ', segs: slot ? [['⬢ ', 'icon'], ['slot ', 'label'], [slot, 'value']] : [['⬡ no slot', 'label']] });
|
|
1130
|
+
(groups || []).filter(g => g?.name).forEach((g, i) => tokens.push({ pre: i ? ', ' : ' · ',
|
|
1131
|
+
segs: [...(i ? [] : [['⬢ ', 'icon'], ['groups ', 'label']]), [g.name, 'value'], ...(g.unread ? [[' ' + g.unread, 'number']] : [])] }));
|
|
1132
|
+
const p = parts || {};
|
|
1133
|
+
// every context item says what it is (owner: "profile moo .. ask?? what?"); /help status explains each in a line
|
|
1134
|
+
const word = { profile: 'profile ', org: 'org ', mode: 'mode ' };
|
|
1135
|
+
const val = (k) => k === 'mode' ? (MODE_WORDS[p.mode] || null) : k === 'auto' ? '↻ auto-restart' : String(p[k]);
|
|
1136
|
+
for (const k of ['profile', 'org', 'server', 'remote', 'mode', 'rules', 'auto'].filter(k => p[k])) tokens.push({ pre: ' · ',
|
|
1137
|
+
segs: k === 'server' ? [['⬡ ', 'icon'], ['server ', 'label'], [p.server.name, 'value'], ...(p.server.role ? [[` (${p.server.role})`, 'label']] : [])]
|
|
1138
|
+
: k === 'remote' ? [['→ ', 'icon'], [String(p.remote), 'value']]
|
|
1139
|
+
: word[k] && val(k) ? [[word[k], 'label'], [val(k), k === 'mode' && p.mode === 'plan' ? 'violet' : 'value']] : [[k === 'mode' ? String(p[k]) : val(k), 'label']] });
|
|
1140
|
+
const tlen = (tk) => tk.segs.reduce((n, sg) => n + len(sg[0]), 0);
|
|
1141
|
+
// split a token too wide for a row into row-sized pieces (whole characters by cell width, never an ellipsis)
|
|
1142
|
+
const fit = (tk) => {
|
|
1143
|
+
if (tlen(tk) <= room) return [tk];
|
|
1144
|
+
const out = [];
|
|
1145
|
+
let segs = [], w = 0;
|
|
1146
|
+
const flush = () => { out.push({ pre: out.length ? '' : tk.pre, segs, forceBreak: out.length > 0 }); segs = []; w = 0; };
|
|
1147
|
+
for (const [t, k, x] of tk.segs) for (const ch of t) {
|
|
1148
|
+
const cw = len(ch);
|
|
1149
|
+
if (w + cw > room && segs.length) flush();
|
|
1150
|
+
const last = segs[segs.length - 1];
|
|
1151
|
+
if (last && last[1] === k) last[0] += ch; else segs.push([ch, k, x]);
|
|
1152
|
+
w += cw;
|
|
1153
|
+
}
|
|
1154
|
+
if (segs.length) flush();
|
|
1155
|
+
return out;
|
|
584
1156
|
};
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1157
|
+
// fill each row left to right; break only between items (or at a forced piece of an over-long one)
|
|
1158
|
+
const pack = (pieces, limit = room) => {
|
|
1159
|
+
const rows = [];
|
|
1160
|
+
let cur = null, w = 0;
|
|
1161
|
+
for (const tk of pieces) {
|
|
1162
|
+
const need = tlen(tk) + (cur && cur.length ? len(tk.pre) : 0);
|
|
1163
|
+
if (!cur || tk.forceBreak || w + need > limit) { cur = [{ ...tk, pre: '' }]; rows.push(cur); w = tlen(tk); continue; }
|
|
1164
|
+
cur.push(tk); w += need;
|
|
1165
|
+
}
|
|
1166
|
+
return rows;
|
|
1167
|
+
};
|
|
1168
|
+
// as few rows as filling gives, evened out (the narrowest limit that keeps that count): no item left alone on a row
|
|
1169
|
+
const balanced = (pieces) => {
|
|
1170
|
+
const n = pack(pieces).length;
|
|
1171
|
+
if (n < 2) return pack(pieces);
|
|
1172
|
+
let lo = Math.max(1, ...pieces.map(tlen)), hi = room;
|
|
1173
|
+
while (lo < hi) { const m = (lo + hi) >> 1; if (pack(pieces, m).length <= n) hi = m; else lo = m + 1; }
|
|
1174
|
+
return pack(pieces, lo);
|
|
1175
|
+
};
|
|
1176
|
+
const toSegs = (row) => row.flatMap(tk => [...(tk.pre ? [[tk.pre, 'sep']] : []), ...tk.segs]);
|
|
1177
|
+
const rowsOut = balanced(tokens.flatMap(fit)).map(row => ({ segs: toSegs(row), hint: '' }));
|
|
1178
|
+
const hintS = String(hint || '');
|
|
1179
|
+
if (hintS) for (const r of pack(hintS.split(' · ').map(t => ({ pre: ' · ', segs: [[t, 'hint']] })))) rowsOut.push({ segs: [], hint: toSegs(r).map(x => x[0]).join('') });
|
|
1180
|
+
const pad = ' '.repeat(lead);
|
|
1181
|
+
return rowsOut.map((r, i) => {
|
|
1182
|
+
const body = r.segs.map(x => x[0]).join('');
|
|
1183
|
+
const head = i === 0 ? ' ' + 'Ⓠ'.padEnd(qw, ' ') + ' ✦ ' : pad;
|
|
1184
|
+
return { segs: r.segs, hint: r.hint, gap: 0, lead, text: head + body + r.hint };
|
|
1185
|
+
});
|
|
588
1186
|
}
|
|
589
1187
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1188
|
+
/** QUOXON-FW17: custom rows, one truncated line each, just above the status line. */
|
|
1189
|
+
export function RowsBar({ rows, cols, limit }) {
|
|
1190
|
+
if (!rows?.length) return null;
|
|
1191
|
+
// TUI-QPE: layoutRows fits cells rows by keep (whole items drop, never mid-word), lines up a grid's columns and
|
|
1192
|
+
// leaves out rows below their min_cols; plain rows take the classic rowParts + clipParts path unchanged.
|
|
1193
|
+
// /qpe N keeps the first N lines of a grid; it applies to drawn lines (a row can bring several), so expand first.
|
|
1194
|
+
const laid = layoutRows(limit ? limitGrid(expandLines(rows), limit.grid, limit.n) : rows, cols);
|
|
1195
|
+
if (!laid.length) return null;
|
|
1196
|
+
return h(Box, { flexDirection: 'column', width: cols },
|
|
1197
|
+
...laid.map(({ st: r, parts }) =>
|
|
1198
|
+
// ROWS: label, value, tone and trend as their own runs; every role is a palette colour (no grey, owner rule).
|
|
1199
|
+
h(Text, { key: r.row.id, wrap: 'truncate-end' }, ' ', ...parts.map(([t, role, bold], i) => h(Text, { key: i, color: c(themeRole(role)), bold: Boolean(bold) }, t)))));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export function StatusLine({ cols, slot, agent, groups = [], parts, right }) {
|
|
1203
|
+
const p = Array.isArray(parts) ? { profile: parts[0], org: parts[1], model: parts[2], mode: parts[3], rules: parts[4], auto: parts[5], server: parts[6], remote: parts[7] } : parts;
|
|
1204
|
+
const qw = statusGlyphWidth();
|
|
1205
|
+
const rows = statusRows({ cols, slot, agent, groups, parts: p, hint: right, qw });
|
|
1206
|
+
// The still Q (qglyph.js): two Braille cells, or Ⓠ under NO_COLOR / no Braille. statusRows reserved its width.
|
|
597
1207
|
const q = glyphStill('resting', { mono: mono(), mode: glyphMode(process.env), depth: colorDepth(process.env) });
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1208
|
+
const colour = (k, x) => k === 'agentDot' || k === 'agent' ? agentColor(x) : c(k === 'sep' || k === 'hint' ? 'label' : k);
|
|
1209
|
+
return h(Box, { width: cols, flexDirection: 'column' }, ...rows.map((r, i) => h(Text, { key: 'r' + i, wrap: 'truncate-end' },
|
|
1210
|
+
i === 0 ? h(Text, null, ' ', h(Text, { bold: true }, ...cellNodes(q.cells, 'sq')), h(Text, { color: c('icon') }, ' ✦ ')) : ' '.repeat(r.lead),
|
|
1211
|
+
...r.segs.map(([t, k, x], j) => h(Text, { key: j, color: colour(k, x), bold: k === 'agent' || k === 'number' }, t)),
|
|
1212
|
+
r.hint ? h(Text, { color: c('label') }, ' '.repeat(r.gap), ...hintNodes(r.hint)) : null))); // no grey (owner rule)
|
|
603
1213
|
}
|
|
604
1214
|
|
|
605
|
-
/**
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
m[3] ? h(Text, { key: 'n', color: c('value') }, m[3]) : null, h(Text, { key: 'd', color: c('muted') }, ' · '),
|
|
612
|
-
h(Text, { key: 'a', color: agent ? agentColor(m[5]) : c('muted') }, m[4] + ' '), h(Text, { key: 'an', color: agent ? agentColor(m[5]) : c('muted'), bold: agent }, m[5])];
|
|
1215
|
+
/** `Enter send · /help`: each key in the key role, its words and the separators in label (never a grey). */
|
|
1216
|
+
function hintNodes(hint) {
|
|
1217
|
+
return hint.split(' · ').flatMap((part, i) => {
|
|
1218
|
+
const [k, ...rest] = part.split(' ');
|
|
1219
|
+
return [i ? h(Text, { key: 's' + i, color: c('label') }, ' · ') : null, h(Text, { key: 'k' + i, color: c('key') }, k), rest.length ? h(Text, { key: 'w' + i, color: c('label') }, ' ' + rest.join(' ')) : null];
|
|
1220
|
+
}).filter(Boolean);
|
|
613
1221
|
}
|
|
614
1222
|
|
|
615
1223
|
/** Live, actionable counts under the status line. Never a transcript message. */
|
|
616
|
-
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [] }) {
|
|
1224
|
+
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false, inbox = '' }) {
|
|
1225
|
+
const base = NotifyBarBase({ approvals, error, cols, signedOut, alerts, hideCount });
|
|
1226
|
+
if (!inbox || signedOut) return base;
|
|
1227
|
+
const line = h(Text, { key: 'inbox', color: c('ask'), bold: true }, ' ' + clip(inbox, cols - 3)); // HITL-INBOX-BAR: 📨 N new · newest title
|
|
1228
|
+
return base ? h(Box, { flexDirection: 'column' }, base, line) : line;
|
|
1229
|
+
}
|
|
1230
|
+
function NotifyBarBase({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false }) {
|
|
617
1231
|
if (signedOut) return h(Text, { color: c('warn') }, ' ' + clip('not signed in: approvals unavailable until you sign in', cols - 2));
|
|
618
|
-
const count = approvals ? h(Text, { key: 'n', color: c('
|
|
1232
|
+
const count = approvals && !hideCount ? h(Text, { key: 'n', color: c('ask'), bold: true }, ' ' + clip(`▶ ${approvals} approval${approvals === 1 ? '' : 's'} waiting for you${error ? ' (last known)' : ''} · /approvals`, cols - 3)) : null;
|
|
619
1233
|
// Background workflows that keep failing: one deduped line each (max 2), never roster rows.
|
|
620
1234
|
const alertLines = alerts.slice(0, 2).map((t, i) => h(Text, { key: 'a' + i, color: c('fail') }, ' ' + clip(t, cols - 2)));
|
|
621
1235
|
if (alertLines.length) {
|
|
@@ -637,12 +1251,14 @@ export function Roster({ view, peek = [], confirm = null, hint = null, cols, fx
|
|
|
637
1251
|
// glyph keeps the state colour; the rest of the line steps down in tone with position and (for finished rows) age
|
|
638
1252
|
...view.lines.map((l, i) => {
|
|
639
1253
|
const e = fx && l.key != null ? fx(l.key) : null; // a row that just appeared decodes in (new rows land on top)
|
|
640
|
-
|
|
641
|
-
h(Text, { color: c(l.selected ? 'text' : ROSTER_TONE[l.state]) }, ' ' + l.marker + l.glyph),
|
|
642
|
-
e == null ? h(Text, { color: c(l.selected ? 'text' : l.tone) }, l.rest) : h(Text, null, ...fxNodes([{ t: l.rest, color: l.selected ? 'text' : l.tone }], { e, timing: TIMING.roster }, 29 + i))
|
|
1254
|
+
const row = h(Text, { key: 'r' + i, bold: l.selected, wrap: 'truncate-end' },
|
|
1255
|
+
h(Text, { color: c(l.selected ? 'text' : (l.glyphRole || ROSTER_TONE[l.state])) }, ' ' + l.marker + l.glyph),
|
|
1256
|
+
e == null ? h(Text, { color: c(l.selected ? 'text' : l.tone) }, l.rest) : h(Text, null, ...fxNodes([{ t: l.rest, color: l.selected ? 'text' : l.tone }], { e, timing: TIMING.roster }, 29 + i)),
|
|
1257
|
+
l.pulse ? h(Text, { color: c(l.pulseRole === 'dim' && l.selected ? 'text' : l.pulseRole) }, l.pulse) : null);
|
|
1258
|
+
return l.extra ? h(Box, { key: 'rx' + i, flexDirection: 'column' }, row, ...l.extra.map((t, j) => h(Text, { key: j, color: c('error') }, ' ' + t))) : row; // the stop reason, whole and wrapped
|
|
643
1259
|
}),
|
|
644
1260
|
view.more ? h(Text, { key: 'more', color: c('dim') }, ` +${view.more} more · /roster`) : null,
|
|
645
|
-
...peek.
|
|
1261
|
+
...peek.flatMap((p, i) => wrapText(p, cols - 5).map((t, j) => h(Text, { key: `p${i}.${j}`, color: c('dim') }, (j ? ' ' : ' ') + t))), // QLOOP-LIVE: a peek line wraps, it is never cut
|
|
646
1262
|
confirm ? h(Text, { key: 'cf', color: c('warn') }, ' ' + clip(confirm, cols - 2)) : null,
|
|
647
1263
|
hint ? h(Text, { key: 'h', color: c('dim') }, ' ' + clip(hint, cols - 2)) : null);
|
|
648
1264
|
}
|