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.
Files changed (327) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/admin.js +26 -0
  5. package/lib/api/endpoints/agent-schedules.js +26 -0
  6. package/lib/api/endpoints/agents.js +7 -0
  7. package/lib/api/endpoints/assistants.js +12 -0
  8. package/lib/api/endpoints/away.js +15 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/contactsq.js +2 -0
  11. package/lib/api/endpoints/conversations.js +14 -0
  12. package/lib/api/endpoints/executions.js +13 -0
  13. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  14. package/lib/api/endpoints/files.js +14 -0
  15. package/lib/api/endpoints/hat.js +30 -0
  16. package/lib/api/endpoints/inbox.js +20 -0
  17. package/lib/api/endpoints/index.js +14 -0
  18. package/lib/api/endpoints/memory.js +19 -0
  19. package/lib/api/endpoints/mind.js +13 -0
  20. package/lib/api/endpoints/mon.js +6 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/plugins.js +18 -0
  23. package/lib/api/endpoints/presence.js +33 -0
  24. package/lib/api/endpoints/quoxplan.js +7 -0
  25. package/lib/api/endpoints/quoxseo.js +6 -0
  26. package/lib/api/endpoints/router.js +17 -0
  27. package/lib/api/endpoints/safety.js +7 -0
  28. package/lib/api/endpoints/skills.js +20 -0
  29. package/lib/api/endpoints/stepup.js +20 -0
  30. package/lib/api/endpoints/teams.js +2 -2
  31. package/lib/api/endpoints/triggers.js +26 -0
  32. package/lib/api/endpoints/v1.js +86 -0
  33. package/lib/api/endpoints/ward.js +7 -0
  34. package/lib/api/endpoints/workflows.js +7 -0
  35. package/lib/api/endpoints/x.js +8 -0
  36. package/lib/commands/actions.js +61 -0
  37. package/lib/commands/admin.js +16 -0
  38. package/lib/commands/agent.js +163 -27
  39. package/lib/commands/agentAttach.js +94 -6
  40. package/lib/commands/approvals.js +73 -1
  41. package/lib/commands/aside.js +24 -0
  42. package/lib/commands/ask.js +144 -0
  43. package/lib/commands/assistant.js +59 -4
  44. package/lib/commands/auth.js +8 -2
  45. package/lib/commands/backup.js +8 -4
  46. package/lib/commands/brain.js +105 -14
  47. package/lib/commands/build.js +19 -5
  48. package/lib/commands/chat.js +57 -2
  49. package/lib/commands/claude.js +34 -8
  50. package/lib/commands/contacts.js +49 -0
  51. package/lib/commands/conversation.js +35 -4
  52. package/lib/commands/dashboard.js +43 -0
  53. package/lib/commands/department.js +28 -7
  54. package/lib/commands/devResident.js +15 -3
  55. package/lib/commands/devSupervisor.js +13 -1
  56. package/lib/commands/discord.js +75 -39
  57. package/lib/commands/done.js +98 -28
  58. package/lib/commands/engagement.js +17 -6
  59. package/lib/commands/fetchAllowlist.js +71 -0
  60. package/lib/commands/file.js +38 -1
  61. package/lib/commands/fleet.js +311 -4
  62. package/lib/commands/governance.js +12 -3
  63. package/lib/commands/gripe.js +44 -13
  64. package/lib/commands/hat.js +178 -9
  65. package/lib/commands/helpers.js +18 -1
  66. package/lib/commands/hyperion.js +58 -0
  67. package/lib/commands/inbox.js +74 -23
  68. package/lib/commands/licence.js +14 -155
  69. package/lib/commands/lucid.js +10 -4
  70. package/lib/commands/memory.js +85 -8
  71. package/lib/commands/mind.js +71 -0
  72. package/lib/commands/mon.js +69 -2
  73. package/lib/commands/monitoring.js +1 -1
  74. package/lib/commands/motion.js +2 -2
  75. package/lib/commands/nucleus.js +48 -17
  76. package/lib/commands/objective.js +11 -0
  77. package/lib/commands/offload.js +89 -0
  78. package/lib/commands/onboard.js +2 -1
  79. package/lib/commands/org.js +11 -1
  80. package/lib/commands/peek.js +28 -0
  81. package/lib/commands/plan.js +38 -0
  82. package/lib/commands/plugin.js +110 -0
  83. package/lib/commands/presence.js +89 -0
  84. package/lib/commands/provider.js +159 -0
  85. package/lib/commands/qlarity.js +19 -3
  86. package/lib/commands/quoxchat.js +13 -4
  87. package/lib/commands/quoxseo.js +38 -0
  88. package/lib/commands/reach.js +2 -1
  89. package/lib/commands/router.js +77 -0
  90. package/lib/commands/rows.js +358 -0
  91. package/lib/commands/run.js +64 -9
  92. package/lib/commands/server.js +292 -0
  93. package/lib/commands/setup.js +33 -7
  94. package/lib/commands/skills.js +48 -5
  95. package/lib/commands/slot.js +260 -7
  96. package/lib/commands/status.js +1 -1
  97. package/lib/commands/team.js +3 -1
  98. package/lib/commands/tips.js +23 -0
  99. package/lib/commands/tool.js +133 -45
  100. package/lib/commands/trace.js +12 -7
  101. package/lib/commands/trigger.js +95 -0
  102. package/lib/commands/update.js +46 -0
  103. package/lib/commands/vault.js +31 -8
  104. package/lib/commands/vm.js +237 -0
  105. package/lib/commands/voice.js +34 -3
  106. package/lib/commands/volt.js +45 -3
  107. package/lib/commands/walkthrough.js +32 -0
  108. package/lib/commands/watch.js +73 -21
  109. package/lib/commands/welcome.js +14 -0
  110. package/lib/commands/workflow.js +189 -32
  111. package/lib/commands/x.js +13 -4
  112. package/lib/core/actingAs.js +2 -2
  113. package/lib/core/activeSummary.js +186 -0
  114. package/lib/core/aside.js +54 -0
  115. package/lib/core/askDecision.js +60 -0
  116. package/lib/core/auth.js +164 -13
  117. package/lib/core/callerSource.js +1 -1
  118. package/lib/core/cliBuild.js +62 -0
  119. package/lib/core/client.js +144 -35
  120. package/lib/core/commandRegistrars.js +121 -0
  121. package/lib/core/config.js +142 -24
  122. package/lib/core/customHats.js +168 -0
  123. package/lib/core/doneEngineFetch.js +8 -0
  124. package/lib/core/errors.js +329 -20
  125. package/lib/core/global-flags.js +105 -5
  126. package/lib/core/hyperion.js +102 -0
  127. package/lib/core/inboxResolution.js +65 -0
  128. package/lib/core/lazyCommands.js +4 -1
  129. package/lib/core/offload.js +504 -0
  130. package/lib/core/org-override.js +1 -1
  131. package/lib/core/orgResolve.js +70 -0
  132. package/lib/core/output.js +4 -4
  133. package/lib/core/peek.js +126 -0
  134. package/lib/core/profileUsers.js +51 -0
  135. package/lib/core/promptCancel.js +18 -0
  136. package/lib/core/repoPath.js +11 -0
  137. package/lib/core/residentSeed.js +3 -1
  138. package/lib/core/rootOptions.js +39 -0
  139. package/lib/core/router.js +168 -0
  140. package/lib/core/selfUpdate.js +188 -0
  141. package/lib/core/sessionsOverview.js +64 -0
  142. package/lib/core/slotKeeper.js +248 -3
  143. package/lib/core/slotWake.js +120 -0
  144. package/lib/core/supervisorEngine.js +71 -2
  145. package/lib/core/version.js +1 -1
  146. package/lib/core/vm.js +361 -0
  147. package/lib/core/vmTui.js +86 -0
  148. package/lib/core/walkthrough.js +35 -0
  149. package/lib/hyperion/deck.js +29 -0
  150. package/lib/lucid/LucidCard.jsx +68 -0
  151. package/lib/lucid/LucidIntro.jsx +45 -58
  152. package/lib/lucid/cards.js +86 -0
  153. package/lib/lucid/markdown.js +81 -0
  154. package/lib/lucid/motion.js +56 -0
  155. package/lib/quoxon/activity.js +14 -2
  156. package/lib/quoxon/aocl.js +117 -0
  157. package/lib/quoxon/api.js +504 -25
  158. package/lib/quoxon/app.js +3000 -230
  159. package/lib/quoxon/approvalFacts.js +94 -0
  160. package/lib/quoxon/approvalsView.js +325 -0
  161. package/lib/quoxon/asideView.js +40 -0
  162. package/lib/quoxon/ask.js +121 -0
  163. package/lib/quoxon/away.js +52 -0
  164. package/lib/quoxon/bang.js +224 -0
  165. package/lib/quoxon/bulkRisk.js +17 -0
  166. package/lib/quoxon/capabilities.js +13 -6
  167. package/lib/quoxon/cells.js +53 -0
  168. package/lib/quoxon/commandShort.js +112 -0
  169. package/lib/quoxon/commands.js +363 -38
  170. package/lib/quoxon/connect.js +210 -23
  171. package/lib/quoxon/connectCard.js +19 -2
  172. package/lib/quoxon/consent.js +86 -0
  173. package/lib/quoxon/copy.js +4 -3
  174. package/lib/quoxon/countdownLine.js +11 -0
  175. package/lib/quoxon/demo.autoplay.js +29 -0
  176. package/lib/quoxon/demo.fixtures.js +1 -1
  177. package/lib/quoxon/demo.js +44 -3
  178. package/lib/quoxon/devObserver.js +20 -0
  179. package/lib/quoxon/dividerAocl.js +191 -0
  180. package/lib/quoxon/dividerRule.js +57 -0
  181. package/lib/quoxon/dragonModes.js +150 -0
  182. package/lib/quoxon/dragonView.js +41 -0
  183. package/lib/quoxon/earlyInput.js +40 -0
  184. package/lib/quoxon/echo.js +15 -0
  185. package/lib/quoxon/egg.js +2 -2
  186. package/lib/quoxon/events.js +244 -25
  187. package/lib/quoxon/feedback.js +50 -0
  188. package/lib/quoxon/gallery.js +21 -7
  189. package/lib/quoxon/handoffOffer.js +78 -0
  190. package/lib/quoxon/hatSwitch.js +37 -0
  191. package/lib/quoxon/headless.js +161 -0
  192. package/lib/quoxon/hostUrls.js +41 -0
  193. package/lib/quoxon/indicator.js +6 -2
  194. package/lib/quoxon/intent.js +12 -1
  195. package/lib/quoxon/keys.js +49 -6
  196. package/lib/quoxon/launcher.js +103 -0
  197. package/lib/quoxon/main.js +92 -160
  198. package/lib/quoxon/markdown.js +45 -21
  199. package/lib/quoxon/markdownSettle.js +19 -0
  200. package/lib/quoxon/matrixDomains.json +46 -0
  201. package/lib/quoxon/mentions.js +90 -0
  202. package/lib/quoxon/mind.js +103 -0
  203. package/lib/quoxon/mode.js +46 -0
  204. package/lib/quoxon/model.js +132 -26
  205. package/lib/quoxon/modelPrefs.js +345 -0
  206. package/lib/quoxon/motion/clock.js +3 -2
  207. package/lib/quoxon/motion/level.js +91 -0
  208. package/lib/quoxon/mouse.js +211 -0
  209. package/lib/quoxon/noai.js +57 -0
  210. package/lib/quoxon/objects/away.js +72 -0
  211. package/lib/quoxon/objects/fetch.js +17 -5
  212. package/lib/quoxon/objects/fleet.js +149 -0
  213. package/lib/quoxon/objects/health.js +186 -0
  214. package/lib/quoxon/objects/index.js +24 -3
  215. package/lib/quoxon/objects/mirror.js +137 -0
  216. package/lib/quoxon/objects/peek.js +17 -0
  217. package/lib/quoxon/objects/prove.js +130 -0
  218. package/lib/quoxon/objects/replay.js +125 -0
  219. package/lib/quoxon/objects/sitrep.js +58 -0
  220. package/lib/quoxon/objects/view.js +11 -0
  221. package/lib/quoxon/onboarding/copy.js +32 -4
  222. package/lib/quoxon/onboarding/credential.js +17 -4
  223. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  224. package/lib/quoxon/onboarding/failureCause.js +33 -0
  225. package/lib/quoxon/onboarding/finish.js +15 -0
  226. package/lib/quoxon/onboarding/index.js +34 -7
  227. package/lib/quoxon/onboarding/installRunner.js +64 -5
  228. package/lib/quoxon/onboarding/preflight.js +17 -3
  229. package/lib/quoxon/onboarding/probe.js +65 -0
  230. package/lib/quoxon/onboarding/view.js +76 -19
  231. package/lib/quoxon/orgLabel.js +10 -0
  232. package/lib/quoxon/orgSwitch.js +33 -0
  233. package/lib/quoxon/ownBroadcast.js +22 -0
  234. package/lib/quoxon/palette.js +43 -0
  235. package/lib/quoxon/print.js +9 -5
  236. package/lib/quoxon/profileSwitch.js +53 -0
  237. package/lib/quoxon/promptHistory.js +41 -0
  238. package/lib/quoxon/providers.js +48 -0
  239. package/lib/quoxon/qart.js +20 -51
  240. package/lib/quoxon/qdragon.js +410 -0
  241. package/lib/quoxon/qdragonFly.js +61 -0
  242. package/lib/quoxon/qdragonScenes.js +282 -0
  243. package/lib/quoxon/qeyes.js +213 -0
  244. package/lib/quoxon/qmotion.js +58 -37
  245. package/lib/quoxon/render.js +638 -119
  246. package/lib/quoxon/resend.js +72 -0
  247. package/lib/quoxon/roster.js +95 -9
  248. package/lib/quoxon/route.js +6 -5
  249. package/lib/quoxon/rows.js +711 -0
  250. package/lib/quoxon/rowsStore.js +80 -0
  251. package/lib/quoxon/screenContext.js +150 -0
  252. package/lib/quoxon/screenFrames.js +254 -0
  253. package/lib/quoxon/session.js +172 -2
  254. package/lib/quoxon/sessionDead.js +17 -0
  255. package/lib/quoxon/signin.js +125 -0
  256. package/lib/quoxon/slash/agents.js +112 -0
  257. package/lib/quoxon/slash/argSources.js +65 -0
  258. package/lib/quoxon/slash/args.js +65 -0
  259. package/lib/quoxon/slash/automate.js +89 -0
  260. package/lib/quoxon/slash/broadcast.js +140 -0
  261. package/lib/quoxon/slash/broadcastRun.js +108 -0
  262. package/lib/quoxon/slash/card.js +30 -0
  263. package/lib/quoxon/slash/challenge.js +114 -0
  264. package/lib/quoxon/slash/cliCommand.js +53 -0
  265. package/lib/quoxon/slash/cliLeaves.json +948 -0
  266. package/lib/quoxon/slash/compact.js +1 -1
  267. package/lib/quoxon/slash/confirm.js +30 -0
  268. package/lib/quoxon/slash/crystallize.js +69 -0
  269. package/lib/quoxon/slash/dashboard.js +136 -0
  270. package/lib/quoxon/slash/draft.js +54 -0
  271. package/lib/quoxon/slash/hatWalk.js +206 -0
  272. package/lib/quoxon/slash/inverses.js +77 -0
  273. package/lib/quoxon/slash/killer.js +24 -0
  274. package/lib/quoxon/slash/killerApi.js +16 -0
  275. package/lib/quoxon/slash/ledger.js +13 -8
  276. package/lib/quoxon/slash/listView.js +3 -1
  277. package/lib/quoxon/slash/modelCmd.js +261 -0
  278. package/lib/quoxon/slash/palette.js +21 -7
  279. package/lib/quoxon/slash/paletteController.js +117 -7
  280. package/lib/quoxon/slash/panels.js +21 -6
  281. package/lib/quoxon/slash/picker.js +20 -2
  282. package/lib/quoxon/slash/restart.js +71 -13
  283. package/lib/quoxon/slash/rowCmd.js +157 -0
  284. package/lib/quoxon/slash/rowWalk.js +175 -0
  285. package/lib/quoxon/slash/settings.js +16 -6
  286. package/lib/quoxon/slash/spawn.js +177 -0
  287. package/lib/quoxon/slash/stepUp.js +274 -0
  288. package/lib/quoxon/slash/stepUpCard.js +60 -0
  289. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  290. package/lib/quoxon/slash/undoApi.js +38 -0
  291. package/lib/quoxon/slash/undoCmd.js +49 -0
  292. package/lib/quoxon/slash/undoWish.js +15 -0
  293. package/lib/quoxon/slash/watch.js +164 -0
  294. package/lib/quoxon/slash/wish.js +23 -0
  295. package/lib/quoxon/slash/wishCmd.js +32 -0
  296. package/lib/quoxon/slotChat.js +300 -0
  297. package/lib/quoxon/slotFormat.js +195 -0
  298. package/lib/quoxon/slotRun.js +131 -0
  299. package/lib/quoxon/sound/mapping.js +2 -1
  300. package/lib/quoxon/stray.js +22 -0
  301. package/lib/quoxon/suggestCommand.js +59 -0
  302. package/lib/quoxon/term.js +46 -11
  303. package/lib/quoxon/textBuffer.js +176 -2
  304. package/lib/quoxon/theme.js +58 -25
  305. package/lib/quoxon/tips.js +433 -0
  306. package/lib/quoxon/toolError.js +41 -0
  307. package/lib/quoxon/transcript.js +42 -4
  308. package/lib/quoxon/web/qface.js +18 -10
  309. package/lib/quoxon/welcome.js +1 -1
  310. package/lib/quoxon/whatsnew.js +136 -0
  311. package/lib/quoxon/whatsnew.jsonl +89 -0
  312. package/lib/quoxon/windowOrg.js +113 -0
  313. package/lib/server/diag.js +129 -0
  314. package/lib/server/doctor.js +71 -0
  315. package/lib/server/install.js +149 -0
  316. package/lib/server/pluginEnable.js +161 -0
  317. package/lib/server/skew.js +56 -0
  318. package/lib/server/snapshot.js +131 -0
  319. package/lib/server/status.js +128 -0
  320. package/lib/server/sys.js +65 -0
  321. package/lib/server/uninstall.js +63 -0
  322. package/lib/server/update.js +234 -0
  323. package/lib/toolOutputShape.js +315 -0
  324. package/lib/tui/views/HyperionDeck.jsx +79 -0
  325. package/lib/tui/views/LucidDeck.jsx +209 -217
  326. package/package.json +4 -1
  327. package/lib/core/websiteSession.js +0 -148
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Dragon scenes: pure timelines t (ms) -> { pose, eyes, caption }. No timers, no I/O (portable to the web).
3
+ * ONE dragon everywhere (v8): googly dot eyes (qeyes.js), a smile that opens to talk, pink cheeks, dim bat wings,
4
+ * a spade tail with spines, fire from the mouth. /dragon (the pick), /dragon classic, hero and fire, the cinematic
5
+ * moods, the greeting and /dragon-fly are all this dragon; moods are shown through the eyes and the smile only.
6
+ */
7
+ import { restPose, kf, hash, clamp, smooth, dragonLayout, renderDragon } from './qdragon.js';
8
+ import { eyesForDragon, eyeState, blinkOpen, overlay, GOOGLY } from './qeyes.js';
9
+
10
+ const TAU = Math.PI * 2;
11
+
12
+ // ---- the pick: googly dot eyes, sway sway sway whip, then fire from the mouth (ported from cli.quox.ai) -------
13
+ // The same pure functions the website lab plays (src/components/lab/dragon/dragonlab.js pickCells), so the CLI
14
+ // and the web draw the same frames. Eyes are qeyes.js Braille dots (Dots v3), laid over the dragon by composeCells:
15
+ // they are the dragon's only eyes.
16
+ export const SWAY_MS = TAU / 0.0068; // one sway cycle (about 924 ms)
17
+ export const PICK = { sway: 3 * SWAY_MS, whip: 950, fire: 2500, rest: 700 };
18
+ export const PICK_EYES = 'dots';
19
+ export const PICK_MS = PICK.sway + PICK.whip + PICK.fire + PICK.rest;
20
+ /** Which phase of the pick t (ms) falls in: 'sway' | 'whip' | 'fire' | 'rest'. */
21
+ export function pickPhase(t) {
22
+ const c = ((t % PICK_MS) + PICK_MS) % PICK_MS, w0 = PICK.sway, f0 = w0 + PICK.whip, r0 = f0 + PICK.fire;
23
+ return c < w0 ? 'sway' : c < f0 ? 'whip' : c < r0 ? 'fire' : 'rest';
24
+ }
25
+ /** The eyes at t: shut (open 0) while it breathes, otherwise the idle blinks and glances. */
26
+ export function pickEyeState(t) {
27
+ const c = ((t % PICK_MS) + PICK_MS) % PICK_MS, ft = c - PICK.sway - PICK.whip;
28
+ return pickPhase(t) === 'fire' && ft > 120 && ft < 1500 ? { open: 0 } : eyeState('idle', t);
29
+ }
30
+ /** The dragon's pose (no quadrant eyes) at absolute time t. */
31
+ export function pickPose(t) {
32
+ const p = restPose(), c = ((t % PICK_MS) + PICK_MS) % PICK_MS;
33
+ p.tail.len = 1.55; p.tail.k = 3.2; p.tail.phase = t * 0.0068;
34
+ const w0 = PICK.sway, f0 = w0 + PICK.whip, r0 = f0 + PICK.fire;
35
+ if (c < w0) p.tail.amp = 0.6 + 1.6 * smooth(c / 350);
36
+ else if (c < f0) {
37
+ const u = (c - w0) / PICK.whip, crack = smooth(u / 0.2) * (1 - smooth((u - 0.35) / 0.55));
38
+ p.tail.amp = 2.2 + 2.8 * crack; p.tail.phase += crack * 4; p.tail.k = 3.2 + 2.8 * crack; p.tail.curl = 0.8 * crack;
39
+ } else if (c < r0) {
40
+ const ft = c - f0;
41
+ p.tail.amp = 2.2 - 1.6 * smooth(ft / 600);
42
+ p.mouth = smooth(ft / 160) * (1 - smooth((ft - 1500) / 500));
43
+ p.fire = { t: ft, dur: 1300, seed: 3 };
44
+ } else p.tail.amp = 0.6;
45
+ return p;
46
+ }
47
+ const EYES2 = GOOGLY; // the owner's eyes, unchanged: every scene, mood, greeting and the flight use them
48
+ const INK = [30, 41, 59]; // eye ink on the light theme (white would vanish on a light page)
49
+ /**
50
+ * The pick at t ms. opts: style ('dots'), theme, mono (NO_COLOR: no colour), still (reduced motion: the settled
51
+ * dragon, eyes open, no tail motion, no fire). Cells are { ch, rgb, bg } like every other clip.
52
+ */
53
+ export function pickCells(t, cols = 64, style = PICK_EYES, { theme = 'dark', mono = false, still = false, size, lm, mouthSide = 'right', talk } = {}) {
54
+ const L = dragonLayout(cols, { size, lm });
55
+ let pose, eyes;
56
+ if (still) { pose = restPose(); eyes = eyeState('open', 0); }
57
+ else if (talk) { pose = talkPose(t, talk); eyes = eyeState('idle', t); } // talking: the pick's calm sway, the mouth working, no fire
58
+ else { pose = pickPose(t); eyes = pickEyeState(t); }
59
+ pose.mouthSide = mouthSide;
60
+ return composeCells(pose, eyes, L, style, { theme, mono, t });
61
+ }
62
+ /** Body (renderDragon) with the Braille googly eyes laid over it: the one way a dragon frame is made. */
63
+ export function composeCells(pose, eyes, L, style, { theme, mono, t }) {
64
+ const body = renderDragon(pose, L, { mono, t, theme });
65
+ const layer = eyesForDragon(L, eyes, EYES2, style).map((row) => row.map((c) => c && {
66
+ ch: c.ch, bg: null, rgb: !c.rgb || mono ? null : theme === 'light' ? INK : c.rgb,
67
+ }));
68
+ return overlay(body, layer);
69
+ }
70
+ /** How open the mouth is at t while it talks: 0..1, a deterministic syllable cadence (about 6 a second, with the odd pause). */
71
+ export const TALK_SYLLABLE_MS = 160;
72
+ export function talkOpen(t) {
73
+ const k = Math.floor(t / TALK_SYLLABLE_MS), u = (t - k * TALK_SYLLABLE_MS) / TALK_SYLLABLE_MS;
74
+ if (hash(k * 5 + 3) < 0.14) return 0; // a pause between words
75
+ return (0.45 + 0.55 * hash(k * 7 + 11)) * Math.sin(Math.PI * u) ** 0.8;
76
+ }
77
+ /** The pose while it talks: calm tail sway, the mouth open by `talk` (a 0..1 number, or true for the cadence), no fire, no glow. */
78
+ export function talkPose(t, talk = true) {
79
+ const p = restPose();
80
+ p.tail.len = 1.55; p.tail.k = 3.2; p.tail.phase = t * 0.0068; p.tail.amp = 0.6;
81
+ p.mouth = 0; p.smile = typeof talk === 'number' ? clamp(talk) : talkOpen(t);
82
+ return p;
83
+ }
84
+ /** pickCells with the mouth talking and no fire: talkCells(t, cols, style, { mouthSide }) . */
85
+ export const talkCells = (t, cols = 64, style = PICK_EYES, opts = {}) => pickCells(t, cols, style, { ...opts, talk: opts.talk || true });
86
+
87
+ // ---- sleep: the screensaver dragon (eyes closed, a slow minimal sway, a breath of z; no whip, no fire) -------
88
+ export const SLEEP_Z = [' ', 'z', 'z z', 'z z Z'];
89
+ /** The z that drifts up with each slow breath at t ms (4.8 s cycle). */
90
+ export const sleepZ = (t) => SLEEP_Z[Math.floor((((t % 4800) + 4800) % 4800) / 1200)];
91
+ /** The sleeping pose at t ms: tail barely swaying (amp 0.1 vs the pick's 0.6), eyes shut, mouth closed, no fire. */
92
+ export function sleepPose(t, { still = false } = {}) {
93
+ const p = restPose();
94
+ p.fire = null; p.mouth = 0;
95
+ if (!still) { p.tail.len = 1.4; p.tail.k = 2.6; p.tail.phase = t * 0.0011; p.tail.amp = 0.1; }
96
+ const z = still ? 'z' : sleepZ(t);
97
+ if (z.trim()) p.label = { text: z, rgb: [186, 230, 253] };
98
+ return p;
99
+ }
100
+ /** Sleeping dragon cells: sleepCells(t, cols, style, { theme, mono, still, size, lm }). Eyes are always shut. */
101
+ export function sleepCells(t, cols = 64, style = PICK_EYES, { theme = 'dark', mono = false, still = false, size, lm } = {}) {
102
+ const L = dragonLayout(cols, { size, lm });
103
+ return composeCells(sleepPose(t, { still }), eyeState('closed', 0), L, style, { theme, mono, t });
104
+ }
105
+
106
+ // ---- the hero: talk on the right, sway sway sway whip, the mouth moves left, fire leftwards (cli.quox.ai hero) ---
107
+ export const HERO_TALK_MS = 1400, HERO_MOVE_MS = 450;
108
+ export const heroMs = (talkMs = HERO_TALK_MS) => talkMs + PICK.sway + PICK.whip + HERO_MOVE_MS + PICK.fire + PICK.rest;
109
+ export const HERO_MS = heroMs();
110
+ /** Hero timeline: 'talk' | 'sway' | 'whip' | 'move' | 'fire' | 'rest'. Loops on heroMs(talkMs). */
111
+ export function heroPhase(t, { talkMs = HERO_TALK_MS } = {}) {
112
+ const T = heroMs(talkMs), c = ((t % T) + T) % T;
113
+ const a = talkMs, b = a + PICK.sway, d = b + PICK.whip, e = d + HERO_MOVE_MS, f = e + PICK.fire;
114
+ return c < a ? 'talk' : c < b ? 'sway' : c < d ? 'whip' : c < e ? 'move' : c < f ? 'fire' : 'rest';
115
+ }
116
+ /** The hero's pose and eye state at t. The tail keeps swaying through the move; fire comes from the left mouth. */
117
+ function heroFrame(t, talkMs) {
118
+ const fr = heroFrame0(t, talkMs);
119
+ fr.pose.flap = 0.12 * Math.sin(t * 0.0068 * 0.5); // the wings keep a gentle beat
120
+ return fr;
121
+ }
122
+ function heroFrame0(t, talkMs) {
123
+ const T = heroMs(talkMs), c = ((t % T) + T) % T, ph = heroPhase(t, { talkMs });
124
+ const w0 = PICK.sway, f0 = w0 + PICK.whip;
125
+ if (ph === 'talk') return { pose: talkPose(t, true), eyes: eyeState('idle', t) };
126
+ // pick time pt: 0 at the start of the sway; frozen at the end of the whip during the move
127
+ const base = c - talkMs, pt = ph === 'move' ? f0 - 1 : ph === 'fire' || ph === 'rest' ? base - HERO_MOVE_MS : base;
128
+ const pose = pickPose(pt);
129
+ pose.tail.phase += (t - pt) * 0.0068; // the sway never stutters at the seams
130
+ pose.fire = ph === 'fire' && pt > f0 ? pose.fire : null;
131
+ pose.mouthSide = ph === 'move' ? (c - talkMs - w0 - PICK.whip < HERO_MOVE_MS / 2 ? 'right' : 'left') : ph === 'fire' || ph === 'rest' ? 'left' : 'right';
132
+ if (ph === 'move') pose.mouth = 0;
133
+ // v2: the face turns left (mouth and cheeks slide, eyes look left), then the mouth opens wide to breathe
134
+ const u = ph === 'move' ? smooth((c - talkMs - w0 - PICK.whip) / HERO_MOVE_MS) : ph === 'fire' ? 1 : ph === 'rest' ? 1 - smooth((c - talkMs - w0 - PICK.whip - HERO_MOVE_MS - PICK.fire) / PICK.rest) : 0;
135
+ pose.faceX = -0.06 * u;
136
+ if (ph === 'fire') pose.smile = pose.mouth; // the mouth opens wide to breathe
137
+ pose.mouth = 0;
138
+ const ft = pt - f0;
139
+ if (ph === 'move') return { pose, eyes: { ...eyeState('idle', t), gx: -1 * u, gy: 0.4 } };
140
+ return { pose, eyes: ph === 'fire' && ft > 120 && ft < 1500 ? { open: 0 } : eyeState('idle', t) };
141
+ }
142
+ /**
143
+ * The hero at t ms (cli.quox.ai): the mouth on the right talks (no fire) for talkMs, then the tail sways three times
144
+ * and whips, the mouth moves to the left and the dragon breathes fire leftwards, eyes shut. The Q needs room on its
145
+ * left for the fire: lm defaults to as much margin as cols allow. still: eyes open, mouth closed, no fire.
146
+ */
147
+ export function heroCells(t, cols = 64, style = PICK_EYES, { talkMs = HERO_TALK_MS, theme = 'dark', mono = false, still = false, size, lm } = {}) {
148
+ if (lm === undefined) { const L0 = dragonLayout(cols, { size }); lm = Math.max(L0.qx0 / 2, Math.min(Math.ceil(L0.H * 1.25 / 2) + 4, Math.floor((cols * 2 - L0.qw) / 2) - 2)); }
149
+ const L = dragonLayout(cols, { size, lm });
150
+ if (still) return composeCells(restPose(), eyeState('open', 0), L, style, { theme, mono, t: 0 });
151
+ const { pose, eyes } = heroFrame(t, talkMs);
152
+ return composeCells(pose, eyes, L, style, { theme, mono, t });
153
+ }
154
+ const HERO_CAP = { talk: 'talking', sway: 'tail sway', whip: 'tail whip', move: 'the mouth moves to the left', fire: 'fire breath to the left, eyes shut', rest: 'settle' };
155
+
156
+ // ---- moods: the same dragon, expressed through the eyes and the smile only ----------------------------------------
157
+ export const MOODS = ['waiting', 'success', 'failure', 'greeting'];
158
+ const GREETING = "Hi, I'm Quox!";
159
+ const MOOD_NOTE = {
160
+ waiting: 'waiting: eyes wide on you, a slow look about, a patient blink',
161
+ success: 'success: a blink, then happy closed eyes and a big smile',
162
+ failure: 'failure: the eyes sink and look away, the smile turns down',
163
+ greeting: 'greeting: eyes open, look at you, a blink, the smile says hi',
164
+ };
165
+ const greetCells = (text, t) => {
166
+ const n = Math.floor(clamp(t / 45, 0, text.length));
167
+ return [...text].map((ch, i) => ({ ch: i < n ? ch : ' ', rgb: [56 + (168 - 56) * (i / (text.length - 1)), 189 + (85 - 189) * (i / (text.length - 1)), 248 + (247 - 248) * (i / (text.length - 1))].map(Math.round) }));
168
+ };
169
+ /** The calm base every mood sits on: the pick's easy tail sway, wings gently flapping, smile closed. */
170
+ function calmPose(t) {
171
+ const p = talkPose(t, 0);
172
+ p.flap = 0.12 * Math.sin(t * 0.0068 * 0.5);
173
+ return p;
174
+ }
175
+ /** A mood at t ms since it began: { pose, eyes, caption }. still: the static representative form. */
176
+ export function moodFrame(mood, t, { still = false } = {}) {
177
+ const p = calmPose(still ? 0 : t);
178
+ let eyes = eyeState('open', 0);
179
+ switch (mood) {
180
+ case 'waiting': {
181
+ const u = still ? 0 : t;
182
+ eyes = { open: still ? 1 : blinkOpen((u % 2600) - 2200), gx: still ? 0 : 0.55 * Math.sin(u / 1500), gy: 0.55 };
183
+ break;
184
+ }
185
+ case 'success': {
186
+ const shut = still || (t >= 1100 && t < 3300);
187
+ eyes = { open: shut ? 0 : blinkOpen(t - 450), gx: 0, gy: 1 };
188
+ p.smile = still ? 0.9 : kf(t, [[1000, 0], [1400, 0.9], [3000, 0.9], [3400, 0]]);
189
+ break;
190
+ }
191
+ case 'failure': {
192
+ const k = still ? 1 : kf(t, [[0, 0], [350, 1]]);
193
+ eyes = { open: 1 - 0.45 * k, gx: -0.7 * k, gy: 1 };
194
+ p.frown = k > 0.5 ? 1 : 0;
195
+ break;
196
+ }
197
+ case 'greeting': {
198
+ eyes = { open: still ? 1 : kf(t, [[0, 0], [450, 1]]) * Math.min(blinkOpen(t - 1700), blinkOpen(t - 1960)), gx: 0, gy: 1 };
199
+ p.smile = still ? 0 : t > 700 && t < 1700 ? talkOpen(t) : 0;
200
+ p.label = { cells: greetCells(GREETING, still ? 1e9 : t - 400) };
201
+ break;
202
+ }
203
+ default: throw new Error(`unknown dragon mood: ${mood}`);
204
+ }
205
+ return { pose: p, eyes, caption: MOOD_NOTE[mood] };
206
+ }
207
+
208
+ // ---- client clips (CLI-QUOX F3) ---------------------------------------------------------------------------
209
+
210
+ /** Where the fire clip starts inside the hero: just before the face turns left. */
211
+ const FIRE_OFFSET = HERO_TALK_MS + PICK.sway + PICK.whip;
212
+ const FIRE_MS = HERO_MOVE_MS + PICK.fire + PICK.rest;
213
+ const heroClipFrame = (t, offset = 0) => { const tt = t + offset; return { ...heroFrame(tt, HERO_TALK_MS), caption: HERO_CAP[heroPhase(tt)] }; };
214
+ /** The hero: talk, sway sway sway whip, the face turns left, fire from the mouth, settle. /dragon, classic and the pick all play it. */
215
+ const heroClip = {
216
+ dur: HERO_MS,
217
+ at: (t) => heroClipFrame(t),
218
+ still: () => ({ pose: restPose(), eyes: eyeState('open', 0), caption: 'the dragon (static form)' }),
219
+ cells: (t, cols, o) => heroCells(t, cols, PICK_EYES, o),
220
+ width: 100, title: 'The dragon',
221
+ caption: 'googly dot eyes, a smile that talks; the tail sways and whips, then the face turns left and the dragon breathes fire from its mouth, eyes squeezed shut (/dragon)',
222
+ };
223
+ /** /dragon fire: the hero's last act on its own: the face turns left, fire from the mouth, settle. */
224
+ const fireClip = {
225
+ dur: FIRE_MS,
226
+ at: (t) => heroClipFrame(t, FIRE_OFFSET),
227
+ still: heroClip.still,
228
+ cells: (t, cols, o) => heroCells(t + FIRE_OFFSET, cols, PICK_EYES, o),
229
+ width: 100, title: 'Fire',
230
+ caption: 'the face turns left and the dragon breathes fire from its mouth, eyes squeezed shut (/dragon fire)',
231
+ };
232
+ /** A mood as a short vignette (cinematic level only). No label unless asked: the status line already says what happened. */
233
+ const moodClip = (mood, dur, width, title, caption, label = false) => ({
234
+ dur, width, title, caption,
235
+ at(t) { const fr = moodFrame(mood, t); if (!label) fr.pose.label = null; return fr; },
236
+ still() { const fr = moodFrame(mood, 0, { still: true }); if (!label) fr.pose.label = null; return fr; },
237
+ });
238
+
239
+ /**
240
+ * Everything the client plays, by name. width = the frame the clip wants (the hero and fire need room to the left
241
+ * for the fire; moods are compact). pick and idle (/dragon, /dragon classic) are the hero itself: no separate designs.
242
+ */
243
+ export const CLIPS = {
244
+ hero: heroClip,
245
+ pick: heroClip,
246
+ idle: heroClip,
247
+ fire: fireClip,
248
+ waiting: moodClip('waiting', 2500, 40, 'Mood: waiting', 'waiting for your approval: eyes wide on you (cinematic motion)'),
249
+ success: moodClip('success', 2300, 40, 'Mood: success', 'done: a blink, then happy eyes and a big smile (cinematic motion)'),
250
+ failure: moodClip('failure', 2200, 40, 'Mood: failure', 'failed: the eyes sink and look away, the smile turns down (cinematic motion)'),
251
+ greeting: moodClip('greeting', 2600, 64, 'Mood: greeting', 'hello: the eyes open, look at you, blink, the smile says hi (the cinematic intro)', true),
252
+ };
253
+ export const CLIP_NAMES = Object.keys(CLIPS);
254
+ /** The moods the app plays at the cinematic level, keyed by what happened. */
255
+ export const MOOD_CLIPS = ['waiting', 'success', 'failure', 'greeting'];
256
+
257
+ /**
258
+ * Cells of a clip at t ms: rows of { ch, rgb, bg }. opts: cols (the room there is), theme 'dark' | 'light',
259
+ * mono (NO_COLOR: shapes only), still (reduced motion: the static form), size / lm (force a layout), label (false:
260
+ * no words in the frame).
261
+ */
262
+ export function clipCells(name, t, { cols = 100, theme = 'dark', mono = false, still = false, size, lm, label = true } = {}) {
263
+ const c = CLIPS[name];
264
+ if (!c) throw new Error(`unknown dragon clip: ${name}`);
265
+ const L = dragonLayout(Math.max(size ? 8 : 40, Math.min(cols, c.width)), { size, lm }); // a forced size (the intro) may be narrower than 40
266
+ const tt = Math.max(0, Math.min(t, c.dur));
267
+ if (c.cells) return c.cells(tt, L.cols, { theme, mono, still, size, lm });
268
+ const fr = still ? c.still() : c.at(tt);
269
+ if (!label) fr.pose.label = null; // the intro has its own words beside the art
270
+ return composeCells(fr.pose, fr.eyes, L, PICK_EYES, { theme, mono, t: tt });
271
+ }
272
+ /** The caption of a clip at t (what the dragon is doing now). */
273
+ export const clipCaption = (name, t) => CLIPS[name].at(Math.max(0, Math.min(t, CLIPS[name].dur))).caption;
274
+
275
+ // ---- the launch egg: the v8 dragon's googly-eye greeting beside the settled Q ------------------------------
276
+ import { qDims } from './qart.js';
277
+ export const EGG_MS = CLIPS.greeting.dur;
278
+ /** The egg at t ms (after the Q has settled): the v8 dragon opens its googly eyes, looks, blinks and smiles, at the Q's size. */
279
+ export function eggCells(size, t, { color = true, theme = 'dark' } = {}) {
280
+ const L = dragonLayout(qDims(size).cols + 6, { size, lm: 0 });
281
+ return clipCells('greeting', t, { cols: L.cols, size, lm: 0, theme, mono: !color, label: false });
282
+ }
@@ -0,0 +1,213 @@
1
+ // Quox eyes (canonical source: quox cli/lib/quoxon/qeyes.js; the cli.quox.ai site vendors this file as is).
2
+ // Two friendly cartoon eyes in the Q's own terminal cell pipeline, so the web (cellsToHTML) and the
3
+ // terminal (cellsToAnsi) draw identical eyes. Two techniques, both terminal-safe:
4
+ // dots Braille dots (the Q's own particles): a closed disc of white dots, a pupil of UNLIT dots (the
5
+ // background shows through, so it reads black), a 2x2 dot glint, and a 2-dot arc when closed.
6
+ // blocks Unicode quadrant blocks with a foreground AND a background colour (as qdragon's eye layer).
7
+ // Plus motion as pure functions of time: blink, look around, idle (eyeState).
8
+ //
9
+ // PURE AND PORTABLE: no DOM, no timers, no Node APIs. A function of (geometry, state) or (mode, t).
10
+ // The left eye is computed once and the right eye is the same eye TRANSLATED (both centres snapped to even
11
+ // dots, so both sit on the same Braille cell phase): the two always match, and both pupils look the same way.
12
+ // Compositing is per cell: a cell inside an eye's footprint is drawn from the eye layer only, so a Q dot never
13
+ // shares a cell (and its one colour) with an eye. The Q itself is never redrawn.
14
+ //
15
+ // Geometry is the Q's own unit space (qart.js): y 0..1 down the Q, x 0..0.82, the bowl's axis at x 0.41.
16
+
17
+ const AXIS = 0.41;
18
+ const BR = [[0x1, 0x8], [0x2, 0x10], [0x4, 0x20], [0x40, 0x80]];
19
+ const QUAD = [' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█'];
20
+ export const EYE_WHITE = [255, 255, 255]; // pure white (owner: whiter whites)
21
+ export const EYE_PUPIL = [10, 12, 18];
22
+ const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
23
+ const smooth = (e) => { e = clamp(e); return e * e * (3 - 2 * e); };
24
+
25
+ /**
26
+ * The owner's pick ("big and googly"). r, sep, y in Q units. pupil: pupil radius as a share of r. rest: where the
27
+ * pupils rest (gaze -1..1; a little down reads as "looking at you"). rim: the least white (dots) kept round the
28
+ * pupil at every gaze. glint: the 2x2 glint's offset from the pupil centre, as a share of the pupil radius (out
29
+ * towards the upper left, near the pupil's edge, so it reads as a highlight and not a bullseye). sag: closed lid.
30
+ */
31
+ export const GOOGLY = { r: 0.104, sep: 0.124, y: 0.53, ox: 0.95, oy: 1.02, pupil: 0.46, rest: [0, 1], rim: 2, glint: 0.62, sag: 0.34 };
32
+ /** Variant A of the bullseye fix (rejected by screenshot: the thinner rim still read as a donut). */
33
+ export const GOOGLY_A = { ...GOOGLY, pupil: 0.52 };
34
+ /** The v2 dots (before the bullseye fix): smaller pupil, glint near its centre. Kept for comparison. */
35
+ export const GOOGLY_V2 = { ...GOOGLY, pupil: 0.43, glint: 0.45, rest: [0, 0.6] };
36
+
37
+ function centres(geo, v) {
38
+ const R = v.r * geo.Hq, axis = 2 * Math.round((geo.ox + AXIS * geo.Hq) / 2), cy = (geo.oy ?? 0) + v.y * geo.Hq;
39
+ const cxL = axis - 2 * Math.ceil((v.sep * geo.Hq) / 2);
40
+ return { R, cy, cxL, shift: 2 * (axis - cxL) }; // right eye = left eye + shift (an even number of dots)
41
+ }
42
+
43
+ /** The left eye in dot units: f(x, y) -> 0 none | 1 white | 2 pupil | 3 glint, for an open eye. */
44
+ function eyeShape(cx, cy, R, v, open, gx, gy) {
45
+ const rx = R * v.ox, ry = R * v.oy, pr = R * v.pupil;
46
+ const lidY = cy - ry + (1 - open) * 2 * ry * 0.92; // the upper lid: everything above it is closed
47
+ // the pupil travels inside the white, shrunk by its radius plus the rim, and snaps to whole dots
48
+ const ax = Math.max(0, rx - pr - v.rim), ay = Math.max(0, ry - pr - v.rim), n = Math.hypot(gx, gy), s = n > 1 ? 1 / n : 1;
49
+ const px = cx + Math.trunc(gx * s * ax), py = cy + Math.trunc(gy * s * ay); // truncate: never into the rim
50
+ const glint = pr >= 2.6, g = pr * v.glint * 0.75, gx0 = Math.round(px - g), gy0 = Math.round(py - g); // up-left, a dark dot clear of the rim
51
+ return (x, y) => {
52
+ if (((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 > 1 || y < lidY) return 0;
53
+ if (y < lidY + 2) return 1; // the lid's edge: a 2-dot white line
54
+ if (Math.hypot(x - px, y - py) > pr) return 1;
55
+ if (glint && Math.floor(x) >= gx0 && Math.floor(x) <= gx0 + 1 && Math.floor(y) >= gy0 && Math.floor(y) <= gy0 + 1) return 3;
56
+ return 2;
57
+ };
58
+ }
59
+ /** The closed lid's centre line y(x) (an arc bowed upwards, like a shut lid; owner flipped it from a smile) and its half-width, in dot units. */
60
+ function lid(cx, cy, R, v) {
61
+ const w = R * v.ox * 0.88, d = R >= 6 ? R * v.sag : 0.6, y0 = cy - d / 2;
62
+ return { n: Math.round(w), y: (x) => { const u = (x - cx) / w; return y0 + d * u * u; } };
63
+ }
64
+
65
+ /** Footprint: the cells each eye owns (its white ellipse plus a little margin), as a Uint8Array cols*rows. */
66
+ function footprint(geo, v) {
67
+ const { R, cy, cxL, shift } = centres(geo, v), foot = new Uint8Array(geo.cols * geo.rows);
68
+ for (const cx of [cxL, cxL + shift]) for (let r = 0; r < geo.rows; r++) for (let c = 0; c < geo.cols; c++) {
69
+ const x = c * 2 + 1, y = r * 4 + 2;
70
+ if (((x - cx) / (R * v.ox + 0.4)) ** 2 + ((y - cy) / (R * v.oy + 0.4)) ** 2 <= 1) foot[r * geo.cols + c] = 1;
71
+ }
72
+ return foot;
73
+ }
74
+ const gaze = (state, v) => [state.gx ?? v.rest[0], state.gy ?? v.rest[1]];
75
+
76
+ // ---- dots ---------------------------------------------------------------------------------------------------
77
+ /** Lit dots of both eyes on the W x H dot grid (Uint8Array, 1 = lit). state: { open 0..1, gx, gy -1..1 }. */
78
+ export function eyeDots(geo, state = {}, v = GOOGLY) {
79
+ const W = geo.cols * 2, H = geo.rows * 4, lit = new Uint8Array(W * H);
80
+ const { R, cy, cxL, shift } = centres(geo, v), open = clamp(state.open ?? 1);
81
+ const set = (x, y) => { for (const xx of [x, x + shift]) if (xx >= 0 && y >= 0 && xx < W && y < H) lit[y * W + xx] = 1; };
82
+ if (open < 0.15) {
83
+ const L = lid(cxL, cy, R, v); // one clean arc, 2 dots thick, symmetric about the eye centre
84
+ for (let x = cxL - L.n; x < cxL + L.n; x++) { const y = Math.round(L.y(x + 0.5) - 0.5); set(x, y); set(x, y + 1); }
85
+ return lit;
86
+ }
87
+ const [gx, gy] = gaze(state, v), f = eyeShape(cxL, cy, R, v, open, gx, gy);
88
+ for (let y = Math.floor(cy - R * 2); y <= Math.ceil(cy + R * 2); y++) for (let x = Math.floor(cxL - R * 2); x <= Math.ceil(cxL + R * 2); x++) {
89
+ const k = f(x + 0.5, y + 0.5);
90
+ if (k === 1 || k === 3) set(x, y);
91
+ }
92
+ return lit;
93
+ }
94
+
95
+ /** The dot eye layer: rows of cells (null = no eye here). */
96
+ export function eyeCells(geo, state, v = GOOGLY) {
97
+ const W = geo.cols * 2, lit = eyeDots(geo, state, v), foot = footprint(geo, v), rows = [];
98
+ for (let r = 0; r < geo.rows; r++) {
99
+ const row = [];
100
+ for (let c = 0; c < geo.cols; c++) {
101
+ let bits = 0;
102
+ for (let dy = 0; dy < 4; dy++) for (let dx = 0; dx < 2; dx++) if (lit[(r * 4 + dy) * W + c * 2 + dx]) bits |= BR[dy][dx];
103
+ row.push(bits ? { ch: String.fromCharCode(0x2800 + bits), rgb: EYE_WHITE } : foot[r * geo.cols + c] ? { ch: ' ', rgb: null } : null);
104
+ }
105
+ rows.push(row);
106
+ }
107
+ return rows;
108
+ }
109
+
110
+ // ---- blocks (reference) -------------------------------------------------------------------------------------
111
+ /** The block eye layer: 2x2 quadrant pixels per cell (a pixel is 1 dot wide, 2 tall), fg + bg colours. */
112
+ export function blockEyeCells(geo, state = {}, v = GOOGLY) {
113
+ const PW = geo.cols * 2, PH = geo.rows * 2, px = new Uint8Array(PW * PH); // 0 none, 1 white, 2 pupil
114
+ const { R, cy, cxL, shift } = centres(geo, v), open = clamp(state.open ?? 1);
115
+ const put = (x, y, k) => { for (const xx of [x, x + shift]) if (xx >= 0 && y >= 0 && xx < PW && y < PH) px[y * PW + xx] = k; };
116
+ if (open < 0.15) {
117
+ const L = lid(cxL, cy, R, v);
118
+ for (let x = cxL - L.n; x < cxL + L.n; x++) put(x, Math.floor(L.y(x + 0.5) / 2), 1);
119
+ } else {
120
+ const [gx, gy] = gaze(state, v), f = eyeShape(cxL, cy, R, { ...v, pupil: 0.43 }, open, gx, gy), small = R < 6;
121
+ for (let y = Math.floor((cy - R * 2) / 2); y <= Math.ceil((cy + R * 2) / 2); y++) for (let x = Math.floor(cxL - R * 2); x <= Math.ceil(cxL + R * 2); x++) {
122
+ const k = f(x + 0.5, y * 2 + 1);
123
+ if (k) put(x, y, k === 3 ? 1 : k === 2 && small ? 1 : k);
124
+ }
125
+ if (small) { const pr0 = Math.round(cy / 2); for (const x of [cxL - 1, cxL]) for (const y of [pr0 - 1, pr0]) put(x, y, 2); }
126
+ }
127
+ const foot = footprint(geo, v), rows = [];
128
+ for (let r = 0; r < geo.rows; r++) {
129
+ const row = [];
130
+ for (let c = 0; c < geo.cols; c++) {
131
+ const q = [px[(r * 2) * PW + c * 2], px[(r * 2) * PW + c * 2 + 1], px[(r * 2 + 1) * PW + c * 2], px[(r * 2 + 1) * PW + c * 2 + 1]];
132
+ if (!q.some((k) => k)) { row.push(foot[r * geo.cols + c] ? { ch: ' ', rgb: null } : null); continue; }
133
+ let m = 0; q.forEach((k, i) => { if (k === 1) m |= 1 << i; });
134
+ if (q.includes(1)) row.push({ ch: QUAD[m], rgb: EYE_WHITE, bg: q.includes(2) ? EYE_PUPIL : null });
135
+ else row.push({ ch: '█', rgb: EYE_PUPIL });
136
+ }
137
+ rows.push(row);
138
+ }
139
+ return rows;
140
+ }
141
+
142
+ // ---- motion: pure functions of time ---------------------------------------------------------------------------
143
+ export const BLINK_MS = 200;
144
+ /** Lid openness u ms into a blink: shut in 70 ms, held 40, open again by 200. */
145
+ export function blinkOpen(u) {
146
+ if (u < 0 || u >= BLINK_MS) return 1;
147
+ if (u < 70) return 1 - smooth(u / 70);
148
+ if (u < 110) return 0;
149
+ return smooth((u - 110) / 90);
150
+ }
151
+ const hash = (n) => { let x = Math.imul((n | 0) ^ 0x9e3779b9, 0x85ebca6b); x ^= x >>> 13; x = Math.imul(x, 0xc2b2ae35); x ^= x >>> 16; return (x >>> 0) / 4294967296; };
152
+ // keyframes [[ms, [gx, gy]], ...], eased between neighbours, held at the ends
153
+ function gazeKf(t, keys) {
154
+ if (t <= keys[0][0]) return keys[0][1];
155
+ for (let i = 0; i < keys.length - 1; i++) {
156
+ const [t0, a] = keys[i], [t1, b] = keys[i + 1];
157
+ if (t < t1) { const e = smooth((t - t0) / (t1 - t0)); return [a[0] + (b[0] - a[0]) * e, a[1] + (b[1] - a[1]) * e]; }
158
+ }
159
+ return keys[keys.length - 1][1];
160
+ }
161
+ const REST = GOOGLY.rest;
162
+ /** Look around: centre, left, right, up, down, centre; 260 ms moves, 600 ms holds. */
163
+ export const LOOK_KEYS = (() => {
164
+ const pts = [REST, [-1, 0.2], [1, 0.2], [0, -1], [0, 1], REST], keys = [];
165
+ let t = 0;
166
+ for (const p of pts) { keys.push([t, p]); t += 600; keys.push([t, p]); t += 260; }
167
+ return keys;
168
+ })();
169
+ export const LOOK_MS = LOOK_KEYS[LOOK_KEYS.length - 1][0] + 400;
170
+
171
+ /**
172
+ * The eyes' state at t ms for a mode: 'open' | 'closed' | 'blink' | 'look' | 'idle'. Returns { open, gx, gy }.
173
+ * blink a blink every 1.6 s; every third one is a double blink.
174
+ * look the look-around loop (LOOK_KEYS).
175
+ * idle blinks every 2.6 to 5 s (one in five a double), and now and then a glance that holds and returns.
176
+ */
177
+ export function eyeState(mode, t) {
178
+ if (mode === 'closed') return { open: 0, gx: REST[0], gy: REST[1] };
179
+ if (mode === 'open' || t < 0) return { open: 1, gx: REST[0], gy: REST[1] };
180
+ if (mode === 'blink') {
181
+ const k = Math.floor(t / 1600), u = t - k * 1600;
182
+ return { open: Math.min(blinkOpen(u - 300), k % 3 === 2 ? blinkOpen(u - 560) : 1), gx: REST[0], gy: REST[1] };
183
+ }
184
+ if (mode === 'look') { const [gx, gy] = gazeKf(t % LOOK_MS, LOOK_KEYS); return { open: 1, gx, gy }; }
185
+ // idle: a seeded schedule of events, each 2.6 to 5 s long, so any t maps to the same frame
186
+ let start = 0, i = 0;
187
+ for (;;) { const len = 2600 + hash(i * 7 + 1) * 2400; if (t < start + len) break; start += len; i++; if (i > 100000) break; }
188
+ const u = t - start, len = 2600 + hash(i * 7 + 1) * 2400;
189
+ let open = 1, g = REST;
190
+ if (hash(i * 7 + 2) < 0.4) {
191
+ // a glance: move in 240 ms, hold, come back before the event ends
192
+ const dir = [[-1, 0.2], [1, 0.2], [-0.8, -0.7], [0.8, -0.7], [0, 1]][Math.floor(hash(i * 7 + 3) * 5)];
193
+ g = gazeKf(u, [[300, REST], [540, dir], [540 + 900, dir], [540 + 900 + 260, REST]]);
194
+ }
195
+ const b = len - 700; // the blink near the end of each event
196
+ open = Math.min(blinkOpen(u - b), hash(i * 7 + 4) < 0.2 ? blinkOpen(u - b - 260) : 1);
197
+ return { open, gx: g[0], gy: g[1] };
198
+ }
199
+
200
+ // ---- compose ------------------------------------------------------------------------------------------------
201
+ /** Composite: eye cells over base cells (same grid). Base cells are untouched wherever the eye layer is null. */
202
+ export function overlay(base, eyes) {
203
+ return base.map((row, r) => row.map((cell, c) => (eyes[r] && eyes[r][c]) || cell));
204
+ }
205
+ const layer = (style) => (style === 'blocks' ? blockEyeCells : eyeCells);
206
+ /** Eyes for the settled qart Q, given its mask ({ W, H, ox } from qMask). style: 'dots' | 'blocks'. */
207
+ export function eyesForQ(mask, state, v = GOOGLY, style = 'dots') {
208
+ return layer(style)({ cols: mask.W / 2, rows: mask.H / 4, ox: mask.ox, Hq: mask.H }, state, v);
209
+ }
210
+ /** Eyes for a qdragon layout ({ cols, rows, qx0, qy0, H }). */
211
+ export function eyesForDragon(L, state, v = GOOGLY, style = 'dots') {
212
+ return layer(style)({ cols: L.cols, rows: L.rows, ox: L.qx0, oy: L.qy0, Hq: L.H }, state, v);
213
+ }
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Ink adaptors for the pure Q frames (qart.js, qglyph.js, qhello.js): the status glyph, the hello intro
3
- * used by Getting started, and the on-demand Dragon's Eye (/eye). The frame math lives in the pure modules
3
+ * used by Getting started. The frame math lives in the pure modules
4
4
  * so the web face draws the same frames; this file only maps cells to <Text>.
5
5
  */
6
6
  import React from 'react';
7
7
  import { Box, Text, useStdout } from 'ink';
8
8
  import { qGlyph, glyphMode, colorDepth } from './qglyph.js';
9
- import { labSize, qDims, introPlan, eyeCellsAt, EYE_MS, INTRO_MS } from './qart.js';
9
+ import { labSize, qDims, introPlan, INTRO_MS } from './qart.js';
10
10
  import { helloCells, helloGradient, helloMs } from './qhello.js';
11
- import { mono, c, clip } from './theme.js';
11
+ import { mono, c, clip, isLight } from './theme.js';
12
+ import { CLIPS, clipCells, clipCaption, eggCells, EGG_MS } from './qdragonScenes.js';
12
13
  import { reducedMotion } from './motion/clock.js';
13
14
  import { FrameContext, useFrame } from './motion/frame.js';
14
15
  const isQuiet = () => reducedMotion(); // motion only (NO_COLOR is colour only; QGlyph falls back to the circled Q itself)
@@ -20,11 +21,11 @@ const rgbS = (rgb) => (rgb ? `rgb(${rgb.join(',')})` : undefined);
20
21
  /** Runs of same-coloured cells -> one <Text> each (keeps Ink's per-frame tokenising cheap). */
21
22
  export function cellNodes(cells, keyBase = 'c') {
22
23
  const out = [];
23
- let run = '', col, bold = false, k = 0;
24
- const flush = () => { if (run) out.push(h(Text, { key: `${keyBase}${k++}`, color: col, bold }, run)); run = ''; };
24
+ let run = '', col, bg, bold = false, k = 0;
25
+ const flush = () => { if (run) out.push(h(Text, { key: `${keyBase}${k++}`, color: col, backgroundColor: bg, bold }, run)); run = ''; };
25
26
  for (const cl of cells) {
26
- const cc = rgbS(cl.rgb), b = Boolean(cl.bold);
27
- if (cc !== col || b !== bold) { flush(); col = cc; bold = b; }
27
+ const cc = rgbS(cl.rgb), bb = rgbS(cl.bg), b = Boolean(cl.bold); // bg: the dragon's quadrant-block eyes
28
+ if (cc !== col || bb !== bg || b !== bold) { flush(); col = cc; bg = bb; bold = b; }
28
29
  run += cl.ch;
29
30
  }
30
31
  flush();
@@ -47,12 +48,12 @@ export function QGlyph({ state = 'working', ms = Date.now(), since = null, quiet
47
48
  // ---- hello intro (Getting started) ----------------------------------------------------------------------
48
49
  import { HELLO } from './onboarding/copy.js';
49
50
  export { HELLO };
50
- /** Total ms of the hello intro: Orbit & Lock, the greeting decode, then (egg) the Dragon's Eye. */
51
- export const helloIntroMs = ({ eye = false, text = HELLO } = {}) => INTRO_MS + helloMs(text) + (eye ? EYE_MS + 400 : 0);
51
+ /** Total ms of the hello intro: Orbit & Lock, the greeting decode, then (egg) the dragon's googly-eye greeting. */
52
+ export const helloIntroMs = ({ eye = false, text = HELLO } = {}) => INTRO_MS + helloMs(text) + (eye ? EGG_MS + 400 : 0);
52
53
 
53
54
  /**
54
55
  * The Q, then the Q says hello beside it. t: ms since the intro started (Infinity = settled). The greeting
55
- * decodes in once the Q has locked; the eye (launch egg) plays after the greeting has landed.
56
+ * decodes in once the Q has locked; the egg (the dragon's googly-eye greeting) plays after the greeting has landed.
56
57
  */
57
58
  export function HelloIntro({ item, cols, rows, t = Infinity, eye = false, text = HELLO }) {
58
59
  const { stdout } = useStdout();
@@ -61,7 +62,7 @@ export function HelloIntro({ item, cols, rows, t = Infinity, eye = false, text =
61
62
  const helloAt = t - INTRO_MS;
62
63
  const plan = introPlan(size, { color });
63
64
  let cells;
64
- if (eye && helloAt >= helloMs(text)) cells = eyeCellsAt(size, helloAt - helloMs(text), { color });
65
+ if (eye && helloAt >= helloMs(text)) cells = eggCells(size, helloAt - helloMs(text), { color, theme: isLight() ? 'light' : 'dark' });
65
66
  else cells = plan.at(Math.min(t, INTRO_MS));
66
67
  const wide = cols >= 60;
67
68
  const w = qDims(size).cols;
@@ -74,38 +75,58 @@ export function HelloIntro({ item, cols, rows, t = Infinity, eye = false, text =
74
75
  info);
75
76
  }
76
77
 
77
- // ---- /eye: the Dragon's Eye on demand --------------------------------------------------------------------
78
- const listeners = new Set();
79
- let eyeStart = null;
80
- /** Ask the live <EyeLive/> to play the eye now. Returns true (the command was handled). */
81
- export function playEye(now = Date.now()) { eyeStart = now; for (const fn of listeners) fn(); return true; }
82
- export const eyePlaying = (now = Date.now()) => eyeStart != null && now - eyeStart < EYE_MS + 400;
83
- /** How long the eye shows (reduced motion: a still eye for 2 s). The app's shared clock frames it while true. */
84
- export const eyeActive = (now = Date.now(), quiet = isQuiet()) => eyeStart != null && now - eyeStart < (quiet ? 2000 : EYE_MS + 400);
78
+ // ---- /dragon: the living dragon on demand (and the cinematic moods) ---------------------------------------
79
+ const dragonListeners = new Set();
80
+ let dragonNow = null; // { clip, start }
81
+ export const DRAGON_STILL_MS = 2000;
82
+ /** Play a dragon clip now ('pick' for /dragon, 'idle' for /dragon classic (both the hero), 'fire' for /dragon fire, or a mood). Returns true. */
83
+ export function playDragon(clip = 'pick', now = Date.now()) {
84
+ if (!CLIPS[clip]) return false;
85
+ dragonNow = { clip, start: now };
86
+ for (const fn of dragonListeners) fn();
87
+ return true;
88
+ }
89
+ /** The clip playing at `now`, or null. Reduced motion or NO_COLOR: a still dragon for DRAGON_STILL_MS. */
90
+ export function dragonPlaying(now = Date.now(), still = isQuiet() || mono()) {
91
+ if (!dragonNow) return null;
92
+ const span = still ? DRAGON_STILL_MS : CLIPS[dragonNow.clip].dur + 300;
93
+ return now - dragonNow.start < span ? { ...dragonNow, t: now - dragonNow.start, still } : null;
94
+ }
95
+ export const dragonActive = (now = Date.now(), still) => dragonPlaying(now, still) != null;
96
+ /** Test hook: forget any clip in flight. */
97
+ export const resetDragon = () => { dragonNow = null; };
85
98
 
86
99
  /**
87
- * Mounted once in the live region; renders nothing until /eye. Then the Q opens its eye, blinks once and
88
- * dissolves, and the component goes back to nothing (it never enters history). Its timer runs only while
89
- * the eye plays. Reduced motion: a still open eye for two seconds.
100
+ * Mounted once in the live region; renders nothing until /dragon (or a cinematic mood). The clip plays in the
101
+ * live region and never enters history. Short terminals (under 32 rows) get the compact dragon. Reduced
102
+ * motion or NO_COLOR: the static dragon (shapes only under NO_COLOR) for two seconds.
103
+ */
104
+ /**
105
+ * TUI-UX #5 (shared with the /mirror guard): the full dragon is 25 rows and the live region also carries the margin, caption,
106
+ * composer and status rows (about 12). When that total reaches the terminal height Ink wipes the whole screen on every frame,
107
+ * so the dragon only goes full size when it clearly fits; otherwise the 15-row dragon plays.
90
108
  */
91
- export function EyeLive({ cols, rows }) {
109
+ export const DRAGON_FULL_MIN_ROWS = 40;
110
+ export const dragonTall = (rows) => rows >= DRAGON_FULL_MIN_ROWS;
111
+
112
+ export function DragonLive({ cols, rows }) {
92
113
  const { stdout } = useStdout();
93
- const onClock = React.useContext(FrameContext) != null; // inside the app: the shared clock drives the frames
114
+ const onClock = React.useContext(FrameContext) != null;
94
115
  useFrame();
95
116
  const [, bump] = React.useState(0);
96
- React.useEffect(() => { const fn = () => bump((x) => x + 1); listeners.add(fn); return () => listeners.delete(fn); }, []);
97
- const quiet = isQuiet();
98
- const playing = eyeStart != null && Date.now() - eyeStart < (quiet ? 2000 : EYE_MS + 400);
117
+ React.useEffect(() => { const fn = () => bump((x) => x + 1); dragonListeners.add(fn); return () => dragonListeners.delete(fn); }, []);
118
+ const now = Date.now();
119
+ const p = dragonPlaying(now);
99
120
  React.useEffect(() => {
100
- if (!playing || onClock) return undefined; // standalone only (no shared clock): its own bounded timer
101
- const id = setInterval(() => { bump((x) => x + 1); if (Date.now() - eyeStart >= (quiet ? 2000 : EYE_MS + 400)) clearInterval(id); }, quiet ? 500 : 50);
121
+ if (!p || onClock) return undefined; // standalone only (no shared clock): its own bounded timer
122
+ const id = setInterval(() => { bump((x) => x + 1); if (!dragonActive()) clearInterval(id); }, p.still ? 500 : 40);
102
123
  return () => clearInterval(id);
103
- }, [playing, quiet, onClock]);
104
- if (!playing) return null;
105
- const size = labSize(cols, rows ?? termRows(stdout) ?? 40);
106
- const t = quiet ? EYE_MS * 0.42 : Date.now() - eyeStart;
107
- const cells = eyeCellsAt(size, t, { color: !mono() });
108
- return h(Box, { flexDirection: 'row', marginTop: 1 },
109
- h(Box, { flexDirection: 'column' }, ...cells.map((row, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(row, `e${k}-`)))),
110
- cols >= 60 ? h(Box, { marginLeft: 2, flexDirection: 'column', justifyContent: 'center' }, h(Text, { color: c('dim') }, "the Dragon's Eye")) : null);
124
+ }, [Boolean(p), p?.still, onClock]);
125
+ if (!p) return null;
126
+ const tall = dragonTall(rows ?? termRows(stdout) ?? 40);
127
+ const cells = clipCells(p.clip, p.t, { cols, theme: isLight() ? 'light' : 'dark', mono: mono(), still: p.still, size: tall ? undefined : 'mid' });
128
+ const caption = p.still ? 'the dragon' : clipCaption(p.clip, p.t);
129
+ return h(Box, { flexDirection: 'column', marginTop: 1 },
130
+ ...cells.map((row, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(row, `d${k}-`))),
131
+ h(Text, { color: c('dim') }, clip(caption, Math.max(10, cols - 2))));
111
132
  }