quoxcli 0.0.0-stage → 0.2.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 +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox motion`: the product version of the animation lab (~/quoxon-work/intro/lab). Plays every Q motion
|
|
3
|
+
* with a caption: Orbit & Lock, Portal, Particle collapse, the Dragon's Eye, the perpetual status Q in its
|
|
4
|
+
* four states, the hello, the composer ring and the reveal effects. No server, no account, nothing sent.
|
|
5
|
+
*
|
|
6
|
+
* Scenes are pure: galleryScenes() returns { id, title, caption, ms, frame(t) -> ANSI lines }. runGallery()
|
|
7
|
+
* is the only part that touches a terminal (cursor, timers, signals).
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
labSize, orbitCellsAt, portalCellsAt, particleCellsAt, eyeCellsAt, cellsToAnsi, qCells,
|
|
11
|
+
INTRO_MS, PORTAL_MS, EYE_MS, SKY, VIOLET, AMBER,
|
|
12
|
+
} from './qart.js';
|
|
13
|
+
import { qGlyph, glyphAnsi, GLYPH_STATES } from './qglyph.js';
|
|
14
|
+
import { helloCells, helloAnsi, helloGradient, helloMs } from './qhello.js';
|
|
15
|
+
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
const fg = (rgb) => `\x1b[38;2;${rgb.map((v) => Math.max(0, Math.min(255, Math.round(v)))).join(';')}m`;
|
|
18
|
+
const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);
|
|
19
|
+
const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
|
20
|
+
const DIM = [58, 66, 82], MUTED = [120, 132, 150], TXT = [226, 232, 240], TEAL = [6, 182, 212];
|
|
21
|
+
const GREEN = [34, 197, 94], WHITE = [255, 255, 255];
|
|
22
|
+
const paint = (s, rgb, mono) => (mono || !rgb ? s : fg(rgb) + s + RESET);
|
|
23
|
+
|
|
24
|
+
// ---- composer ring (lab rules.mjs: the website's conic border as one light ring over the two rules) ------
|
|
25
|
+
const PAL = { work: [SKY, VIOLET, TEAL], wait: [AMBER, [0xea, 0x58, 0x0c], [0xb4, 0x53, 0x09]] };
|
|
26
|
+
const STOPS = [[0, 0, 0.85], [0.35, 1, 0.8], [0.6, 0, 0], [0.85, 2, 0.55], [1, 0, 0.85]];
|
|
27
|
+
function ringAt(u, w) {
|
|
28
|
+
let i = 0; while (i < STOPS.length - 2 && u > STOPS[i + 1][0]) i++;
|
|
29
|
+
const [p0, c0, a0] = STOPS[i], [p1, c1, a1] = STOPS[i + 1], t = (u - p0) / (p1 - p0 || 1);
|
|
30
|
+
const col = (c) => mix(PAL.work[c], PAL.wait[c], w);
|
|
31
|
+
const A = a0 + (a1 - a0) * t;
|
|
32
|
+
const pm = mix(col(c0).map((v) => v * a0), col(c1).map((v) => v * a1), t);
|
|
33
|
+
return [A ? pm.map((v) => v / A) : col(c0), A];
|
|
34
|
+
}
|
|
35
|
+
const RING_SEG = [['idle', 0.8], ['work', 3.2], ['settle', 0.4], ['idle', 0.6], ['wait', 2.2], ['settle', 0.4]];
|
|
36
|
+
export const RING_MS = RING_SEG.reduce((a, s) => a + s[1], 0) * 1000;
|
|
37
|
+
/** Ring state at t ms, integrated at 60 Hz from 0 (deterministic). { angle, k, w, mode } */
|
|
38
|
+
export function ringState(t) {
|
|
39
|
+
let angle = 0, k = 0, w = 0, mode = 'idle', settleFrom = { om: 144 }, om = 0, prev = 'idle';
|
|
40
|
+
const T = t / 1000, dt = 1 / 60;
|
|
41
|
+
for (let s = 0; s < T; s += dt) {
|
|
42
|
+
let acc = 0, si = 0;
|
|
43
|
+
while (si < RING_SEG.length - 1 && acc + RING_SEG[si][1] <= s) acc += RING_SEG[si++][1];
|
|
44
|
+
const [m, dur] = RING_SEG[si], p = clamp((s - acc) / dur);
|
|
45
|
+
if (m === 'settle' && prev !== 'settle') settleFrom = { om };
|
|
46
|
+
mode = m; prev = m;
|
|
47
|
+
let wt = 0;
|
|
48
|
+
if (m === 'work') { om = 144; k = Math.min(1, k + dt / 0.15); wt = 0; }
|
|
49
|
+
else if (m === 'wait') { om = 144 * 0.3; k = Math.min(1, k + dt / 0.15); wt = 1; }
|
|
50
|
+
else if (m === 'settle') { om = settleFrom.om * (1 - p) ** 2; k = 1 - p * p * (3 - 2 * p); wt = w; }
|
|
51
|
+
else { om = 0; k = 0; w = 0; }
|
|
52
|
+
if (m !== 'idle') w += (wt - w) * Math.min(1, dt / 0.2);
|
|
53
|
+
angle = (angle + om * dt) % 360;
|
|
54
|
+
}
|
|
55
|
+
return { angle, k, w, mode };
|
|
56
|
+
}
|
|
57
|
+
/** The two composer rules at ring state st, W columns wide. */
|
|
58
|
+
export function ringLines(W, st, mono = false) {
|
|
59
|
+
const line = (top) => {
|
|
60
|
+
let s = '', last = '';
|
|
61
|
+
const base = top ? mix(DIM, SKY, 0.75) : DIM;
|
|
62
|
+
for (let x = 0; x < W; x++) {
|
|
63
|
+
let rgb = base;
|
|
64
|
+
if (st.k > 0) {
|
|
65
|
+
const pos = top ? (x + 0.5) / (2 * W) : (W + (W - 1 - x) + 0.5) / (2 * W);
|
|
66
|
+
const u = (((pos - st.angle / 360) % 1) + 1) % 1;
|
|
67
|
+
const [c, a] = ringAt(u, st.w);
|
|
68
|
+
rgb = mix(base, mix(DIM, c, a), st.k);
|
|
69
|
+
}
|
|
70
|
+
if (!mono) { const e = fg(rgb); if (e !== last) { s += e; last = e; } }
|
|
71
|
+
s += '─';
|
|
72
|
+
}
|
|
73
|
+
return s + (mono ? '' : RESET);
|
|
74
|
+
};
|
|
75
|
+
const label = { idle: 'idle', work: 'working', wait: 'waiting for approval', settle: 'done' }[st.mode];
|
|
76
|
+
return [line(true), paint('› ', SKY, mono) + paint('Ask Quox anything…', MUTED, mono), line(false), paint(`ring: ${label}`, MUTED, mono)];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- reveal effects (lab fx.mjs, the reveals that ship in the client) ------------------------------------
|
|
80
|
+
const decodeLine = (text, t, f, mono, perChar = 14) => helloAnsi(helloCells(text, t, { fg: f, mono, perChar }));
|
|
81
|
+
/** fx at t ms: a tool row settles, an answer drops in, an approval card arms, a receipt seals, ProofFlow lights. */
|
|
82
|
+
export function fxLines(t, W, mono = false) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const at = (s) => t - s;
|
|
85
|
+
// tool row (0 ms): spinner while running, then a flash to ✔ and the result decodes in
|
|
86
|
+
const L = at(0), SETTLE = 900;
|
|
87
|
+
const head = L < SETTLE ? paint('◐◓◑◒'[Math.floor(t / 125) % 4], SKY, mono) : paint('✔', L < SETTLE + 120 ? WHITE : GREEN, mono);
|
|
88
|
+
out.push(head + decodeLine(' SEO │ summary │ ', L, (i, ch) => (ch === '│' ? DIM : i < 5 ? TEAL : MUTED), mono) + (L < SETTLE ? paint('…', MUTED, mono) : decodeLine('30 crawls · 2.4s', L - SETTLE, TXT, mono)));
|
|
89
|
+
out.push('');
|
|
90
|
+
// answer (1400 ms): lines drop in one by one, first chars decode, afterglow behind
|
|
91
|
+
const ans = ['Crawls finished with no blocking errors this week.', 'Impressions are up 8 percent; 3 pages moved into the top ten.'];
|
|
92
|
+
ans.forEach((ln, k) => { const s = at(1400 + k * 110); out.push(s < -140 ? '' : ' ' + decodeLine(ln.slice(0, W - 4), s, TXT, mono, 6)); });
|
|
93
|
+
out.push('');
|
|
94
|
+
// approval card (2600 ms): amber rule draws in, title decodes, 300 ms arming, then ready
|
|
95
|
+
const A = at(2600), iw = Math.min(W - 4, 50), draw = clamp(A / 250), n = Math.round(draw * (iw + 2));
|
|
96
|
+
const rule = (a, b) => (A <= 0 ? '' : paint([...(a + '─'.repeat(iw) + b)].slice(0, n).join(''), AMBER, mono));
|
|
97
|
+
out.push(rule('╭', '╮'));
|
|
98
|
+
const armed = clamp((A - 400) / 300), ready = A >= 700;
|
|
99
|
+
out.push(A > 100 ? paint('│ ', AMBER, mono) + decodeLine('Send the weekly SEO report?', A - 100, TXT, mono) : '');
|
|
100
|
+
out.push(A > 100 ? paint('│ ', AMBER, mono) + paint('[ Approve ]', ready ? AMBER : DIM, mono) + ' ' + paint('[ Deny ]', ready ? MUTED : DIM, mono) + ' ' + paint(ready ? 'ready ' : 'arming', ready ? GREEN : AMBER, mono) + ' ' + paint([...'▰▰▰▰▰▰'].map((c, i) => (ready || i < Math.floor(armed * 6) ? '▰' : '▱')).join(''), ready ? GREEN : AMBER, mono) : '');
|
|
101
|
+
out.push(rule('╰', '╯'));
|
|
102
|
+
out.push('');
|
|
103
|
+
// receipt seal (3800 ms): text resolves, the seal settles with a one-frame glint
|
|
104
|
+
const R = at(3800), seq = ['·', '○', '◌', '◎'];
|
|
105
|
+
const seal = R < 0 ? ' ' : R < 400 ? paint(seq[Math.floor(R / 70) % 4], mix(DIM, TEAL, clamp(R / 400)), mono) : paint('◉', R < 460 ? WHITE : TEAL, mono);
|
|
106
|
+
out.push(R < -140 ? '' : seal + decodeLine(' WARD │ witnessed · seq 393', R, (i, ch) => (ch === '│' ? DIM : i < 5 ? TEAL : TXT), mono));
|
|
107
|
+
// ProofFlow (4600 ms): AEE > AOCL > TOOL > WARD light up in sequence; completed ones stay still
|
|
108
|
+
const P = at(4600);
|
|
109
|
+
out.push(P < 0 ? '' : paint('proof ', MUTED, mono) + ['AEE', 'AOCL', 'TOOL', 'WARD'].map((nm, i) => { const d = P - i * 120; return paint(nm, d < 0 ? DIM : d < 100 ? WHITE : SKY, mono); }).join(paint(' ─▸ ', mix(DIM, SKY, 0.8), mono)));
|
|
110
|
+
out.push('');
|
|
111
|
+
// closing line (5400 ms)
|
|
112
|
+
const C = at(5400);
|
|
113
|
+
out.push(C < -140 ? '' : decodeLine('Thought for 12s · 1.4k tokens · $0.02', C, (i, ch) => (ch === '·' ? DIM : MUTED), mono));
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
export const FX_MS = 6600;
|
|
117
|
+
|
|
118
|
+
// ---- scenes ------------------------------------------------------------------------------------------------
|
|
119
|
+
const STATE_WORD = { resting: 'Ready', working: 'Thinking…', waiting: 'Waiting for approval', failing: 'Failed' };
|
|
120
|
+
const STATE_TAG = { resting: 'resting: slow breathing glow', working: 'working: the gradient flows through it', waiting: 'waiting: amber pulse', failing: 'failing: a red flash, then steady red' };
|
|
121
|
+
const STATE_RGB = { resting: SKY, working: SKY, waiting: AMBER, failing: [239, 68, 68] };
|
|
122
|
+
export const PERP_MS = 2600;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The gallery's scenes for a terminal. narrow forces the 40-column layout (the compact particle Q).
|
|
126
|
+
* Each scene: { id, title, caption, ms, frame(t, { mono }) -> string[] }.
|
|
127
|
+
*/
|
|
128
|
+
export function galleryScenes({ cols = 100, rows = 30, narrow = false } = {}) {
|
|
129
|
+
const W = narrow ? 40 : cols;
|
|
130
|
+
const size = narrow ? 'small' : labSize(W, rows);
|
|
131
|
+
const big = size === 'small' ? 'small' : size;
|
|
132
|
+
const art = (cells) => cellsToAnsi(cells);
|
|
133
|
+
const hello = "Hi, I'm Quox.";
|
|
134
|
+
return [
|
|
135
|
+
{ id: 'orbit', title: 'Orbit & Lock', caption: size === 'small' ? 'the compact Q: particles circle, then collapse into the mark (the intro under 60 columns)' : 'particles swirl on a ring with trailing drag, then lock into the Q in an angular wave; horns grow out, the tail sweeps in last (the intro)', ms: INTRO_MS + 300,
|
|
136
|
+
frame: (t, o) => art(orbitCellsAt(big, t, { color: !o.mono })) },
|
|
137
|
+
{ id: 'portal', title: 'Portal', caption: 'a ring spins edge-on and opens; the Q fills in from the opening, the tail sweeps in last', ms: PORTAL_MS + 600,
|
|
138
|
+
frame: (t, o) => art(portalCellsAt(big, t, { color: !o.mono })) },
|
|
139
|
+
{ id: 'particle', title: 'Particle collapse', caption: 'density rises on a circle, then collapses into the compact Q', ms: 1100 + 500,
|
|
140
|
+
frame: (t, o) => art(particleCellsAt('small', t, { color: !o.mono })) },
|
|
141
|
+
{ id: 'eye', title: "The Dragon's Eye", caption: 'a slit-pupil eye opens in the bowl, blinks once, and dissolves back into the Q (/eye; rarely on launch)', ms: EYE_MS + 500,
|
|
142
|
+
frame: (t, o) => art(eyeCellsAt(big, t, { color: !o.mono })) },
|
|
143
|
+
{ id: 'hello', title: 'Hello', caption: 'the Q locks, then says hello: the line decodes in from katakana with a short afterglow', ms: INTRO_MS + helloMs(hello) + 600,
|
|
144
|
+
frame: (t, o) => {
|
|
145
|
+
const q = art(t < INTRO_MS ? orbitCellsAt(big, t, { color: !o.mono }) : qCells(big, { color: !o.mono }));
|
|
146
|
+
const line = helloAnsi(helloCells(hello, t - INTRO_MS, { fg: helloGradient(hello), mono: o.mono }));
|
|
147
|
+
if (W < 60) return [...q, '', line];
|
|
148
|
+
const mid = Math.floor(q.length / 2);
|
|
149
|
+
return q.map((l, i) => (i === mid ? l + ' ' + line : l));
|
|
150
|
+
} },
|
|
151
|
+
{ id: 'perpetual', title: 'The perpetual Q', caption: 'the status glyph carries state in colour alone, one fixed shape: resting breathes, working flows sky to violet to teal, waiting pulses amber, failing flashes red', ms: PERP_MS * 4,
|
|
152
|
+
frame: (t, o) => {
|
|
153
|
+
const i = Math.min(3, Math.floor(t / PERP_MS)), st = GLYPH_STATES[i], tt = t - i * PERP_MS;
|
|
154
|
+
return GLYPH_STATES.map((s, k) => {
|
|
155
|
+
const g = qGlyph(s, k === i ? tt : 0, { mono: o.mono, quiet: k !== i, since: 0 });
|
|
156
|
+
const word = paint(STATE_WORD[s], k === i ? STATE_RGB[s] : DIM, o.mono);
|
|
157
|
+
const tag = W >= 70 ? ' ' + paint(STATE_TAG[s], k === i ? MUTED : DIM, o.mono) : '';
|
|
158
|
+
return (k === i ? '› ' : ' ') + glyphAnsi(g) + ' ' + word + tag;
|
|
159
|
+
}).concat(['', ' ' + paint(`now: ${st}`, MUTED, o.mono)]);
|
|
160
|
+
} },
|
|
161
|
+
{ id: 'ring', title: 'The composer ring', caption: 'the two composer rules become one light ring while Quox works; amber while it waits for you', ms: RING_MS,
|
|
162
|
+
frame: (t, o) => ringLines(Math.min(W - 2, 72), ringState(t), o.mono) },
|
|
163
|
+
{ id: 'fx', title: 'Reveal effects', caption: 'tool row, answer drop-in, approval arming, receipt seal, ProofFlow, closing line', ms: FX_MS,
|
|
164
|
+
frame: (t, o) => fxLines(t, Math.min(W - 2, 78), o.mono) },
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
export const SCENE_IDS = ['orbit', 'portal', 'particle', 'eye', 'hello', 'perpetual', 'ring', 'fx'];
|
|
168
|
+
|
|
169
|
+
// ---- runner (the only impure part) ---------------------------------------------------------------------------
|
|
170
|
+
const visible = (s) => s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ''); // eslint-disable-line no-control-regex
|
|
171
|
+
const width = (s) => [...visible(s)].length;
|
|
172
|
+
|
|
173
|
+
/** One full screen for a scene frame: title + caption on top, the frame centred under it. */
|
|
174
|
+
export function screenFor(scene, t, { cols, mono, index, total }) {
|
|
175
|
+
const lines = scene.frame(t, { mono });
|
|
176
|
+
const head = paint(`[${index + 1}/${total}] ${scene.title}`, SKY, mono);
|
|
177
|
+
const cap = wrap(scene.caption, Math.max(20, cols - 2)).map((l) => paint(l, MUTED, mono));
|
|
178
|
+
const w = Math.max(0, ...lines.map(width));
|
|
179
|
+
const left = ' '.repeat(Math.max(0, Math.floor((cols - w) / 2)));
|
|
180
|
+
return [head, ...cap, '', ...lines.map((l) => left + l)];
|
|
181
|
+
}
|
|
182
|
+
function wrap(s, n) { const o = []; let l = ''; for (const w of s.split(' ')) { if ((l + ' ' + w).trim().length > n) { o.push(l); l = w; } else l = (l + ' ' + w).trim(); } if (l) o.push(l); return o; }
|
|
183
|
+
|
|
184
|
+
export async function runGallery({ out = process.stdout, env = process.env, only = [], narrow = false, slow = false, mono: monoFlag = false } = {}) {
|
|
185
|
+
const cols = narrow ? 40 : Math.max(40, out.columns || 100), rows = out.rows || 30;
|
|
186
|
+
const mono = monoFlag || Boolean(env.NO_COLOR) || env.TERM === 'dumb';
|
|
187
|
+
const still = env.QUOX_REDUCED_MOTION === '1' || !out.isTTY;
|
|
188
|
+
const all = galleryScenes({ cols, rows, narrow });
|
|
189
|
+
const bad = only.filter((n) => !SCENE_IDS.includes(n));
|
|
190
|
+
if (bad.length) throw new Error(`unknown motion: ${bad.join(', ')} (one of ${SCENE_IDS.join(', ')})`);
|
|
191
|
+
const scenes = only.length ? all.filter((s) => only.includes(s.id)) : all;
|
|
192
|
+
if (still) {
|
|
193
|
+
// Reduced motion or piped: every scene's settled frame, printed once.
|
|
194
|
+
scenes.forEach((s, i) => out.write(screenFor(s, s.ms, { cols, mono, index: i, total: scenes.length }).join('\n') + '\n\n'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const SLOW = slow ? 3 : 1;
|
|
198
|
+
let restored = false;
|
|
199
|
+
const restore = () => { if (!restored) { restored = true; out.write(RESET + '\x1b[?25h\n'); } };
|
|
200
|
+
const onSig = () => { restore(); process.exit(130); };
|
|
201
|
+
process.once('SIGINT', onSig); process.once('SIGTERM', onSig);
|
|
202
|
+
out.write('\x1b[?25l');
|
|
203
|
+
try {
|
|
204
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
205
|
+
const s = scenes[i];
|
|
206
|
+
out.write('\x1b[2J\x1b[H');
|
|
207
|
+
const t0 = performance.now();
|
|
208
|
+
for (let n = 0; ; n++) {
|
|
209
|
+
const t = (performance.now() - t0) / SLOW;
|
|
210
|
+
const done = t >= s.ms;
|
|
211
|
+
const lines = screenFor(s, done ? s.ms : t, { cols, mono, index: i, total: scenes.length });
|
|
212
|
+
out.write('\x1b[?2026h\x1b[H' + lines.map((l) => l + '\x1b[K').join('\n') + '\x1b[J\x1b[?2026l');
|
|
213
|
+
if (done) break;
|
|
214
|
+
const wait = t0 + (n + 1) * (1000 / 30) - performance.now();
|
|
215
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
216
|
+
}
|
|
217
|
+
await new Promise((r) => setTimeout(r, 700 * SLOW));
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
process.off('SIGINT', onSig); process.off('SIGTERM', onSig);
|
|
221
|
+
restore();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perpetual status indicator model (CLI-QUOX): a one-cell Q whose COLOUR carries state.
|
|
3
|
+
* resting dim sky, slow breathing working sky <-> violet shimmer (katakana rain beside it)
|
|
4
|
+
* waiting amber pulse (needs approval) failing red, static
|
|
5
|
+
* The glyph is Ⓠ (U+24C0, the documented Quox stand-in): compared in xterm.js against `◖Q`, `Q` and `◖Q◗`
|
|
6
|
+
* it was the only one that reads as a Q at a glance in every colour; the bare Q is lost beside text and
|
|
7
|
+
* the half-circle forms look like a bullet. Pure functions, tick t advances every 120 ms.
|
|
8
|
+
*/
|
|
9
|
+
export const Q_GLYPH = 'Ⓠ';
|
|
10
|
+
export const STATES = ['resting', 'working', 'waiting', 'failing'];
|
|
11
|
+
const SKY = [56, 189, 248], VIOLET = [168, 85, 247], AMBER = [245, 158, 11], RED = [239, 68, 68];
|
|
12
|
+
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
|
|
13
|
+
const tri = (t, period) => { const k = (t % period) / period; return k < 0.5 ? k * 2 : 2 - k * 2; };
|
|
14
|
+
|
|
15
|
+
/** working / waiting / failing from what the app knows; Working is only mounted while a turn runs. */
|
|
16
|
+
export function indicatorState(activity, failing = false) {
|
|
17
|
+
if (failing) return 'failing';
|
|
18
|
+
if (/^waiting for approval/i.test(String(activity || ''))) return 'waiting';
|
|
19
|
+
return 'working';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** rgb for the Q at tick t. quiet (reduced motion) freezes the animation at a representative colour. */
|
|
23
|
+
export function qColor(state, t = 0, quiet = false) {
|
|
24
|
+
switch (state) {
|
|
25
|
+
case 'resting': return mix([40, 70, 90], SKY, quiet ? 0.5 : 0.25 + 0.5 * tri(t, 20));
|
|
26
|
+
case 'waiting': return mix([120, 76, 6], AMBER, quiet ? 1 : 0.3 + 0.7 * tri(t, 10));
|
|
27
|
+
case 'failing': return RED;
|
|
28
|
+
default: return mix(SKY, VIOLET, quiet ? 0.5 : tri(t, 16));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The word shown for a state when the activity text is not the whole story. */
|
|
33
|
+
export function stateWord(state, activity) {
|
|
34
|
+
if (state === 'failing') return 'Failed';
|
|
35
|
+
if (state === 'resting') return 'Ready';
|
|
36
|
+
const a = String(activity || '').replace(/[.…\s]+$/u, '');
|
|
37
|
+
return !a || a === 'Working' ? 'Thinking' : a;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 842 -> "842", 1234 -> "1.2k", 12400 -> "12k". */
|
|
41
|
+
export function fmtTokens(n) {
|
|
42
|
+
n = Math.max(0, Math.round(Number(n) || 0));
|
|
43
|
+
if (n < 1000) return String(n);
|
|
44
|
+
const k = n / 1000;
|
|
45
|
+
return k < 10 ? `${k.toFixed(1).replace(/\.0$/, '')}k` : `${Math.round(k)}k`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** USD for the closing line: undefined (omit) when absent, zero or not a number; never invented. */
|
|
49
|
+
export function fmtCost(usd) {
|
|
50
|
+
const v = Number(usd);
|
|
51
|
+
if (!Number.isFinite(v) || v <= 0) return null;
|
|
52
|
+
return `$${v < 0.01 ? v.toFixed(4) : v.toFixed(2)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalise a server usage object. Real numbers only: { tokens, cost } with null for anything missing.
|
|
57
|
+
* tokens = output tokens (what streamed back); falls back to input+output only when no split is given.
|
|
58
|
+
*/
|
|
59
|
+
export function normUsage(u, doneCost) {
|
|
60
|
+
const o = (u && typeof u === 'object') ? u : {};
|
|
61
|
+
const out = o.output_tokens ?? o.completion_tokens;
|
|
62
|
+
const inp = o.input_tokens ?? o.prompt_tokens;
|
|
63
|
+
const total = o.total_tokens;
|
|
64
|
+
const tokens = Number.isFinite(Number(out)) && out != null ? Number(out)
|
|
65
|
+
: Number.isFinite(Number(total)) && total != null ? Number(total)
|
|
66
|
+
: Number.isFinite(Number(inp)) && inp != null ? Number(inp) : null;
|
|
67
|
+
const cost = Number(o.cost_usd ?? doneCost);
|
|
68
|
+
return { tokens, cost: Number.isFinite(cost) && cost > 0 ? cost : null, real: tokens != null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Live token figure: the real number once the server reported it, else an estimate from received text (~chars/4). */
|
|
72
|
+
export function turnTokens(turn) {
|
|
73
|
+
if (!turn) return 0;
|
|
74
|
+
if (turn.usage?.real) return turn.usage.tokens;
|
|
75
|
+
return Math.ceil((turn.chars || 0) / 4);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Glyphs that stand in for letters while the word "decrypts" (owner 2026-10-12: far more variety). Half-width
|
|
80
|
+
* katakana only: one cell each, the width of a Latin letter, so the word never jitters. 42 of them: the whole
|
|
81
|
+
* U+FF66..U+FF9D block minus forms that read as a Latin letter, digit, bracket or bare box/stroke in DejaVu
|
|
82
|
+
* Sans Mono fallback (ェエ like I/E, ュョユヨ boxes, ニ "=", ノ "/", フ "7", ヘ "^", リ "U", レ "L", ロ a box, ー a dash)
|
|
83
|
+
* and the voiced marks ゙ ゚.
|
|
84
|
+
*/
|
|
85
|
+
export const SCRAMBLE_GLYPHS = 'ヲァィゥォャッアイウオカキクケコサシスセソタチツテトナヌネハヒホマミムメモヤラルワン';
|
|
86
|
+
|
|
87
|
+
/** Deterministic hash in [0,1). */
|
|
88
|
+
const hsh = (a, b, c = 0) => { let x = (a * 374761393 + b * 668265263 + c * 2147483647) | 0; x = Math.imul(x ^ (x >>> 13), 1274126177); return ((x ^ (x >>> 16)) >>> 0) / 4294967296; };
|
|
89
|
+
|
|
90
|
+
/** Per-cell flicker period (ms), 60..180, so cells never pulse in lockstep. */
|
|
91
|
+
export const cellPeriod = (i, seed = 0) => 60 + Math.floor(hsh(i, 911, seed) * 121);
|
|
92
|
+
export const SCRAMBLE_CHANCE = 0.5; // x mean cascade 3/5 of an episode -> ~30% of letters scrambled at any time
|
|
93
|
+
const EPISODE = 5; // an episode is 5 flicker periods: a cascade of 2..4 glyphs, then the letter
|
|
94
|
+
const FLASH_MS = 90; // one frame of bright flash when a cell resolves
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Glyph for cell i in flicker bucket k. Even and odd buckets draw from disjoint halves of the set, so a cell
|
|
98
|
+
* never shows the same glyph twice in a row.
|
|
99
|
+
*/
|
|
100
|
+
export function cellGlyph(i, k, seed = 0) {
|
|
101
|
+
const half = Math.floor(SCRAMBLE_GLYPHS.length / 2);
|
|
102
|
+
const odd = ((k % 2) + 2) % 2;
|
|
103
|
+
return SCRAMBLE_GLYPHS[odd * half + Math.floor(hsh(i, k, seed) * half)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The activity word at time `now` (ms): "Matrix rain through the word". Pure. Returns [{ ch, glyph, flash }].
|
|
108
|
+
* decode-in: a word that just changed (at `since`) resolves left to right over DECODE_MS (unresolved = glyph)
|
|
109
|
+
* steady: each letter runs its own clock (cellPeriod); per episode of 4 periods it is either plain or
|
|
110
|
+
* cascades through 2..4 different glyphs and resolves with one bright flash frame
|
|
111
|
+
* Spaces are never scrambled; every cell is one column, so the width never changes.
|
|
112
|
+
*/
|
|
113
|
+
export function wordFrame(word, now, { since = -Infinity, seed = 0 } = {}) {
|
|
114
|
+
const chars = [...String(word)];
|
|
115
|
+
const letters = chars.filter(ch => ch.trim()).length;
|
|
116
|
+
let li = -1;
|
|
117
|
+
return chars.map((ch, i) => {
|
|
118
|
+
if (!ch.trim()) return { ch, glyph: false, flash: false };
|
|
119
|
+
li++;
|
|
120
|
+
const P = cellPeriod(i, seed), phase = Math.floor(hsh(i, 313, seed) * P * EPISODE);
|
|
121
|
+
const tt = now + phase, k = Math.floor(tt / P);
|
|
122
|
+
const decodeAt = since + ((li + 1) / letters) * DECODE_MS;
|
|
123
|
+
if (now < decodeAt) return { ch: cellGlyph(i, k, seed), glyph: true, flash: false };
|
|
124
|
+
if (now - decodeAt < FLASH_MS) return { ch, glyph: false, flash: true }; // resolve flash on decode-in too
|
|
125
|
+
if (now < since + DECODE_MS + 200) return { ch, glyph: false, flash: false }; // a fresh word reads cleanly first
|
|
126
|
+
const E = Math.floor(k / EPISODE), pos = k - E * EPISODE;
|
|
127
|
+
const cascade = (e) => (hsh(i, e, seed + 17) < SCRAMBLE_CHANCE ? 2 + Math.floor(hsh(i, e, seed + 29) * 3) : 0);
|
|
128
|
+
const m = cascade(E);
|
|
129
|
+
if (pos < m) return { ch: cellGlyph(i, k, seed), glyph: true, flash: false };
|
|
130
|
+
const resolvedAt = (E * EPISODE + m) * P;
|
|
131
|
+
if (m > 0 && pos === m && tt - resolvedAt < Math.min(P, FLASH_MS)) return { ch, glyph: false, flash: true };
|
|
132
|
+
return { ch, glyph: false, flash: false };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Gradient stops per state for the coloured word (same families as the composer ring). */
|
|
137
|
+
export const WORD_PALETTES = {
|
|
138
|
+
working: [SKY, VIOLET, [6, 182, 212]],
|
|
139
|
+
writing: [[34, 197, 94], [74, 222, 128], [20, 184, 166]],
|
|
140
|
+
waiting: [AMBER, [234, 88, 12], [251, 191, 36]],
|
|
141
|
+
failing: [RED, RED, RED],
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Colour of a word cell: one gradient sweeping across the word over time (~1.6 s per pass). Resolved letters
|
|
145
|
+
* are slightly brighter than glyphs; a flash is near white. quiet (reduced motion) freezes the sweep.
|
|
146
|
+
*/
|
|
147
|
+
export function wordColor(state, i, n, now, kind = 'letter', quiet = false) {
|
|
148
|
+
const pal = WORD_PALETTES[state] || WORD_PALETTES.working;
|
|
149
|
+
const u = ((((n > 1 ? i / (n - 1) : 0) * 0.65 - (quiet ? 0 : now / 1600)) % 1) + 1) % 1;
|
|
150
|
+
const x = u * pal.length, a = Math.floor(x) % pal.length, b = (a + 1) % pal.length;
|
|
151
|
+
const base = mix(pal[a], pal[b], x - Math.floor(x));
|
|
152
|
+
if (kind === 'flash') return mix(base, [255, 255, 255], 0.7);
|
|
153
|
+
if (kind === 'glyph') return mix(base, [30, 36, 48], 0.28);
|
|
154
|
+
return mix(base, [255, 255, 255], 0.12);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Previous model (kept for the drop-in reveal and existing callers): one frame with a random 30-45% of the
|
|
159
|
+
* letter positions scrambled. Returns [{ ch, glyph }]. rand is injectable for tests.
|
|
160
|
+
*/
|
|
161
|
+
export function scrambleFrame(word, rand = Math.random) {
|
|
162
|
+
const chars = [...String(word)];
|
|
163
|
+
const idx = chars.map((c, i) => (c.trim() ? i : -1)).filter((i) => i >= 0);
|
|
164
|
+
const chosen = new Set();
|
|
165
|
+
if (idx.length > 1) {
|
|
166
|
+
const k = Math.min(idx.length - 1, Math.max(1, Math.round(idx.length * (0.3 + 0.15 * rand()))));
|
|
167
|
+
const pool = [...idx];
|
|
168
|
+
while (chosen.size < k) chosen.add(pool.splice(Math.floor(rand() * pool.length), 1)[0]);
|
|
169
|
+
}
|
|
170
|
+
return chars.map((ch, i) => (chosen.has(i) ? { ch: SCRAMBLE_GLYPHS[Math.floor(rand() * SCRAMBLE_GLYPHS.length)], glyph: true } : { ch, glyph: false }));
|
|
171
|
+
}
|
|
172
|
+
/** Shimmer colour for the glyph at position i, tick t (sky to violet). */
|
|
173
|
+
export const glyphColor = (i, t) => mix(SKY, VIOLET, tri(t + i * 2, 16));
|
|
174
|
+
|
|
175
|
+
export const DECODE_MS = 300;
|
|
176
|
+
/**
|
|
177
|
+
* A word that just changed decodes in: letters resolve left to right over DECODE_MS. Unresolved positions
|
|
178
|
+
* are always glyphs; resolved ones follow the normal 30-45% scramble. progress 0..1 (>=1 = steady state).
|
|
179
|
+
*/
|
|
180
|
+
export function decodeFrame(word, progress, rand = Math.random) {
|
|
181
|
+
const base = scrambleFrame(word, rand);
|
|
182
|
+
if (progress >= 1) return base;
|
|
183
|
+
const n = base.filter((g) => g.ch.trim()).length;
|
|
184
|
+
let seen = 0;
|
|
185
|
+
const revealed = Math.floor(clamp01(progress) * n);
|
|
186
|
+
return base.map((g) => {
|
|
187
|
+
if (!g.ch.trim()) return g;
|
|
188
|
+
const i = seen++;
|
|
189
|
+
if (i < revealed) return g;
|
|
190
|
+
return g.glyph ? g : { ch: SCRAMBLE_GLYPHS[Math.floor(rand() * SCRAMBLE_GLYPHS.length)], glyph: true };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local intent hints. When a message is plainly a client command in words, the turn is NOT intercepted
|
|
3
|
+
* (the model still answers); the client only prints one dim `tip: /command` line above the answer.
|
|
4
|
+
* Exact, anchored patterns on the whole message: anything with extra words is ordinary chat.
|
|
5
|
+
*/
|
|
6
|
+
const WORD = '([A-Za-z0-9][\\w.-]{0,39})';
|
|
7
|
+
// Questions answered by a local panel (/context, /cost, /status): the panel prints above the answer.
|
|
8
|
+
const PANELS = [
|
|
9
|
+
[/^(?:how much (?:of (?:the|my|our) )?context (?:have (?:we|i) used|has been used|is (?:used|left)|are we using|am i using|do (?:we|i) have left)|what(?:'s| is) (?:the|my|our) context(?: usage)?|context usage)$/i, '/context'],
|
|
10
|
+
[/^(?:what(?:'s| is) (?:this|it) costing(?: (?:me|us))?|how much (?:has|did) (?:this|it) cost(?: (?:me|us))?(?: so far)?|how much (?:have (?:i|we)|am i|are we) spen(?:t|ding)(?: so far)?|what(?:'s| is| has been) (?:the|my|our) (?:cost|spend|usage)(?: so far)?)$/i, '/cost'],
|
|
11
|
+
[/^(?:what(?:'s| is) my status|what(?:'s| is) the status|show (?:me )?my status|status)$/i, '/status']
|
|
12
|
+
];
|
|
13
|
+
function PANEL_RULES() { return PANELS.map(([re, cmd]) => [re, () => cmd]); }
|
|
14
|
+
|
|
15
|
+
const RULES = [
|
|
16
|
+
[new RegExp(`^attach(?: me)?(?: to)? ${WORD}$`, 'i'), (m) => `/attach ${m[1]}`],
|
|
17
|
+
[/^rename (?:this|the|my) (?:session|conversation|chat) (?:to|as) ([\w.-]+(?: [\w.-]+){0,2})$/i, (m) => m[1].split(' ').some(w => /^(and|then|but|so|please)$/i.test(w)) ? null : `/rename ${m[1]}`],
|
|
18
|
+
[/^(?:what(?:'s| is) running|what is running now)$/i, () => '/roster'],
|
|
19
|
+
[/^(?:show|list|open) (?:my |the )?(?:pending )?approvals$/i, () => '/approvals'],
|
|
20
|
+
[/^(?:what(?:'s| is)|show me what(?:'s| is)) (?:waiting|pending) (?:for )?(?:my )?approval$/i, () => '/approvals'],
|
|
21
|
+
[/^detach(?: from (?:the )?agent)?$/i, () => '/detach'],
|
|
22
|
+
[/^(?:start )?(?:a )?new conversation$/i, () => '/new'],
|
|
23
|
+
[/^(?:please )?(?:compact|summari[sz]e and compact|compress) (?:this|the|our|my) (?:conversation|chat|session|context)(?: please)?$/i, () => '/compact'],
|
|
24
|
+
...PANEL_RULES()
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** -> `tip: /attach seer` or null. */
|
|
28
|
+
export function intentHint(text) {
|
|
29
|
+
const t = String(text || '').trim().replace(/[.?!]+$/, '').trim();
|
|
30
|
+
if (!t || t.includes('\n') || t.startsWith('/')) return null;
|
|
31
|
+
for (const [re, fmt] of RULES) { const m = re.exec(t); const r = m && fmt(m); if (r) return `tip: ${r}`; }
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** -> '/context' | '/cost' | '/status' when the message asks a question a local panel answers, else null. */
|
|
36
|
+
export function intentPanel(text) {
|
|
37
|
+
const t = String(text || '').trim().replace(/[.?!]+$/, '').trim();
|
|
38
|
+
if (!t || t.includes('\n') || t.startsWith('/')) return null;
|
|
39
|
+
for (const [re, cmd] of PANELS) if (re.test(t)) return cmd;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw stdin tokeniser for the QUOXON composer. We own input parsing (instead of
|
|
3
|
+
* Ink's useInput) because we need bracketed paste, Shift+Enter, Ctrl+_ and a
|
|
4
|
+
* paste-burst heuristic, none of which Ink 6 exposes.
|
|
5
|
+
*
|
|
6
|
+
* parseChunk(chunk, state) -> { events, state }
|
|
7
|
+
* Events: { type: 'text', text } | { type: 'paste', text } | { type: 'key', name, ctrl?, alt? } | { type: 'focus', focused }
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const PASTE_ON = '\x1b[?2004h';
|
|
11
|
+
export const PASTE_OFF = '\x1b[?2004l';
|
|
12
|
+
const PASTE_START = '\x1b[200~';
|
|
13
|
+
const PASTE_END = '\x1b[201~';
|
|
14
|
+
|
|
15
|
+
export const initialKeyState = () => ({ inPaste: false, pasteBuf: '', partial: '' });
|
|
16
|
+
/** Time after which a dangling paste start / partial marker is flushed (app owns the timer). */
|
|
17
|
+
export const PASTE_DANGLING_MS = 1500;
|
|
18
|
+
/** Non-bracketed input arriving within this many ms of the previous chunk is a paste burst. */
|
|
19
|
+
export const BURST_MS = 20;
|
|
20
|
+
|
|
21
|
+
/** True when the key state holds something a timer must eventually flush. */
|
|
22
|
+
export const hasDangling = (st) => Boolean(st?.inPaste || st?.partial);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flush a dangling paste start or half marker after a timeout. Buffered paste text becomes a PASTE
|
|
26
|
+
* event (inserted, never submitted); a half marker is dropped. Never produces Enter.
|
|
27
|
+
*/
|
|
28
|
+
export function flushDangling(state) {
|
|
29
|
+
const events = [];
|
|
30
|
+
if (state?.inPaste && state.pasteBuf) {
|
|
31
|
+
let t = state.pasteBuf;
|
|
32
|
+
for (let n = Math.min(PASTE_END.length - 1, t.length); n > 0; n--) { if (t.endsWith(PASTE_END.slice(0, n))) { t = t.slice(0, -n); break; } }
|
|
33
|
+
if (t) events.push({ type: 'paste', text: t });
|
|
34
|
+
}
|
|
35
|
+
return { events, state: initialKeyState() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CSI_KEYS = {
|
|
39
|
+
'\x1b[A': 'up', '\x1b[B': 'down', '\x1b[C': 'right', '\x1b[D': 'left',
|
|
40
|
+
'\x1bOA': 'up', '\x1bOB': 'down', '\x1bOC': 'right', '\x1bOD': 'left',
|
|
41
|
+
'\x1b[H': 'home', '\x1b[F': 'end', '\x1bOH': 'home', '\x1bOF': 'end',
|
|
42
|
+
'\x1b[1~': 'home', '\x1b[4~': 'end', '\x1b[7~': 'home', '\x1b[8~': 'end',
|
|
43
|
+
'\x1b[3~': 'delete', '\x1b[Z': 'shift-tab',
|
|
44
|
+
'\x1b[13;2u': 'shift-enter', '\x1b[27;2;13~': 'shift-enter', '\x1b\r': 'shift-enter', '\x1b\n': 'shift-enter',
|
|
45
|
+
'\x1b[1;5D': 'word-left', '\x1b[1;5C': 'word-right', '\x1b[1;3D': 'word-left', '\x1b[1;3C': 'word-right'
|
|
46
|
+
};
|
|
47
|
+
const CSI_LIST = Object.keys(CSI_KEYS).sort((a, b) => b.length - a.length);
|
|
48
|
+
|
|
49
|
+
const CTRL = {
|
|
50
|
+
'\x01': 'home', '\x05': 'end', '\x03': 'ctrl-c', '\x04': 'ctrl-d', '\x07': 'ctrl-g',
|
|
51
|
+
'\x0f': 'ctrl-o', '\x13': 'ctrl-s', '\x15': 'kill-line', '\x17': 'delete-word',
|
|
52
|
+
'\x1f': 'undo', '\x19': 'redo', '\x0a': 'newline', '\x7f': 'backspace', '\x08': 'backspace',
|
|
53
|
+
'\r': 'enter', '\t': 'tab', '\x02': 'left', '\x06': 'right'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function parseChunk(chunk, state = initialKeyState()) {
|
|
57
|
+
const events = [];
|
|
58
|
+
let s = String(chunk);
|
|
59
|
+
let st = { partial: '', ...state };
|
|
60
|
+
if (st.partial) { s = st.partial + s; st.partial = ''; }
|
|
61
|
+
|
|
62
|
+
while (s.length) {
|
|
63
|
+
if (st.inPaste) {
|
|
64
|
+
// search the joined buffer so an end marker split across chunks is still found
|
|
65
|
+
const buf = st.pasteBuf + s;
|
|
66
|
+
const end = buf.indexOf(PASTE_END);
|
|
67
|
+
if (end === -1) { st.pasteBuf = buf; s = ''; break; }
|
|
68
|
+
events.push({ type: 'paste', text: buf.slice(0, end) });
|
|
69
|
+
st = { inPaste: false, pasteBuf: '', partial: '' };
|
|
70
|
+
s = buf.slice(end + PASTE_END.length);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (s.startsWith(PASTE_START)) { st = { inPaste: true, pasteBuf: '', partial: '' }; s = s.slice(PASTE_START.length); continue; }
|
|
74
|
+
// a half-received start marker (ESC [ 2 0 ...): hold it for the next chunk, never read it as Esc + text
|
|
75
|
+
if (s.length >= 2 && s.length < PASTE_START.length && PASTE_START.startsWith(s)) { st.partial = s; s = ''; break; }
|
|
76
|
+
if (s === '\x1b') { events.push({ type: 'key', name: 'escape' }); s = ''; continue; }
|
|
77
|
+
if (s[0] === '\x1b') {
|
|
78
|
+
// Focus reporting (CSI ?1004h, enabled by sound/index.js): CSI I = focus in, CSI O = focus out.
|
|
79
|
+
if (s.startsWith('\x1b[I') || s.startsWith('\x1b[O')) { events.push({ type: 'focus', focused: s[2] === 'I' }); s = s.slice(3); continue; }
|
|
80
|
+
const hit = CSI_LIST.find(k => s.startsWith(k));
|
|
81
|
+
if (hit) { events.push({ type: 'key', name: CSI_KEYS[hit] }); s = s.slice(hit.length); continue; }
|
|
82
|
+
// Unknown escape sequence: swallow ESC [ ... final byte, or a bare ESC+char (alt-x).
|
|
83
|
+
const m = /^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(s) || /^\x1bO./.exec(s);
|
|
84
|
+
if (m) { s = s.slice(m[0].length); continue; }
|
|
85
|
+
if (s.length >= 2) { events.push({ type: 'key', name: 'escape' }); s = s.slice(1); continue; }
|
|
86
|
+
}
|
|
87
|
+
// Plain run: printable text up to the next control char.
|
|
88
|
+
// Burst heuristic: text plus newlines delivered in ONE chunk, with content after
|
|
89
|
+
// the first newline, is a paste (terminal without bracketed paste), not typing.
|
|
90
|
+
const run = /^[^\x00-\x1f\x7f]+/.exec(s);
|
|
91
|
+
if (run) { events.push({ type: 'text', text: run[0] }); s = s.slice(run[0].length); continue; }
|
|
92
|
+
const c = s[0];
|
|
93
|
+
if (CTRL[c]) { events.push({ type: 'key', name: CTRL[c] }); }
|
|
94
|
+
s = s.slice(1);
|
|
95
|
+
}
|
|
96
|
+
return { events: coalesceBurst(events), state: st };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* If a chunk produced text and then an Enter/newline FOLLOWED by more text, it is
|
|
101
|
+
* a non-bracketed paste: fold it into one paste event. A trailing single Enter
|
|
102
|
+
* (text then Enter at the end) stays as text + enter so fast typing still submits.
|
|
103
|
+
*/
|
|
104
|
+
function coalesceBurst(events) {
|
|
105
|
+
const hasInner = events.some((e, i) =>
|
|
106
|
+
e.type === 'key' && (e.name === 'enter' || e.name === 'newline') && i > 0 && i < events.length - 1 &&
|
|
107
|
+
events.slice(i + 1).some(x => x.type === 'text'));
|
|
108
|
+
if (!hasInner) return events;
|
|
109
|
+
let text = '';
|
|
110
|
+
const rest = [];
|
|
111
|
+
for (let i = 0; i < events.length; i++) {
|
|
112
|
+
const e = events[i];
|
|
113
|
+
if (e.type === 'text') text += e.text;
|
|
114
|
+
else if (e.type === 'key' && (e.name === 'enter' || e.name === 'newline')) {
|
|
115
|
+
// a trailing Enter after the burst is a submit, not part of the paste
|
|
116
|
+
if (i === events.length - 1) rest.push(e); else text += '\n';
|
|
117
|
+
} else if (e.type === 'key' && e.name === 'tab') text += '\t';
|
|
118
|
+
else rest.push(e);
|
|
119
|
+
}
|
|
120
|
+
return [{ type: 'paste', text }, ...rest];
|
|
121
|
+
}
|