quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a dev observer wired to this install? /observe and /note exist only for the developer whose
|
|
3
|
+
* window a QUOXON observer session watches; a customer has no observer, so for them these commands
|
|
4
|
+
* are hidden (help, palette, autocomplete) and refuse with a plain line.
|
|
5
|
+
*
|
|
6
|
+
* The switch is config driven, in the same style as the dev-transcripts flag: QUOX_DEV_OBSERVER=1
|
|
7
|
+
* turns it on for one shell, QUOX_DEV_OBSERVER=0 forces it off, otherwise the flag file
|
|
8
|
+
* ~/.config/quox/dev-observer decides (touch it on a dev box; it survives new shells).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const DEV_OBSERVER_REFUSAL = 'No dev observer is set up on this install, so there is nothing to send this to.';
|
|
15
|
+
|
|
16
|
+
export function devObserverOn(env = process.env, { home = homedir(), exists = existsSync } = {}) {
|
|
17
|
+
if (env.QUOX_DEV_OBSERVER === '0') return false;
|
|
18
|
+
if (env.QUOX_DEV_OBSERVER === '1') return true;
|
|
19
|
+
try { return exists(join(home, '.config', 'quox', 'dev-observer')); } catch { return false; }
|
|
20
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-DIVIDER: the composer's bottom rule (`──────── anthropic claude-sonnet-5-5 ─`) as a live AOCL strip.
|
|
3
|
+
*
|
|
4
|
+
* Owner design (2026-10-17 22:09, after the first cut): a PIPELINE, like the dashboard command screen's
|
|
5
|
+
* LayerStackMini. One compact chain of 11 dots, one per AOCL layer, each in its own layer colour, sits just left of
|
|
6
|
+
* the model label: `────────•─•─●─●─◆─•─•─•─•─•─• anthropic claude-sonnet-5-5 ─`. On Enter the dots light left to
|
|
7
|
+
* right as the real layer events arrive; when they reach the model, only the label animates (katakana shimmer).
|
|
8
|
+
* Motion lives in one place at a time (the newest dot, or the label), never across the row. The finished state is
|
|
9
|
+
* the label green (red on an error or a block, with the blocking dot marked) for ~1.6 s, then the normal rule.
|
|
10
|
+
*
|
|
11
|
+
* HONESTY: only REAL state moves this. A pip lights only when that layer's `aocl.layer` event arrives (or a
|
|
12
|
+
* pre-stream refusal's `aocl_layers` record), in the colour of its real result. A bypassed layer (not evaluated
|
|
13
|
+
* on this path) stays unlit, and so does every layer of a server that sends no `aocl.layer`. The "model is
|
|
14
|
+
* working" phase starts on a real signal: the last pre-model layer (L4) reported, or the first model output.
|
|
15
|
+
* Dots light one step apart for readability (STEP_MS), but never before their own event.
|
|
16
|
+
*
|
|
17
|
+
* Pure: `dividerStep` folds actions into state, `dividerFrame` draws one frame (or null = draw the normal rule).
|
|
18
|
+
* Nothing here runs a timer; the app asks the clock for frames only while `dividerLive` says so.
|
|
19
|
+
*/
|
|
20
|
+
import stringWidth from 'string-width';
|
|
21
|
+
import { wordFrame, wordColor } from './indicator.js';
|
|
22
|
+
import { LAYER_NAMES } from './aocl.js';
|
|
23
|
+
|
|
24
|
+
export const ORDER = ['L0', 'L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10'];
|
|
25
|
+
const PRE_LAST = 'L4';
|
|
26
|
+
export const GREEN_MS = 1600; // the label holds green this long after a completed reply (under the app's 2 s idle guarantee, motion-app.test)
|
|
27
|
+
export const RED_MS = 1600; // and red this long after an error or a block (the error itself stays in the transcript)
|
|
28
|
+
export const MIN_COLS = 70; // narrower than this: the plain rule, no animation
|
|
29
|
+
export const STEP_MS = 120; // dots light at most one per step, left to right (never before their event)
|
|
30
|
+
export const CURRENT_MS = 700; // the newest dot blinks this long after it lights; then it is just lit
|
|
31
|
+
export const TAG_MS = 1200; // the newest layer's name shows beside the chain this long
|
|
32
|
+
export const LIT_RESULTS = new Set(['pass', 'gated', 'blocked']);
|
|
33
|
+
|
|
34
|
+
// Real model work only. `thinking` frames are mostly server staging (routing, processing, provider notes) sent
|
|
35
|
+
// before turn.started; only the model's own reasoning counts.
|
|
36
|
+
const MODEL_SIGNALS = new Set(['chunk', 'text.delta', 'tool.started']);
|
|
37
|
+
const isModelSignal = (ev) => MODEL_SIGNALS.has(ev.type) || (ev.type === 'thinking' && ev.stage === 'model_reasoning');
|
|
38
|
+
|
|
39
|
+
/** A fresh strip for a turn the user just sent. */
|
|
40
|
+
export const dividerStart = (turnId, now) => ({ turnId, startedAt: now, pips: {}, seen: {}, order: [], modelAt: null, phase: 'gates', endedAt: null, blockedAt: null });
|
|
41
|
+
|
|
42
|
+
function light(d, l, now) {
|
|
43
|
+
const id = String(l?.id || '');
|
|
44
|
+
if (!ORDER.includes(id) || d.pips[id]) return d; // unknown layer or already lit: nothing changes
|
|
45
|
+
const result = String(l.result || '');
|
|
46
|
+
if (!LIT_RESULTS.has(result)) { // bypass: a real event, but not evaluated: the pipeline steps over it, unlit
|
|
47
|
+
return result === 'bypass' && !(d.seen || {})[id] ? { ...d, seen: { ...(d.seen || {}), [id]: { result, at: now } } } : d;
|
|
48
|
+
}
|
|
49
|
+
let n = { ...d, pips: { ...d.pips, [id]: { result, at: now } }, seen: { ...(d.seen || {}), [id]: { result, at: now } }, order: [...d.order, id] };
|
|
50
|
+
if (result === 'blocked') n = { ...n, phase: 'error', blockedAt: id, endedAt: n.endedAt ?? now };
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fold one reducer action into the strip. `a` is the quoxon reducer action ({type:'turn.begin'} or
|
|
56
|
+
* {type:'event', ev}); unknown actions return `d` unchanged (same object).
|
|
57
|
+
*/
|
|
58
|
+
export function dividerStep(d, a, now) {
|
|
59
|
+
if (!a) return d;
|
|
60
|
+
if (a.type === 'turn.begin') return dividerStart(a.turnId, now);
|
|
61
|
+
if (a.type !== 'event' || !d || d.endedAt != null && a.ev?.type !== 'aocl.layer') return d;
|
|
62
|
+
const ev = a.ev || {};
|
|
63
|
+
if (ev.type === 'aocl.layer') {
|
|
64
|
+
if (d.endedAt != null && d.phase !== 'error') return d; // a finished green strip never changes
|
|
65
|
+
let n = light(d, ev, now);
|
|
66
|
+
const pre = ev.phase === 'pre' || ORDER.indexOf(String(ev.id)) <= ORDER.indexOf(PRE_LAST);
|
|
67
|
+
if (n.phase === 'gates' && (String(ev.id) === PRE_LAST || !pre)) n = { ...n, phase: 'model', modelAt: n.modelAt ?? now };
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
if (isModelSignal(ev)) return d.phase === 'gates' ? { ...d, phase: 'model', modelAt: now } : d;
|
|
71
|
+
if (ev.type === 'aocl.verdict' && ev.verdict === 'blocked') {
|
|
72
|
+
const id = ORDER.includes(ev.layer) ? ev.layer : 'L8';
|
|
73
|
+
return { ...d, phase: 'error', blockedAt: d.blockedAt || id, endedAt: d.endedAt ?? now };
|
|
74
|
+
}
|
|
75
|
+
if (ev.type === 'error') return { ...d, phase: 'error', endedAt: now };
|
|
76
|
+
if (ev.type === 'turn.finished') {
|
|
77
|
+
const ok = (ev.status || 'completed') === 'completed';
|
|
78
|
+
return ok && d.phase !== 'error' ? { ...d, phase: 'done', endedAt: now } : { ...d, phase: 'error', endedAt: d.endedAt ?? now };
|
|
79
|
+
}
|
|
80
|
+
return d;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True while the strip needs frames: a turn in flight, or the green/red hold after it. */
|
|
84
|
+
export function dividerLive(d, now) {
|
|
85
|
+
if (!d) return false;
|
|
86
|
+
if (d.endedAt == null) return true;
|
|
87
|
+
return now - d.endedAt < (d.phase === 'done' ? GREEN_MS : RED_MS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Each layer's own colour, from the dashboard (src/lib/aee/envelope.js AOCL_LAYERS, which LayerStackMini uses).
|
|
91
|
+
// L0 and L10 are grey there (#6b7280); the TUI has no grey text, so they take sky and violet.
|
|
92
|
+
export const LAYER_HEX = {
|
|
93
|
+
L0: '#38bdf8', L1: '#3b82f6', L2: '#3b82f6', L3: '#f59e0b', L4: '#8b5cf6', L5: '#8b5cf6',
|
|
94
|
+
L6: '#6366f1', L7: '#f97316', L8: '#14b8a6', L9: '#22c55e', L10: '#a855f7',
|
|
95
|
+
};
|
|
96
|
+
const hexRgb = (h) => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
|
|
97
|
+
const mixRgb = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
|
|
98
|
+
const DARK = [30, 36, 48];
|
|
99
|
+
export const layerRgb = (id) => hexRgb(LAYER_HEX[id]);
|
|
100
|
+
export const dimRgb = (id) => mixRgb(layerRgb(id), DARK, 0.6); // pending / bypassed: the layer's own hue, dimmed (not grey)
|
|
101
|
+
|
|
102
|
+
export const RGB = { done: [34, 197, 94], blocked: [239, 68, 68], model: [56, 189, 248] };
|
|
103
|
+
const LIT = { pass: '●', gated: '◆', blocked: '✖' };
|
|
104
|
+
const PENDING = '•';
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* When each layer the server reported was reached in the pipeline: in layer order, one STEP_MS apart, never
|
|
108
|
+
* before its own event. Bypassed layers are reached (the chain passes them) but stay unlit.
|
|
109
|
+
*/
|
|
110
|
+
export function layerReachTimes(d) {
|
|
111
|
+
const out = {};
|
|
112
|
+
if (!d) return out;
|
|
113
|
+
let t = d.startedAt - STEP_MS;
|
|
114
|
+
for (const id of ORDER) {
|
|
115
|
+
const s = (d.seen || {})[id] || d.pips[id];
|
|
116
|
+
if (!s) continue;
|
|
117
|
+
t = Math.max(t + STEP_MS, s.at);
|
|
118
|
+
out[id] = t;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** When the pipeline hands over to the model (the label starts to shimmer): every pre-model layer seen was reached. */
|
|
124
|
+
export function modelReachedAt(d) {
|
|
125
|
+
if (!d || d.modelAt == null) return null;
|
|
126
|
+
const r = layerReachTimes(d);
|
|
127
|
+
const pre = ORDER.slice(0, ORDER.indexOf(PRE_LAST) + 1).map(id => r[id]).filter(x => x != null);
|
|
128
|
+
return Math.max(d.modelAt, ...pre, d.startedAt) + (pre.length ? STEP_MS : 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The chain of 11 dots joined by rule: cells [{ t, rgb, bold }]. Motion only on the newest dot. */
|
|
132
|
+
export function chainCells(d, now, { still = false } = {}) {
|
|
133
|
+
const reach = layerReachTimes(d);
|
|
134
|
+
const reached = ORDER.filter(id => reach[id] != null && reach[id] <= now);
|
|
135
|
+
const newest = reached.filter(id => d.pips[id]).sort((a, b) => reach[b] - reach[a])[0] || null;
|
|
136
|
+
const cells = [];
|
|
137
|
+
ORDER.forEach((id, i) => {
|
|
138
|
+
if (i) cells.push({ t: '─' });
|
|
139
|
+
const p = d.pips[id];
|
|
140
|
+
if (p && reach[id] <= now) {
|
|
141
|
+
// only the newest dot blinks, only while the turn runs, and a blocked dot never dims (it stays red)
|
|
142
|
+
const blinkOff = !still && d.endedAt == null && p.result !== 'blocked' && id === newest && now - reach[id] < CURRENT_MS && Math.floor((now - reach[id]) / 175) % 2 === 1;
|
|
143
|
+
const rgb = p.result === 'blocked' ? RGB.blocked : layerRgb(id);
|
|
144
|
+
cells.push({ t: LIT[p.result], rgb: blinkOff ? dimRgb(id) : rgb, bold: !blinkOff });
|
|
145
|
+
} else cells.push({ t: PENDING, rgb: dimRgb(id) });
|
|
146
|
+
});
|
|
147
|
+
return { cells, newest, newestAt: newest ? reach[newest] : null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One frame of the rule: `{ cells: [{ t, rgb?, bold?, label? }] }` whose widths sum to exactly `cols`, or null
|
|
152
|
+
* when the normal rule should draw (no strip, the hold is over, NO_COLOR, reduced motion, too narrow, no label).
|
|
153
|
+
* Cells without `rgb` take the rule's own colour; `label: true` takes the label's own colour.
|
|
154
|
+
* `still` (a slow redraw): no blink and no katakana, but every lit dot and the end colours stay.
|
|
155
|
+
*/
|
|
156
|
+
export function dividerFrame({ cols, label, d, now, mono = false, reduced = false, still = false, seed = 0 }) {
|
|
157
|
+
if (mono || reduced || !d || cols < MIN_COLS || !label || !dividerLive(d, now)) return null;
|
|
158
|
+
const lab = ` ${label} `;
|
|
159
|
+
const lw = stringWidth(lab);
|
|
160
|
+
const chain = chainCells(d, now, { still });
|
|
161
|
+
const cw = chain.cells.length;
|
|
162
|
+
const left = cols - lw - 1 - cw;
|
|
163
|
+
if (left < 4) return null;
|
|
164
|
+
// the newest lit layer names itself to the left of the chain, in its colour, while it is new
|
|
165
|
+
const tag = chain.newest && now - chain.newestAt < TAG_MS ? ` ${chain.newest} ${LAYER_NAMES[chain.newest] || ''} ` : '';
|
|
166
|
+
const fill = Math.max(1, left - (tag && tag.length + 1 <= left - 1 ? tag.length : 0));
|
|
167
|
+
const leftCells = Array.from({ length: fill }, () => ({ t: '─' }));
|
|
168
|
+
if (fill < left) {
|
|
169
|
+
const rgb = d.pips[chain.newest].result === 'blocked' ? RGB.blocked : layerRgb(chain.newest);
|
|
170
|
+
leftCells.splice(fill - 1, 0, ...[...tag].map(ch => ({ t: ch, rgb, bold: true })));
|
|
171
|
+
}
|
|
172
|
+
while (leftCells.length < left) leftCells.push({ t: '─' });
|
|
173
|
+
let labelCells;
|
|
174
|
+
if (d.phase === 'done') labelCells = [{ t: lab, rgb: RGB.done, bold: true }];
|
|
175
|
+
else if (d.phase === 'error') labelCells = [{ t: lab, rgb: RGB.blocked, bold: true }];
|
|
176
|
+
else {
|
|
177
|
+
const at = modelReachedAt(d);
|
|
178
|
+
if (at != null && now >= at) {
|
|
179
|
+
if (still) labelCells = [{ t: lab, rgb: RGB.model, bold: true }];
|
|
180
|
+
else {
|
|
181
|
+
const fr = wordFrame(label, now, { since: at, seed });
|
|
182
|
+
const n = fr.length;
|
|
183
|
+
labelCells = [{ t: ' ' }, ...fr.map((c, i) => ({ t: c.ch, rgb: wordColor('working', i, n, now, c.flash ? 'flash' : c.glyph ? 'glyph' : 'letter') })), { t: ' ' }];
|
|
184
|
+
}
|
|
185
|
+
} else labelCells = [{ t: lab, label: true }];
|
|
186
|
+
}
|
|
187
|
+
return { cells: [...leftCells, ...chain.cells, ...labelCells, { t: '─' }] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Plain text of a frame (tests and pty proofs). */
|
|
191
|
+
export const frameText = (f) => (f ? f.cells.map(c => c.t).join('') : null);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-DIVIDER: the composer's bottom rule drawn from a dividerAocl frame (pips, bullet, katakana label, green/red).
|
|
3
|
+
* Draws ONE pre-coloured string per colour run (as the ring does), so a frame is a single row of exactly `cols`
|
|
4
|
+
* cells and Ink tokenises a handful of escapes, not one per cell.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { Text } from 'ink';
|
|
9
|
+
import { dividerFrame } from './dividerAocl.js';
|
|
10
|
+
|
|
11
|
+
const h = React.createElement;
|
|
12
|
+
const RUN_Q = 8; // quantise shimmer colours so neighbouring cells share a run
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per turn, a frame gap this long while the strip is live marks the redraw as slow: the strip then drops its motion
|
|
16
|
+
* (no bullet, no katakana) for the rest of that turn but keeps every lit pip. `slow` is a ref object owned by the caller.
|
|
17
|
+
*/
|
|
18
|
+
export const SLOW_GAP_MS = 400;
|
|
19
|
+
export function noteFrameGap(slow, d, now) {
|
|
20
|
+
if (!slow || !d) return false;
|
|
21
|
+
if (slow.turnId !== d.turnId) { slow.turnId = d.turnId; slow.last = now; slow.slow = false; return false; }
|
|
22
|
+
if (slow.last != null && now - slow.last > SLOW_GAP_MS && d.endedAt == null) slow.slow = true;
|
|
23
|
+
slow.last = now;
|
|
24
|
+
return slow.slow;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The frame as one string of chalk runs; `ruleColor`/`labelColor` are hex or undefined (theme roles resolved). */
|
|
28
|
+
export function paintFrame(f, { ruleColor, labelColor }) {
|
|
29
|
+
let out = '', run = '', key = null, style = null;
|
|
30
|
+
const paint = (s) => {
|
|
31
|
+
if (!s) return '';
|
|
32
|
+
let p = chalk;
|
|
33
|
+
if (style.rgb) p = p.rgb(...style.rgb);
|
|
34
|
+
else if (style.hex) p = p.hex(style.hex);
|
|
35
|
+
if (style.bold) p = p.bold;
|
|
36
|
+
return p(s);
|
|
37
|
+
};
|
|
38
|
+
const flush = () => { if (run) out += paint(run); run = ''; };
|
|
39
|
+
for (const c of f.cells) {
|
|
40
|
+
const rgb = c.rgb ? c.rgb.map(v => Math.max(0, Math.min(255, Math.round(v / RUN_Q) * RUN_Q))) : null;
|
|
41
|
+
const st = { rgb, hex: rgb ? null : (c.label ? labelColor : ruleColor) || null, bold: Boolean(c.bold) };
|
|
42
|
+
const k = `${rgb ? rgb.join(',') : st.hex}|${st.bold}`;
|
|
43
|
+
if (k !== key) { flush(); key = k; style = st; }
|
|
44
|
+
run += c.t;
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The bottom rule as a strip element, or null when the caller should draw its normal Rule (a plain call, not a component). */
|
|
51
|
+
export function dividerRuleEl({ cols, label, divider, now, ruleColor, labelColor, mono = false, reduced = false, slow = null }) {
|
|
52
|
+
if (!divider) return null;
|
|
53
|
+
const isSlow = noteFrameGap(slow, divider, now);
|
|
54
|
+
const f = dividerFrame({ cols, label, d: divider, now, mono, reduced, still: isSlow }); // slow: keep the lit pips, drop the motion
|
|
55
|
+
if (!f) return null;
|
|
56
|
+
return h(Text, { wrap: 'truncate-end' }, paintFrame(f, { ruleColor, labelColor }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /dragon explain | ask | sleep: the pure parts. Provider-neutral: explain and ask only build a framed user
|
|
3
|
+
* message for the normal governed turn path, so whichever AI is selected answers it (and with no AI connected
|
|
4
|
+
* the local no-AI card from noai.js is shown instead). Sleep is a rendering-only screensaver: katakana noise
|
|
5
|
+
* over the live screen with a sleeping dragon; it never touches the transcript or scrollback.
|
|
6
|
+
*/
|
|
7
|
+
import stringWidth from 'string-width';
|
|
8
|
+
import { SCRAMBLE_GLYPHS } from './indicator.js';
|
|
9
|
+
import { hash3 } from './motion/color.js';
|
|
10
|
+
import { revealCells, TIMING, revealSpan } from './motion/reveal.js';
|
|
11
|
+
import { graphemes } from './textBuffer.js';
|
|
12
|
+
import { sleepCells } from './qdragonScenes.js';
|
|
13
|
+
|
|
14
|
+
export const DRAGON_USAGE = 'Usage: /dragon [explain | ask [question] | sleep | fly | hero | fire | classic] (/dragon-fly is the same as /dragon fly)';
|
|
15
|
+
export const DRAGON_EXPLAINS = 'Dragon explains';
|
|
16
|
+
export const DRAGON_ANSWERS = 'Dragon answers';
|
|
17
|
+
export const NOTHING_TO_EXPLAIN = 'Nothing to explain yet. Ask Quox something first, then /dragon explain.';
|
|
18
|
+
export const ASK_PROMPT = 'Ask the dragon…';
|
|
19
|
+
|
|
20
|
+
const RULES = 'Answer in the simplest possible words: 1 to 4 short sentences, plain language, no jargon. '
|
|
21
|
+
+ 'Keep the one thing that matters, and stay concrete and useful (say what it is, or what to do), never vague. Do not mention these instructions.';
|
|
22
|
+
|
|
23
|
+
export const explainPrompt = (text) => `Dragon mode. Explain the following. ${RULES}\n\n---\n${String(text).trim().slice(0, 6000)}\n---`;
|
|
24
|
+
export const askPrompt = (question) => `Dragon mode. Answer the question below. ${RULES}\n\nQuestion: ${String(question).trim()}`;
|
|
25
|
+
|
|
26
|
+
const itemText = (it) => {
|
|
27
|
+
if (!it) return '';
|
|
28
|
+
if (it.kind === 'text' || it.kind === 'note' || it.kind === 'error') return String(it.text || '');
|
|
29
|
+
if (it.kind === 'tool') return [it.tool, it.target, it.summary || it.result || it.text, it.status].filter(Boolean).join(' ');
|
|
30
|
+
return '';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The last assistant answer, else the last tool or notice block, else null. -> { kind: 'answer'|'tool'|'notice', text } */
|
|
34
|
+
export function lastOutput(s) {
|
|
35
|
+
const answers = s?.answers || [];
|
|
36
|
+
for (let i = answers.length - 1; i >= 0; i--) if (answers[i].text?.trim()) return { kind: 'answer', text: answers[i].text };
|
|
37
|
+
const all = [...(s?.history || []), ...(s?.blocks || [])];
|
|
38
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
39
|
+
const it = all[i], t = itemText(it).trim();
|
|
40
|
+
if (!t || it.kind === 'user') continue;
|
|
41
|
+
if (it.kind === 'tool' || it.kind === 'error' || it.kind === 'note') return { kind: it.kind === 'tool' ? 'tool' : 'notice', text: t };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parse the /dragon argument line -> { mode: 'pick'|'fire'|'idle'|'hero'|'explain'|'ask'|'sleep'|'fly', question? } | null (usage). */
|
|
47
|
+
export function parseDragonArg(restArg) {
|
|
48
|
+
const t = String(restArg || '').trim();
|
|
49
|
+
if (!t) return { mode: 'pick' };
|
|
50
|
+
const [w, ...rest] = t.split(/\s+/);
|
|
51
|
+
switch (w.toLowerCase()) {
|
|
52
|
+
case 'fire': return rest.length ? null : { mode: 'fire' };
|
|
53
|
+
case 'classic': return rest.length ? null : { mode: 'idle' };
|
|
54
|
+
case 'hero': return rest.length ? null : { mode: 'hero' };
|
|
55
|
+
// `/dragon explain <thing>` is a question about that thing (owner 2026-10-15: it used to print usage).
|
|
56
|
+
case 'explain': return rest.length ? { mode: 'ask', question: rest.join(' ').trim() } : { mode: 'explain' };
|
|
57
|
+
case 'sleep': return rest.length ? null : { mode: 'sleep' };
|
|
58
|
+
case 'fly': return rest.length ? null : { mode: 'fly' };
|
|
59
|
+
case 'ask': return { mode: 'ask', question: rest.join(' ').trim() };
|
|
60
|
+
// Anything else is a question for the dragon, never a usage error.
|
|
61
|
+
default: return { mode: 'ask', question: t };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---- sleep: katakana veil ---------------------------------------------------------------------------------
|
|
66
|
+
export const VEIL_IN_MS = 700; // text drops into katakana, left to right
|
|
67
|
+
export const VEIL_STEP_MS = 700; // the noise re-rolls this slowly (low frame rate; a shimmer, not a flicker)
|
|
68
|
+
export const WAKE_MS = Math.round(revealSpan(TIMING.row) * 1000); // the existing decode reveal, run forward
|
|
69
|
+
|
|
70
|
+
const isText = (ch) => /^[\p{L}\p{N}\p{P}\p{S}]+$/u.test(ch);
|
|
71
|
+
const kana = (i, k, salt) => SCRAMBLE_GLYPHS[Math.floor(hash3(i, k, salt) * SCRAMBLE_GLYPHS.length)];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One source line as veil cells at `ms` since sleep began (phase 'in' then 'asleep') or since wake began
|
|
75
|
+
* (phase 'wake'). Cells are { ch, kind: 'kana'|'plain'|'blank'|'glow', g }. Width of every column is preserved
|
|
76
|
+
* (a 2-wide char becomes two kana), so rows never change length. still: no re-rolling.
|
|
77
|
+
*/
|
|
78
|
+
export function veilRow(text, ms, { phase = 'asleep', still = false, salt = 1, row = 0 } = {}) {
|
|
79
|
+
if (phase === 'wake') return revealCells(text, ms / 1000, TIMING.row, { salt: salt + row }).map((c) => ({ ...c }));
|
|
80
|
+
const gs = graphemes(String(text ?? '')).map((g) => g.text);
|
|
81
|
+
const n = gs.length, bucket = still ? 0 : Math.floor(ms / VEIL_STEP_MS);
|
|
82
|
+
const edge = phase === 'in' && !still ? (ms / VEIL_IN_MS) * n : Infinity; // cells left of the edge are veiled
|
|
83
|
+
const out = [];
|
|
84
|
+
gs.forEach((ch, i) => {
|
|
85
|
+
const w = stringWidth(ch);
|
|
86
|
+
if (w === 0) return;
|
|
87
|
+
if (ch === ' ' || !isText(ch) || i > edge) { out.push({ ch, kind: 'plain', g: 0 }); return; }
|
|
88
|
+
for (let k = 0; k < w; k++) out.push({ ch: kana(i * 2 + k + row * 131, bucket, salt), kind: 'kana', g: hash3(i, bucket + 7, salt) });
|
|
89
|
+
});
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const veilText = (cells) => cells.map((c) => c.ch).join('');
|
|
94
|
+
export const isVeiled = (cells) => cells.some((c) => c.kind === 'kana');
|
|
95
|
+
|
|
96
|
+
/** The visible text the screen shows, as at most `rows` lines wrapped to `cols` (tail of the transcript). */
|
|
97
|
+
export function sourceLines(s, cols, rows) {
|
|
98
|
+
const all = [...(s?.history || []), ...(s?.blocks || [])];
|
|
99
|
+
const lines = [];
|
|
100
|
+
for (let i = all.length - 1; i >= 0 && lines.length < rows; i--) {
|
|
101
|
+
const it = all[i];
|
|
102
|
+
const t = it.kind === 'user' ? `> ${it.text || ''}` : itemText(it);
|
|
103
|
+
if (!t.trim()) continue;
|
|
104
|
+
const wrapped = [];
|
|
105
|
+
for (const raw of t.split('\n')) {
|
|
106
|
+
let r = raw;
|
|
107
|
+
if (!r) { wrapped.push(''); continue; }
|
|
108
|
+
while (stringWidth(r) > cols) { let cut = cols; while (cut > 1 && stringWidth(r.slice(0, cut)) > cols) cut--; wrapped.push(r.slice(0, cut)); r = r.slice(cut); }
|
|
109
|
+
wrapped.push(r);
|
|
110
|
+
}
|
|
111
|
+
lines.unshift(...wrapped);
|
|
112
|
+
}
|
|
113
|
+
return lines.slice(-rows);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- the sleep screen -----------------------------------------------------------------------------------------
|
|
117
|
+
export const SLEEP_HINT = 'asleep · any key wakes · /back when you return';
|
|
118
|
+
const VEIL_RGB = [129, 140, 248], HINT_RGB = [186, 230, 253];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The whole sleep frame as rows of { ch, rgb } cells, `rows` tall and `cols` wide. ms = since sleep began, or
|
|
122
|
+
* since wake began when wake is true (text decodes back, the dragon is gone). Pure: no transcript access, the
|
|
123
|
+
* caller passes the source lines. still (reduced motion): a static veil and a still dragon. mono: no colour.
|
|
124
|
+
*/
|
|
125
|
+
export function composeSleep({ lines, cols, rows, ms = 0, wake = false, still = false, mono = false, theme = 'dark', size }) {
|
|
126
|
+
const phase = wake ? 'wake' : ms < VEIL_IN_MS && !still ? 'in' : 'asleep';
|
|
127
|
+
const col = (c) => (mono ? null : c);
|
|
128
|
+
const grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({ ch: ' ', rgb: null })));
|
|
129
|
+
const body = wake ? rows : rows - 1; // the last row holds the wake hint while asleep
|
|
130
|
+
const src = lines.slice(-body);
|
|
131
|
+
const top = body - src.length;
|
|
132
|
+
src.forEach((line, i) => {
|
|
133
|
+
veilRow(line, ms, { phase, still, row: i }).slice(0, cols).forEach((c, x) => {
|
|
134
|
+
grid[top + i][x] = { ch: c.ch, rgb: c.kind === 'kana' ? col(VEIL_RGB) : null, bold: c.kind === 'glow' && c.g > 0.5 };
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
if (!wake) {
|
|
138
|
+
const sz = size ?? (rows >= 32 && cols >= 64 ? undefined : 'mid');
|
|
139
|
+
const dcols = Math.min(cols, sz === 'mid' || cols < 64 ? 44 : 72);
|
|
140
|
+
const dc = sleepCells(ms, dcols, undefined, { theme, mono, still, size: sz });
|
|
141
|
+
const x0 = Math.max(0, Math.floor((cols - dcols) / 2)), y0 = Math.max(0, Math.floor((rows - 1 - dc.length) / 2));
|
|
142
|
+
dc.forEach((r, i) => {
|
|
143
|
+
if (y0 + i >= rows - 1) return;
|
|
144
|
+
r.forEach((c, x) => { if (x0 + x < cols) grid[y0 + i][x0 + x] = { ch: c.ch, rgb: mono ? null : c.rgb, bg: c.bg || undefined }; });
|
|
145
|
+
});
|
|
146
|
+
const hint = [...SLEEP_HINT], hx = Math.max(0, Math.floor((cols - hint.length) / 2));
|
|
147
|
+
hint.forEach((ch, i) => { if (hx + i < cols) grid[rows - 1][hx + i] = { ch, rgb: col(HINT_RGB) }; });
|
|
148
|
+
}
|
|
149
|
+
return grid;
|
|
150
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Ink views for /dragon explain|ask (the talking dragon) and /dragon sleep (the screensaver). Frames come from dragonModes.js. */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { cellNodes } from './qmotion.js';
|
|
5
|
+
import { composeSleep } from './dragonModes.js';
|
|
6
|
+
import { pickCells } from './qdragonScenes.js';
|
|
7
|
+
import { flyFrame, flyCols } from './qdragonFly.js';
|
|
8
|
+
import { mono, isLight, clip } from './theme.js';
|
|
9
|
+
import { useFrame } from './motion/frame.js';
|
|
10
|
+
|
|
11
|
+
const h = React.createElement;
|
|
12
|
+
|
|
13
|
+
/** The sleep screen: the whole live region while asleep (and for the decode on wake). Reads the clock itself. */
|
|
14
|
+
export function SleepScreen({ lines, cols, rows, startedAt, wakeAt = null, still = false }) {
|
|
15
|
+
useFrame();
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const wake = wakeAt != null;
|
|
18
|
+
const ms = wake ? now - wakeAt : now - startedAt;
|
|
19
|
+
const grid = composeSleep({ lines, cols, rows, ms, wake, still, mono: mono(), theme: isLight() ? 'light' : 'dark' });
|
|
20
|
+
return h(Box, { flexDirection: 'column' }, ...grid.map((r, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(r, `s${k}-`))));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The compact dragon beside an explain/ask answer: the mouth works while text streams, then it rests with idle eyes. */
|
|
24
|
+
export function DragonTalk({ cols, label, talking, since, still = false }) {
|
|
25
|
+
useFrame();
|
|
26
|
+
const dcols = Math.min(cols, 44);
|
|
27
|
+
const t = Date.now() - since;
|
|
28
|
+
const cells = pickCells(t, dcols, undefined, { theme: isLight() ? 'light' : 'dark', mono: mono(), still: still || !talking, talk: talking && !still, size: 'mid', mouthSide: 'right' });
|
|
29
|
+
return h(Box, { flexDirection: 'column', marginTop: 1 },
|
|
30
|
+
...cells.map((row, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(row, `t${k}-`))),
|
|
31
|
+
h(Text, null, clip(label, Math.max(10, cols - 2))));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** /dragon-fly: one pass of the dragon in flight across the live region (qdragonFly.js). `still`: a single frame, no motion. */
|
|
35
|
+
export function FlyLive({ cols, rows, startedAt, still = false }) {
|
|
36
|
+
useFrame();
|
|
37
|
+
const c = flyCols(cols);
|
|
38
|
+
const cells = flyFrame(Date.now() - startedAt, c, { still, mono: mono(), theme: isLight() ? 'light' : 'dark', termRows: rows });
|
|
39
|
+
return h(Box, { flexDirection: 'column', marginTop: 1 },
|
|
40
|
+
...cells.map((row, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(row, `f${k}-`))));
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys typed before the app is ready (R3 audit D4). bin/quox.js calls this before it loads React, Ink or any
|
|
3
|
+
* command module, so the tty is raw (no cooked echo) and every key is kept for the composer. main() calls it
|
|
4
|
+
* again and gets the same capture. Tiny on purpose: it must not import anything heavy.
|
|
5
|
+
*
|
|
6
|
+
* A line typed while the tty was still cooked reaches us when raw mode turns on, with the kernel's ICRNL
|
|
7
|
+
* having turned its Enter into LF; that first read is mapped back to CR so the Enter submits as typed.
|
|
8
|
+
*/
|
|
9
|
+
import { PASTE_ON, PASTE_OFF } from './keys.js';
|
|
10
|
+
|
|
11
|
+
const COOKED_MS = 50; // the read that drains the cooked line buffer arrives right after raw mode turns on
|
|
12
|
+
let shared = null;
|
|
13
|
+
|
|
14
|
+
export function captureEarlyInput(stdin = process.stdin, stdout = process.stdout) {
|
|
15
|
+
const own = stdin === process.stdin;
|
|
16
|
+
if (own && shared) return shared;
|
|
17
|
+
if (!stdin?.isTTY) return null;
|
|
18
|
+
const chunks = [];
|
|
19
|
+
const t0 = Date.now();
|
|
20
|
+
const fn = (d) => {
|
|
21
|
+
const s = typeof d === 'string' ? d : d.toString('utf8');
|
|
22
|
+
chunks.push(Date.now() - t0 < COOKED_MS ? s.replace(/\n/g, '\r') : s);
|
|
23
|
+
};
|
|
24
|
+
try { stdin.setRawMode?.(true); stdin.resume?.(); } catch { /* not a tty after all */ }
|
|
25
|
+
stdin.on('data', fn);
|
|
26
|
+
if (stdout?.isTTY) stdout.write(PASTE_ON);
|
|
27
|
+
const cap = {
|
|
28
|
+
chunks,
|
|
29
|
+
detach: () => stdin.off('data', fn),
|
|
30
|
+
/** Nothing will take the terminal over (help, a flag error): give it back as it was. */
|
|
31
|
+
release: () => {
|
|
32
|
+
stdin.off('data', fn);
|
|
33
|
+
try { stdin.setRawMode?.(false); stdin.pause?.(); } catch { /* ignore */ }
|
|
34
|
+
if (stdout?.isTTY) stdout.write(PASTE_OFF);
|
|
35
|
+
if (own) shared = null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (own) shared = cap;
|
|
39
|
+
return cap;
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Echo of a command the human typed, shown in the scrollback like a prompt line (wrapped in full, secrets masked).
|
|
2
|
+
// Owner 2026-10-15: "why truncated": the echo wraps like any prompt; only a runaway paste is capped.
|
|
3
|
+
import { redactSecrets } from './slotChat.js';
|
|
4
|
+
import { clip } from './theme.js';
|
|
5
|
+
|
|
6
|
+
const PIN_CMD = /^\/(authenticate|authorise|authorize)(\s|$)/i;
|
|
7
|
+
|
|
8
|
+
/** -> { shown, full }: `shown` is the whole command on one logical line (the view wraps it; capped at ECHO_CAP), `full` is the masked text the transcript records. */
|
|
9
|
+
export const ECHO_CAP = 2000;
|
|
10
|
+
export function echoLine(text, { cols = 100 } = {}) {
|
|
11
|
+
let t = String(text ?? '').trim();
|
|
12
|
+
if (PIN_CMD.test(t)) t = t.split(/\s+/)[0]; // a stray argument may be a PIN: never shown
|
|
13
|
+
const full = redactSecrets(t).text;
|
|
14
|
+
return { shown: clip(full.replace(/\s*\n\s*/g, ' '), ECHO_CAP), full };
|
|
15
|
+
}
|
package/lib/quoxon/egg.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The launch Easter egg (CLI-QUOX). On launch the v8 dragon greets you (googly eyes open, blink, smile), rarely:
|
|
3
3
|
* 1 in EGG_ODDS launches, never twice in a row, never in --print, under reduced motion or off a TTY.
|
|
4
|
-
* /
|
|
4
|
+
* /dragon plays it on demand, and the cinematic motion level always may.
|
|
5
5
|
*
|
|
6
6
|
* pickLaunchEgg is pure; launchEgg adds the one bit of persistence (did the last launch show the eye?) in
|
|
7
7
|
* ~/.config/quox/motion.json. QUOX_EGG=eye forces it (demos, captures); QUOX_EGG=off disables it.
|