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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /model: which provider (and optionally which model) the next turns ask the server for.
|
|
3
|
+
* Pure, apart from the vault list result passed in.
|
|
4
|
+
*
|
|
5
|
+
* The choice is a REQUEST: the stream body carries `provider` (and `model`), and the collector's
|
|
6
|
+
* policy plan (resolveCollectorPolicyPlan) decides. turn.started.provider/model is the truth; when it
|
|
7
|
+
* differs from what was asked, the client says so once (never a silent fallback).
|
|
8
|
+
* null choice = auto (no provider in the body; the server's policy picks).
|
|
9
|
+
*
|
|
10
|
+
* Availability is the same source as /connect: the org's default QuoxVault credential per provider.
|
|
11
|
+
* Model ids mirror src/config/modelCatalog.js (openai + anthropic rows); an id not listed is passed
|
|
12
|
+
* through with a note, the server decides whether it exists.
|
|
13
|
+
*/
|
|
14
|
+
import { PROVIDERS as CONNECT, pickRow, offerText } from './connect.js';
|
|
15
|
+
|
|
16
|
+
export const CHOICES = [
|
|
17
|
+
{ id: 'openai', provider: 'openai', name: 'OpenAI', vault: 'openai', connect: 'openai', models: ['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano'] },
|
|
18
|
+
{ id: 'anthropic', provider: 'anthropic', name: 'Claude API', vault: 'anthropic', connect: 'anthropic', models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'] },
|
|
19
|
+
// B3: served by the chat stream; models are whatever that Ollama has installed (listed by the server).
|
|
20
|
+
{ id: 'ollama', provider: 'ollama', name: 'Ollama', vault: 'ollama', connect: 'ollama', dynamic: true, hint: 'http://<host>:11434', models: [] },
|
|
21
|
+
// B5: OpenAI-compatible presets. Models come from the provider's own /models (api.compatModels).
|
|
22
|
+
// hosted: a model is required (listed, never defaulted); selfHosted: the connection default or the first served.
|
|
23
|
+
...[
|
|
24
|
+
['openrouter', 'OpenRouter', 'openrouter', true, ''], ['huggingface', 'Hugging Face', 'huggingface', true, ''],
|
|
25
|
+
['groq', 'Groq', 'groq', true, ''], ['together', 'Together', 'together', true, ''],
|
|
26
|
+
['lmstudio', 'LM Studio', 'lmstudio', false, ' http://<host>:1234'], ['vllm', 'vLLM', 'vllm', false, ' http://<host>:8000'],
|
|
27
|
+
['custom', 'Custom', 'openai_compatible', false, ' https://<host>/v1']
|
|
28
|
+
].map(([id, name, vault, hosted, hint]) => ({ id, provider: id, name, vault, connect: id, dynamic: true, compat: true, hosted, selfHosted: !hosted, hint: hint.trim(), models: [] }))
|
|
29
|
+
];
|
|
30
|
+
const ALIASES = { gpt: 'openai', chatgpt: 'openai', claude: 'anthropic', 'claude-api': 'anthropic', 'anthropic-api': 'anthropic', local: 'ollama', hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom', default: 'auto', policy: 'auto' };
|
|
31
|
+
const byProvider = (p) => CHOICES.find(c => c.provider === p) || null;
|
|
32
|
+
|
|
33
|
+
/** Stored or flag value -> { provider, model? } | null. Only the two known fields survive. */
|
|
34
|
+
export function normChoice(v) {
|
|
35
|
+
if (!v || typeof v !== 'object' || typeof v.provider !== 'string' || !byProvider(v.provider)) return null;
|
|
36
|
+
const out = typeof v.model === 'string' && v.model.trim() ? { provider: v.provider, model: v.model.trim() } : { provider: v.provider };
|
|
37
|
+
// B3/B5: the Ollama / self-hosted host, for the status line before the first turn (turn.started.endpoint wins after).
|
|
38
|
+
if ((v.provider === 'ollama' || byProvider(v.provider).selfHosted) && typeof v.host === 'string' && v.host.trim()) out.host = v.host.trim().slice(0, 120);
|
|
39
|
+
// D3: prompt profile (compact | full); absent = the server default (compact on ollama / local presets, full elsewhere).
|
|
40
|
+
if (PROFILES.includes(v.profile)) out.profile = v.profile;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const PROFILES = ['compact', 'full'];
|
|
45
|
+
|
|
46
|
+
/** "/model <provider> [model] [compact|full]" text -> { ok:true, choice } (choice null = auto) | { ok:false, error }. */
|
|
47
|
+
export function parseModelArg(arg) {
|
|
48
|
+
const words = String(arg || '').trim().split(/\s+/).filter(Boolean);
|
|
49
|
+
const profile = words.length > 1 && PROFILES.includes(String(words[words.length - 1]).toLowerCase()) ? words.pop().toLowerCase() : null;
|
|
50
|
+
const [rawName, model] = words;
|
|
51
|
+
const n = String(rawName || '').toLowerCase();
|
|
52
|
+
const id = ALIASES[n] || n;
|
|
53
|
+
if (id === 'auto') return { ok: true, choice: null };
|
|
54
|
+
const c = CHOICES.find(x => x.id === id);
|
|
55
|
+
if (!c) return { ok: false, error: `No provider called "${rawName}". Use ${CHOICES.map(x => x.id).join(', ')} or auto (/model lists them).` };
|
|
56
|
+
const choice = model ? { provider: c.provider, model } : { provider: c.provider };
|
|
57
|
+
if (profile) choice.profile = profile;
|
|
58
|
+
return { ok: true, choice };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** -> 'connected' | 'stored' | 'missing' | 'unknown' (+ the row) for one choice. */
|
|
62
|
+
function availability(res, c) {
|
|
63
|
+
if (!res || !res.ok || !Array.isArray(res.rows)) return { state: 'unknown' };
|
|
64
|
+
const { def, any } = pickRow(res.rows, c.vault);
|
|
65
|
+
if (def) return { state: 'connected', row: def };
|
|
66
|
+
if (any) return { state: 'stored', row: any };
|
|
67
|
+
return { state: 'missing' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const same = (a, b) => (a?.provider || null) === (b?.provider || null);
|
|
71
|
+
|
|
72
|
+
/** -> [[text, tone?]] lines for /model. */
|
|
73
|
+
export function modelLines(res, current) {
|
|
74
|
+
const out = [['Model for your next turns (the server decides; turn.started shows what ran):']];
|
|
75
|
+
const mark = (on) => (on ? '▸' : ' ');
|
|
76
|
+
out.push([`${mark(!current)} auto server policy picks the provider and model · /model auto`]);
|
|
77
|
+
for (const c of CHOICES) {
|
|
78
|
+
const a = availability(res, c);
|
|
79
|
+
const picked = same(current, c);
|
|
80
|
+
const sel = picked && current.model ? ` · using ${current.model}` : '';
|
|
81
|
+
if (c.served === false) {
|
|
82
|
+
out.push([`${mark(picked)} ${c.name} ${a.state === 'connected' ? 'connected, but ' : ''}not served by the chat stream yet`]);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c.dynamic) {
|
|
86
|
+
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected · ${a.row.label || a.row.id}${sel} · /model ${c.id} lists its ${c.compat ? '' : 'installed '}models`, 'ok']);
|
|
87
|
+
else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
|
|
88
|
+
else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`]);
|
|
89
|
+
else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const models = c.models.join(', ');
|
|
93
|
+
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected${sel} · /model ${c.id} [${models}]`, 'ok']);
|
|
94
|
+
else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
|
|
95
|
+
else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}`]);
|
|
96
|
+
else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
|
|
97
|
+
}
|
|
98
|
+
out.push([' ? Claude (subscription) dev-only · reached via auto or anthropic when no Claude API key is stored; no status route']);
|
|
99
|
+
if (!res?.ok) out.push([` could not read the vault: ${res?.error || 'no answer'}`, 'warn']);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The /model <arg> decision. -> { ok, choice?, text, tone? }. A provider the vault says is not usable is
|
|
105
|
+
* refused (with the /connect offer), never quietly swapped for another.
|
|
106
|
+
*/
|
|
107
|
+
export function selectModel(res, arg) {
|
|
108
|
+
const p = parseModelArg(arg);
|
|
109
|
+
if (!p.ok) return { ok: false, text: p.error, tone: 'warn' };
|
|
110
|
+
if (!p.choice) return { ok: true, choice: null, text: 'Model: auto. The server policy picks the provider and model.', tone: 'ok' };
|
|
111
|
+
const c = byProvider(p.choice.provider);
|
|
112
|
+
if (c.served === false) return { ok: false, text: `${c.name} is not served by the chat stream yet, so I can't use it here. /model lists what can.`, tone: 'warn' };
|
|
113
|
+
const a = availability(res, c);
|
|
114
|
+
if (c.dynamic && a.state === 'missing') return { ok: false, text: `${c.name} isn't connected, so I can't use it yet. Connect it: /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`, tone: 'warn' };
|
|
115
|
+
if (a.state === 'missing') return { ok: false, text: offerText(CONNECT[c.connect]), tone: 'warn' };
|
|
116
|
+
if (a.state === 'stored') return { ok: false, text: `${c.name} has a key stored, but not as the org default, so the server will not use it. Fix: quox vault set-default ${a.row.id}`, tone: 'warn' };
|
|
117
|
+
const label = p.choice.model ? `${c.id} ${p.choice.model}` : c.id;
|
|
118
|
+
const extra = p.choice.model && !c.dynamic && !c.models.includes(p.choice.model) ? ` ${p.choice.model} is not in this client's list; the server decides whether it exists.` : '';
|
|
119
|
+
if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label}. Could not confirm ${c.name} is connected (vault not readable); the server will say if it is not.${extra}`, tone: 'warn' };
|
|
120
|
+
return { ok: true, choice: p.choice, text: `Model: ${label}. Your next turns ask for it.${extra}`, tone: extra ? 'warn' : 'ok' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* B3: /model ollama [model] against the server's installed-model list (res from api.ollamaModels()).
|
|
125
|
+
* -> { ok, choice?, text, tone, rows? } ; rows = the installed list to show.
|
|
126
|
+
*/
|
|
127
|
+
export function selectOllamaModel(res, model) {
|
|
128
|
+
if (!res?.ok) {
|
|
129
|
+
if (res?.error === 'not_connected') return { ok: false, text: 'Ollama is not connected. /connect ollama http://<host>:11434', tone: 'warn' };
|
|
130
|
+
const why = res?.message || res?.error || 'no answer';
|
|
131
|
+
return model
|
|
132
|
+
? { ok: true, choice: { provider: 'ollama', model }, text: `Model: ollama ${model}. Could not list Ollama's models (${why}); the server will say if it is missing.`, tone: 'warn' }
|
|
133
|
+
: { ok: false, text: `Could not list Ollama's models: ${why}`, tone: 'warn' };
|
|
134
|
+
}
|
|
135
|
+
const models = Array.isArray(res.models) ? res.models : [];
|
|
136
|
+
const names = models.map(m => m.name);
|
|
137
|
+
const host = res.host || null;
|
|
138
|
+
const withHost = (c) => (host ? { ...c, host } : c);
|
|
139
|
+
if (!model) {
|
|
140
|
+
const def = res.default_model || names[0] || null;
|
|
141
|
+
if (!def) return { ok: false, text: `Ollama at ${host} has no models installed. On that machine run: ollama pull qwen2.5`, tone: 'warn' };
|
|
142
|
+
return { ok: true, choice: withHost({ provider: 'ollama' }), text: `Model: Ollama · ${host} · ${def} (the connection's default). Installed models below; pick one with /model ollama <model>.`, tone: 'ok', models, current: def };
|
|
143
|
+
}
|
|
144
|
+
const hit = names.includes(model) ? model : (names.includes(`${model}:latest`) ? `${model}:latest` : null);
|
|
145
|
+
if (!hit) return { ok: false, text: `${model} is not installed on ${host}. On that machine run: ollama pull ${model}`, tone: 'warn', models };
|
|
146
|
+
return { ok: true, choice: withHost({ provider: 'ollama', model: hit }), text: `Model: Ollama · ${host} · ${hit}. Your next turns ask for it.`, tone: 'ok' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One compat row of a /models list: id (strings pass through). */
|
|
150
|
+
const modelId = (m) => (typeof m === 'string' ? m : m?.id);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* B5: /model <preset> [model] against the provider's own /models (res from api.compatModels(preset)).
|
|
154
|
+
* Hosted presets never default a model: with none given the list is shown and no choice is set.
|
|
155
|
+
* -> { ok, choice?, text, tone, models?, current? }
|
|
156
|
+
*/
|
|
157
|
+
export function selectCompatModel(res, presetId, model) {
|
|
158
|
+
const c = byProvider(presetId);
|
|
159
|
+
const connectCmd = `/connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`;
|
|
160
|
+
if (!res?.ok) {
|
|
161
|
+
if (res?.error === 'not_connected') return { ok: false, text: `${c.name} is not connected. ${connectCmd}`, tone: 'warn' };
|
|
162
|
+
const why = res?.message || res?.error || 'no answer';
|
|
163
|
+
return model
|
|
164
|
+
? { ok: true, choice: { provider: c.provider, model }, text: `Model: ${c.id} ${model}. Could not list ${c.name}'s models (${why}); the server will say if it is missing.`, tone: 'warn' }
|
|
165
|
+
: { ok: false, text: `Could not list ${c.name}'s models: ${why}`, tone: 'warn' };
|
|
166
|
+
}
|
|
167
|
+
const models = Array.isArray(res.models) ? res.models : [];
|
|
168
|
+
const ids = models.map(modelId).filter(Boolean);
|
|
169
|
+
const host = c.selfHosted ? (res.host || null) : null;
|
|
170
|
+
const withHost = (ch) => (host ? { ...ch, host } : ch);
|
|
171
|
+
if (!model) {
|
|
172
|
+
if (c.hosted) return { ok: false, text: `${c.name} needs a model. Pick one with /model ${c.id} <model>${ids.length ? ' (listed below)' : ''}.`, tone: 'warn', models };
|
|
173
|
+
const def = res.default_model || ids[0] || null;
|
|
174
|
+
if (!def) return { ok: false, text: `${c.name} at ${host} serves no models yet.`, tone: 'warn' };
|
|
175
|
+
return { ok: true, choice: withHost({ provider: c.provider }), text: `Model: ${c.name} · ${host} · ${def} (the connection's default). Models below; pick one with /model ${c.id} <model>.`, tone: 'ok', models, current: def };
|
|
176
|
+
}
|
|
177
|
+
if (ids.length && !ids.includes(model)) return { ok: false, text: `${model} is not served by ${host || c.name}. Models below.`, tone: 'warn', models };
|
|
178
|
+
if (!ids.length) return { ok: true, choice: withHost({ provider: c.provider, model }), text: `Model: ${c.id} ${model}. ${c.name} listed no models; the server will say if it is missing.`, tone: 'warn' };
|
|
179
|
+
return { ok: true, choice: withHost({ provider: c.provider, model }), text: `Model: ${c.name}${host ? ` · ${host}` : ''} · ${model}. Your next turns ask for it.`, tone: 'ok' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Status-line text: what the server said it ran, else what is selected; null for auto before any turn (keeps the line short). */
|
|
183
|
+
export function modelLabel(ctx, choice) {
|
|
184
|
+
if (ctx?.model && ctx.provider === 'ollama') return `Ollama · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
|
|
185
|
+
const cc = ctx?.provider ? byProvider(ctx.provider) : null;
|
|
186
|
+
if (ctx?.model && cc?.compat) return cc.hosted ? `${cc.name} · ${ctx.model}` : `${cc.name} · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
|
|
187
|
+
if (ctx?.model) return ctx.provider ? `${ctx.provider} ${ctx.model}` : ctx.model;
|
|
188
|
+
if (!choice) return null;
|
|
189
|
+
if (choice.provider === 'ollama') return `Ollama · ${choice.host || '?'} · ${choice.model || 'default model'}`;
|
|
190
|
+
const ch = byProvider(choice.provider);
|
|
191
|
+
if (ch?.compat) return ch.hosted ? `${ch.name} · ${choice.model || 'pick a model'}` : `${ch.name} · ${choice.host || '?'} · ${choice.model || 'default model'}`;
|
|
192
|
+
return choice.model ? `${choice.provider} ${choice.model}` : choice.provider;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One line when turn.started shows the server ran something other than what was asked; else null. */
|
|
196
|
+
export function fallbackNote(choice, ev) {
|
|
197
|
+
if (!choice || !ev || ev.type !== 'turn.started' || !ev.provider) return null;
|
|
198
|
+
const ran = `${ev.provider}${ev.model ? ` ${ev.model}` : ''}`;
|
|
199
|
+
if (ev.provider !== choice.provider) return `You asked for ${choice.provider}; the server ran ${ran} (its policy or a fallback). /model shows what is connected.`;
|
|
200
|
+
if (choice.model && ev.model && ev.model !== choice.model) return `You asked for ${choice.model}; the server ran ${ran}.`;
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Fields for the stream body. */
|
|
205
|
+
export const streamFields = (choice) => (choice ? { provider: choice.provider, ...(choice.model ? { model: choice.model } : {}), ...(choice.profile ? { promptProfile: choice.profile } : {}) } : {});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The motion engine's one clock (CLI-QUOX motion). Every animation and every visible tick in the client
|
|
3
|
+
* (activity word decode, Q state colour, elapsed timer, roster clock, composer ring, drop-in reveals, intro,
|
|
4
|
+
* approval arming) declares a DEMAND each render; the clock keeps exactly one timer armed for the soonest
|
|
5
|
+
* demand and none at all when nothing asks. Idle is still: no timers, no renders.
|
|
6
|
+
*
|
|
7
|
+
* Demand shapes: { every: ms } (repeat while declared) or { at: epochMs } (one wake at that time).
|
|
8
|
+
*/
|
|
9
|
+
export const FRAME_MS = 83; // ~12 fps for anything that moves
|
|
10
|
+
export const SECOND_MS = 1000;
|
|
11
|
+
|
|
12
|
+
/** Delay until the next wake for these demands, or null when nothing is waiting. Pure. */
|
|
13
|
+
export function nextDelay(demands, now) {
|
|
14
|
+
let best = null;
|
|
15
|
+
for (const d of demands || []) {
|
|
16
|
+
if (!d) continue;
|
|
17
|
+
let ms = null;
|
|
18
|
+
if (Number.isFinite(d.every) && d.every > 0) {
|
|
19
|
+
// second ticks align to the wall-clock second so "3s" changes when a second actually passes
|
|
20
|
+
ms = d.every >= SECOND_MS && Number.isFinite(d.since) ? d.every - ((now - d.since) % d.every) : d.every;
|
|
21
|
+
} else if (Number.isFinite(d.at)) ms = Math.max(0, d.at - now);
|
|
22
|
+
if (ms != null && (best == null || ms < best)) best = ms;
|
|
23
|
+
}
|
|
24
|
+
return best == null ? null : Math.max(1, Math.round(best));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Clock {
|
|
28
|
+
constructor(onTick, timers = { setTimeout, clearTimeout }) {
|
|
29
|
+
this.onTick = onTick; this.timers = timers; this.id = null; this.dueAt = null; this.stopped = false;
|
|
30
|
+
this.ticks = 0; this.lastTickAt = null;
|
|
31
|
+
}
|
|
32
|
+
/** Re-arm for the given demands (called after every render). Keeps the timer when it is already due sooner or equal. */
|
|
33
|
+
schedule(demands, now = Date.now()) {
|
|
34
|
+
if (this.stopped) return null;
|
|
35
|
+
const d = nextDelay(demands, now);
|
|
36
|
+
if (d == null) { this.cancel(); return null; }
|
|
37
|
+
const due = now + d;
|
|
38
|
+
if (this.id != null && this.dueAt != null && this.dueAt <= due + 2) return this.dueAt - now;
|
|
39
|
+
this.cancel();
|
|
40
|
+
this.dueAt = due;
|
|
41
|
+
this.id = this.timers.setTimeout(() => {
|
|
42
|
+
this.id = null; this.dueAt = null; this.ticks++; this.lastTickAt = Date.now();
|
|
43
|
+
if (!this.stopped) this.onTick();
|
|
44
|
+
}, d);
|
|
45
|
+
return d;
|
|
46
|
+
}
|
|
47
|
+
cancel() { if (this.id != null) this.timers.clearTimeout(this.id); this.id = null; this.dueAt = null; }
|
|
48
|
+
stop() { this.stopped = true; this.cancel(); }
|
|
49
|
+
get armed() { return this.id != null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Motion is decoration: off under QUOX_REDUCED_MOTION=1, QUOX_TUI_LITE, or when not on a real terminal. Colour is separate (NO_COLOR). */
|
|
53
|
+
export const reducedMotion = (env = process.env) => env.QUOX_REDUCED_MOTION === '1' || Boolean(env.QUOX_TUI_LITE);
|
|
54
|
+
export const motionAllowed = (isTTY, env = process.env) => Boolean(isTTY) && !reducedMotion(env);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Frame source: the clock's ticks fan out to the animated leaves only (Working, the composer ring, items that
|
|
58
|
+
* are revealing), so a frame re-renders those components instead of the whole app. get() is the frame time.
|
|
59
|
+
*/
|
|
60
|
+
export function createFrames(now = Date.now()) {
|
|
61
|
+
let t = now;
|
|
62
|
+
const subs = new Set();
|
|
63
|
+
return {
|
|
64
|
+
get: () => t,
|
|
65
|
+
subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); },
|
|
66
|
+
emit(at = Date.now()) { t = at; for (const fn of [...subs]) fn(); },
|
|
67
|
+
get size() { return subs.size; }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Colour helpers for the motion engine: rgb triples, mixing, hex parsing. Pure. */
|
|
2
|
+
export const hexRgb = (hex) => {
|
|
3
|
+
const m = String(hex || '').replace('#', '');
|
|
4
|
+
const v = m.length === 3 ? m.split('').map((x) => x + x).join('') : m;
|
|
5
|
+
return [0, 2, 4].map((i) => parseInt(v.slice(i, i + 2), 16) || 0);
|
|
6
|
+
};
|
|
7
|
+
export const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);
|
|
8
|
+
export const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
9
|
+
export const rgbStr = (rgb) => `rgb(${rgb.map((v) => Math.max(0, Math.min(255, Math.round(v)))).join(',')})`;
|
|
10
|
+
export const smooth = (x) => { const t = clamp01(x); return t * t * (3 - 2 * t); };
|
|
11
|
+
/** Deterministic hash in [0,1) so a frame is reproducible from (cell, time bucket, salt). */
|
|
12
|
+
export function hash3(a, b, c) {
|
|
13
|
+
let h = (a * 374761393 + b * 668265263 + c * 2147483647) | 0;
|
|
14
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
15
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---- perceptual interpolation (OKLab) --------------------------------------------------------------
|
|
19
|
+
const toLin = (v) => { const c = v / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
|
|
20
|
+
const toSrgb = (c) => { const v = c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055; return Math.max(0, Math.min(255, v * 255)); };
|
|
21
|
+
export function rgbToOklab([r, g, b]) {
|
|
22
|
+
const R = toLin(r), G = toLin(g), B = toLin(b);
|
|
23
|
+
const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B);
|
|
24
|
+
const m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B);
|
|
25
|
+
const s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B);
|
|
26
|
+
return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
|
|
27
|
+
}
|
|
28
|
+
export function oklabToRgb([L, a, b]) {
|
|
29
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3, m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3, s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
30
|
+
return [toSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), toSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), toSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)];
|
|
31
|
+
}
|
|
32
|
+
/** Mix two rgb colours in OKLab (perceptually even: no muddy midpoint). */
|
|
33
|
+
export const mixOk = (a, b, t) => { const A = rgbToOklab(a), B = rgbToOklab(b); return oklabToRgb(A.map((v, i) => v + (B[i] - v) * t)); };
|
|
34
|
+
/** A smooth 0..1 pulse with period `ms` (cosine ease, no hard edges). */
|
|
35
|
+
export const pulse01 = (now, ms = 1200) => 0.5 - 0.5 * Math.cos((2 * Math.PI * (now % ms)) / ms);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** React side of the frame source: a context plus a hook that re-renders the caller on every clock frame. */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export const FrameContext = React.createContext(null);
|
|
5
|
+
const noop = () => () => {};
|
|
6
|
+
/**
|
|
7
|
+
* Re-render the caller on every clock frame and return the current time (ms). The wall clock, not the frame
|
|
8
|
+
* stamp, so a render caused by anything else is never stale. Outside a provider it returns `fallback`.
|
|
9
|
+
*/
|
|
10
|
+
export function useFrame(fallback) {
|
|
11
|
+
const frames = React.useContext(FrameContext);
|
|
12
|
+
React.useSyncExternalStore(frames ? frames.subscribe : noop, frames ? frames.get : () => 0);
|
|
13
|
+
return frames ? Date.now() : (fallback ?? Date.now());
|
|
14
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in reveals (lab prototype `quox-intro-lab fx`): when an item lands in the live region its text decodes
|
|
3
|
+
* in left to right. Just ahead of the resolving edge (LEAD) cells flicker as curated half-width katakana;
|
|
4
|
+
* further ahead they are blank; just behind it a phosphor afterglow tints the real character and fades.
|
|
5
|
+
* Decorative only: every cell is the real character once the reveal ends, widths are identical every frame
|
|
6
|
+
* (a katakana or blank stands only in a one-column cell; wide graphemes are never replaced by a glyph).
|
|
7
|
+
* Pure: (text, elapsed seconds, timing) -> cells.
|
|
8
|
+
*/
|
|
9
|
+
import stringWidth from 'string-width';
|
|
10
|
+
import { SCRAMBLE_GLYPHS } from '../indicator.js';
|
|
11
|
+
import { hash3 } from './color.js';
|
|
12
|
+
import { graphemes } from '../textBuffer.js';
|
|
13
|
+
|
|
14
|
+
export const LEAD = 0.14;
|
|
15
|
+
/** Timings (seconds). row: tool rows, strongest. dim: grey lines (closing line, notes); short glow, never bold. */
|
|
16
|
+
export const TIMING = {
|
|
17
|
+
row: { dur: 0.28, glow: 0.7, bold: true },
|
|
18
|
+
dim: { dur: 0.4, glow: 0.15, bold: false },
|
|
19
|
+
title: { dur: 0.3, glow: 0.35, bold: true },
|
|
20
|
+
roster: { dur: 0.3, glow: 0.3, bold: false },
|
|
21
|
+
settle: { dur: 0.2, glow: 0.5, bold: true },
|
|
22
|
+
};
|
|
23
|
+
export const revealSpan = (t) => t.dur + t.glow; // seconds from start until every cell is plain
|
|
24
|
+
const KEEP = new Set([' ', '│', '─']); // structure only: a '·' separator arrives with its text, never floats ahead of it
|
|
25
|
+
const TEXTISH = /^[\p{L}\p{N}\p{P}]+$/u;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cells for `text` at `elapsed` seconds since the reveal started.
|
|
29
|
+
* Each cell: { ch, kind: 'plain'|'kana'|'blank'|'glow', g (glow 0..1), bold }.
|
|
30
|
+
* offset/total let several segments of one row share one sweeping edge.
|
|
31
|
+
*/
|
|
32
|
+
export function revealCells(text, elapsed, timing = TIMING.row, { offset = 0, total = null, salt = 1 } = {}) {
|
|
33
|
+
const gs = graphemes(String(text ?? '')).map((g) => g.text);
|
|
34
|
+
const n = total ?? gs.length;
|
|
35
|
+
const bucket = Math.floor(elapsed * 30);
|
|
36
|
+
return gs.map((ch, j) => {
|
|
37
|
+
const i = offset + j;
|
|
38
|
+
// Only text decodes: letters, digits, punctuation. Icons and symbols (◐ ✔ ⚠ ⛓ ◉ …) never swap glyphs (owner 2026-10-12).
|
|
39
|
+
if (KEEP.has(ch) || !TEXTISH.test(ch)) return { ch, kind: 'plain', g: 0, bold: false };
|
|
40
|
+
const r = n > 1 ? (i / (n - 1)) * timing.dur : 0;
|
|
41
|
+
const dt = elapsed - r;
|
|
42
|
+
const w = stringWidth(ch);
|
|
43
|
+
if (dt < 0) {
|
|
44
|
+
if (w !== 1) return { ch: ' '.repeat(Math.max(1, w)), kind: 'blank', g: 0, bold: false };
|
|
45
|
+
if (dt < -LEAD) return { ch: ' ', kind: 'blank', g: 0, bold: false };
|
|
46
|
+
return { ch: SCRAMBLE_GLYPHS[Math.floor(hash3(i, bucket, salt) * SCRAMBLE_GLYPHS.length)], kind: 'kana', g: hash3(i, bucket + 7, salt), bold: false };
|
|
47
|
+
}
|
|
48
|
+
const g = timing.glow > 0 && dt < timing.glow ? (1 - dt / timing.glow) ** 1.6 : 0;
|
|
49
|
+
return g > 0 ? { ch, kind: 'glow', g, bold: Boolean(timing.bold) && (dt < 0.05 || g > 0.55) } : { ch, kind: 'plain', g: 0, bold: false };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** True when the reveal has fully ended (all cells plain). */
|
|
54
|
+
export const revealDone = (elapsed, timing) => elapsed >= revealSpan(timing);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer ring (owner-approved lab prototype `quox-intro-lab rules`, plus the green writing sweep from
|
|
3
|
+
* `quox-intro-lab fx`). The two composer rules are one light ring: top rule left to right is the first half
|
|
4
|
+
* of the perimeter, bottom rule right to left the second. The gradient is the quox.ai panel ring:
|
|
5
|
+
* conic(cyan .85, purple .8 @35%, transparent @60%, teal .55 @85%, cyan .85). ~1 revolution / 2.5 s.
|
|
6
|
+
* Modes: work (cyan/purple), write (greens, while answer text streams), wait (slower amber, approval held).
|
|
7
|
+
* When the turn ends the sweep eases to a stop over ~400 ms. Pure state stepping; no timers here.
|
|
8
|
+
*/
|
|
9
|
+
import { mix, smooth, hexRgb } from './color.js';
|
|
10
|
+
|
|
11
|
+
export const PALS = {
|
|
12
|
+
work: [[0x38, 0xbd, 0xf8], [0xa8, 0x55, 0xf7], [0x06, 0xb6, 0xd4]],
|
|
13
|
+
write: [[0x22, 0xc5, 0x5e], [0x4a, 0xde, 0x80], [0x16, 0xa3, 0x4a]],
|
|
14
|
+
wait: [[0xf5, 0x9e, 0x0b], [0xea, 0x58, 0x0c], [0xb4, 0x53, 0x09]],
|
|
15
|
+
};
|
|
16
|
+
const STOPS = [[0, 0, 0.85], [0.35, 1, 0.8], [0.6, 0, 0], [0.85, 2, 0.55], [1, 0, 0.85]];
|
|
17
|
+
export const DIM = [58, 66, 82];
|
|
18
|
+
export const OMEGA = 360 / 2.5; // deg/s while working
|
|
19
|
+
export const SPEED = { work: 1, write: 1, wait: 0.3 };
|
|
20
|
+
export const SETTLE_S = 0.4, RAMP_S = 0.15, BLEND_S = 0.2;
|
|
21
|
+
|
|
22
|
+
export const initialRing = () => ({ phase: 'idle', angle: 0, k: 0, om: 0, wt: { work: 1, write: 0, wait: 0 }, at: null, settleAt: null, from: null });
|
|
23
|
+
|
|
24
|
+
/** Colour + alpha of the ring at perimeter position u in [0,1) for palette weights wt. */
|
|
25
|
+
export function ringAt(u, wt) {
|
|
26
|
+
let i = 0; while (i < STOPS.length - 2 && u > STOPS[i + 1][0]) i++;
|
|
27
|
+
const [p0, c0, a0] = STOPS[i], [p1, c1, a1] = STOPS[i + 1];
|
|
28
|
+
const t = (u - p0) / (p1 - p0 || 1);
|
|
29
|
+
const col = (c) => [0, 1, 2].map((ch) => wt.work * PALS.work[c][ch] + wt.write * PALS.write[c][ch] + wt.wait * PALS.wait[c][ch]);
|
|
30
|
+
const A = a0 + (a1 - a0) * t;
|
|
31
|
+
const pm = mix(col(c0).map((v) => v * a0), col(c1).map((v) => v * a1), t); // premultiplied: the clear stop does not tint
|
|
32
|
+
return [A ? pm.map((v) => v / A) : col(c0), A];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Advance the ring to time `now` (ms). active = a turn is running; mode = 'work' | 'write' | 'wait'.
|
|
37
|
+
* Returns a new state; `animating` tells the clock whether frames are still wanted.
|
|
38
|
+
*/
|
|
39
|
+
export function ringStep(prev, { active, mode = 'work', now }) {
|
|
40
|
+
const s = { ...(prev || initialRing()), wt: { ...(prev || initialRing()).wt } };
|
|
41
|
+
const dt = s.at == null ? 0 : Math.max(0, Math.min(0.25, (now - s.at) / 1000));
|
|
42
|
+
s.at = now;
|
|
43
|
+
if (active) {
|
|
44
|
+
if (s.phase !== 'run') s.phase = 'run';
|
|
45
|
+
s.om = OMEGA * (SPEED[mode] ?? 1);
|
|
46
|
+
s.k = Math.min(1, s.k + dt / RAMP_S);
|
|
47
|
+
for (const m of Object.keys(s.wt)) s.wt[m] += ((m === mode ? 1 : 0) - s.wt[m]) * Math.min(1, dt / BLEND_S);
|
|
48
|
+
} else if (s.phase === 'run') {
|
|
49
|
+
s.phase = 'settle'; s.settleAt = now; s.from = { om: s.om, k: s.k };
|
|
50
|
+
}
|
|
51
|
+
if (s.phase === 'settle') {
|
|
52
|
+
const p = (now - s.settleAt) / 1000 / SETTLE_S;
|
|
53
|
+
if (p >= 1) { s.phase = 'idle'; s.k = 0; s.om = 0; } else { s.om = s.from.om * (1 - p) ** 2; s.k = s.from.k * (1 - smooth(p)); }
|
|
54
|
+
}
|
|
55
|
+
s.angle = (s.angle + s.om * dt) % 360;
|
|
56
|
+
return s;
|
|
57
|
+
}
|
|
58
|
+
export const ringAnimating = (s) => Boolean(s) && s.phase !== 'idle';
|
|
59
|
+
|
|
60
|
+
/** Per-cell rgb for one rule of `cols` cells. base = the rule's static colour (hex or rgb). Pure. */
|
|
61
|
+
export function ruleColors(cols, top, s, base) {
|
|
62
|
+
const b = Array.isArray(base) ? base : hexRgb(base);
|
|
63
|
+
const out = new Array(cols);
|
|
64
|
+
for (let x = 0; x < cols; x++) {
|
|
65
|
+
if (!s || s.k <= 0) { out[x] = b; continue; }
|
|
66
|
+
const pos = top ? (x + 0.5) / (2 * cols) : (cols + (cols - 1 - x) + 0.5) / (2 * cols);
|
|
67
|
+
const u = (((pos - s.angle / 360) % 1) + 1) % 1;
|
|
68
|
+
const [c, a] = ringAt(u, s.wt);
|
|
69
|
+
out[x] = mix(b, mix(DIM, c, a), s.k);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Which ring mode a live turn is in, from what the client actually knows. */
|
|
75
|
+
export function ringMode({ approvals = 0, act = null } = {}) {
|
|
76
|
+
if (approvals > 0 || act?.tone === 'wait') return 'wait';
|
|
77
|
+
if (act?.word === 'Writing') return 'write';
|
|
78
|
+
return 'work';
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E4 agent card. Sources: the shipped assistant identity (name, tagline), the org agent record
|
|
3
|
+
* (description, status, model when its config carries one), the collector's agent -> tools map
|
|
4
|
+
* (GET /api/v1/tools/agents), slots (GET /api/v1/slots: a slot named after the agent, or assigned
|
|
5
|
+
* to it), and the agent's last AEE envelope (GET /aee/query?from=agent.<id>&limit=1) with its WARD
|
|
6
|
+
* receipt when verify-receipt answered. There is no per-agent model in the shipped config (models
|
|
7
|
+
* are routed per turn), so the model row appears only when the server record names one.
|
|
8
|
+
*/
|
|
9
|
+
import { seg, fit, ago, short, boxWidth } from './model.js';
|
|
10
|
+
import { providerOf } from '../theme.js';
|
|
11
|
+
|
|
12
|
+
/** The model an org agent record names, if any: config_json.{model,provider}. */
|
|
13
|
+
export function modelOf(record) {
|
|
14
|
+
let cfg = record?.config_json ?? record?.config ?? null;
|
|
15
|
+
if (typeof cfg === 'string') { try { cfg = JSON.parse(cfg); } catch { cfg = null; } }
|
|
16
|
+
const model = cfg?.model || cfg?.llm?.model || null;
|
|
17
|
+
const provider = cfg?.provider || cfg?.llm?.provider || null;
|
|
18
|
+
return model || provider ? { model, provider } : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A slot belongs to an agent when it is named after it or assigned to it. */
|
|
22
|
+
export function slotFor(slots, id) {
|
|
23
|
+
const n = String(id || '').toLowerCase();
|
|
24
|
+
return (slots || []).find(s => String(s.name || '').toLowerCase() === n || String(s.assigned_to || '').toLowerCase() === n) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* d: { id, name, tagline, description, status, record, tools: [] | null, slot, last: envelope | null,
|
|
29
|
+
* lastErr, receipt: { ok, result } | null, now }
|
|
30
|
+
*/
|
|
31
|
+
export function agentCard(d, cols) {
|
|
32
|
+
const now = d.now ?? Date.now();
|
|
33
|
+
const w = boxWidth(cols, 72), inner = w - 4, nw = w < 56;
|
|
34
|
+
const nm = String(d.name || d.id || 'agent');
|
|
35
|
+
const tag = d.tagline ? (nw ? '' : ` · ${d.tagline}`) : '';
|
|
36
|
+
const right = d.slot ? (nw ? `slot ${d.slot.name}` : `slot ${d.slot.name} · ${d.slot.state || 'unknown'}`) : '';
|
|
37
|
+
const rows = [];
|
|
38
|
+
const L = (label, value, role = 'value') => [seg(label.padEnd(6), 'muted'), ...(Array.isArray(value) ? value : [seg(fit(value, inner - 6), role)])];
|
|
39
|
+
if (d.description) rows.push(L('role', d.description));
|
|
40
|
+
if (d.status && d.status !== 'active') rows.push(L('state', d.status, 'warn'));
|
|
41
|
+
const m = modelOf(d.record);
|
|
42
|
+
if (m) {
|
|
43
|
+
const p = providerOf(m.provider, m.model);
|
|
44
|
+
rows.push(L('model', [seg(fit(m.model || m.provider, inner - 6), p ? `provider:${p}` : 'value')]));
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(d.tools)) rows.push(L('tools', d.tools.length ? `${d.tools.length}: ${d.tools.join(', ')}` : 'none assigned'));
|
|
47
|
+
else if (d.toolsErr) rows.push(L('tools', `could not read (${d.toolsErr})`, 'warn'));
|
|
48
|
+
if (d.slot?.holder_session && d.slot.state === 'held') rows.push(L('held', `by ${d.slot.holder_session}${d.slot.last_heartbeat_at ? ` · heartbeat ${ago(d.slot.last_heartbeat_at, now)} ago` : ''}`, 'muted'));
|
|
49
|
+
if (d.last) {
|
|
50
|
+
const what = d.last.payload?.tool_name || d.last.intent || 'action';
|
|
51
|
+
const when = d.last.ts ? `${ago(d.last.ts, now)} ago` : '';
|
|
52
|
+
const r = d.receipt?.ok ? d.receipt.result : null;
|
|
53
|
+
if (r?.status === 'WITNESSED') rows.push(L('last', [seg('◉ ', 'proto:ward'), seg(fit(`${what} · WARD seq ${r.entry?.seq} witnessed · ${when}`, inner - 8), 'muted')]));
|
|
54
|
+
else {
|
|
55
|
+
const w8 = r?.status === 'NOT_WITNESSED' ? 'not witnessed yet' : r?.status === 'UNTIPPED' ? `WARD seq ${r.entry?.seq}, no signed tip yet` : d.receipt && !d.receipt.ok ? 'WARD did not answer' : 'witness not checked';
|
|
56
|
+
rows.push(L('last', [seg('· ', 'muted'), seg(fit(`${what} · ${w8} · ${when} · ${short(d.last.id, 14)}`, inner - 8), 'muted')]));
|
|
57
|
+
}
|
|
58
|
+
} else if (d.lastErr) rows.push(L('last', `could not read (${d.lastErr})`, 'warn'));
|
|
59
|
+
else rows.push(L('last', 'no recorded action', 'muted'));
|
|
60
|
+
return { box: { width: w, title: `● ${nm.toUpperCase()}${tag}`, titleRole: `agent:${d.id || nm}`, right, rightRole: 'muted', border: 'violet' }, rows };
|
|
61
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E3 Crystal eligibility, computed from a workflow's REAL execution history against quoxflow's
|
|
3
|
+
* crystalliser gate. There is no read-only eligibility route (POST /api/skills/from-execution is the
|
|
4
|
+
* check AND the crystallisation, a write), so the rule is ported here verbatim from
|
|
5
|
+
* quoxflow/src/pipeline/crystalliser.ts (analyseExecutionHistory + extractVariables):
|
|
6
|
+
* >= 3 executions, >= 80% success, and every successful run follows one action sequence.
|
|
7
|
+
* Input mirrors that route's legacy path: completed + failed executions; a run's action log is
|
|
8
|
+
* output.action_log when the run recorded one, else the workflow definition's nodes (the route's
|
|
9
|
+
* synthetic log). The quoxflow route also reads its own execution-history table, which this
|
|
10
|
+
* client cannot see, so the card says what it counted.
|
|
11
|
+
*/
|
|
12
|
+
import { seg, fit, short, boxWidth, plural } from './model.js';
|
|
13
|
+
|
|
14
|
+
export const MIN_EXECUTIONS = 3;
|
|
15
|
+
export const MIN_SUCCESS_RATE = 0.8;
|
|
16
|
+
|
|
17
|
+
/** crystalliser.ts analyseExecutionHistory, same order of checks and same reasons. */
|
|
18
|
+
export function analyseExecutionHistory(executions) {
|
|
19
|
+
const count = executions.length;
|
|
20
|
+
const successCount = executions.filter(e => e.success).length;
|
|
21
|
+
const successRate = count > 0 ? successCount / count : 0;
|
|
22
|
+
if (count < MIN_EXECUTIONS) return { ready: false, reason: `Insufficient executions (${count}/${MIN_EXECUTIONS} required)`, confidence: 0, executionCount: count, successRate };
|
|
23
|
+
if (successRate < MIN_SUCCESS_RATE) return { ready: false, reason: `Low success rate (${(successRate * 100).toFixed(0)}% < ${MIN_SUCCESS_RATE * 100}% required)`, confidence: 0, executionCount: count, successRate };
|
|
24
|
+
const signatures = new Set(executions.filter(e => e.success).map(e => e.actionLog.map(a => a.action).join(',')));
|
|
25
|
+
if (signatures.size > 1) return { ready: false, reason: `Inconsistent action structure across runs (${signatures.size} different patterns)`, confidence: 0, executionCount: count, successRate };
|
|
26
|
+
const confidence = Math.min(Math.min(count / 10, 1) * 0.3 + successRate * 0.5 + 0.2, 1.0);
|
|
27
|
+
return { ready: true, confidence, executionCount: count, successRate };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** crystalliser.ts extractVariables: params that vary between successful runs, by action position. */
|
|
31
|
+
export function extractVariables(executions) {
|
|
32
|
+
const ok = executions.filter(e => e.success && e.actionLog.length > 0);
|
|
33
|
+
if (ok.length < 2) return [];
|
|
34
|
+
const vars = [];
|
|
35
|
+
ok[0].actionLog.forEach((first, idx) => {
|
|
36
|
+
for (const key of Object.keys(first.params || {})) {
|
|
37
|
+
const values = ok.map(e => e.actionLog[idx]?.params?.[key]).filter(v => v !== undefined);
|
|
38
|
+
if (new Set(values.map(v => JSON.stringify(v))).size > 1) vars.push({ name: key, values });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return vars;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Real executions (+ optional definition) -> crystalliser ExecutionRecords. */
|
|
45
|
+
export function toRecords(executions, def) {
|
|
46
|
+
const synthetic = Array.isArray(def?.nodes) ? def.nodes.map(n => ({ action: n.type, params: n.config ?? n.data ?? {} })) : null;
|
|
47
|
+
return executions
|
|
48
|
+
.filter(e => e && (e.status === 'completed' || e.status === 'failed'))
|
|
49
|
+
.map(e => {
|
|
50
|
+
const log = Array.isArray(e.output?.action_log) ? e.output.action_log.map(a => ({ action: a.action, params: a.params || {} })) : synthetic;
|
|
51
|
+
return { id: e.id, success: e.status === 'completed', actionLog: log || [], logKnown: Array.isArray(e.output?.action_log) };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Successful runs still needed so both the count and the rate pass (consecutive successes from now). */
|
|
56
|
+
export function runsNeeded(count, successes) {
|
|
57
|
+
for (let k = 0; k < 10000; k++) {
|
|
58
|
+
const n = count + k, s = successes + k;
|
|
59
|
+
if (n >= MIN_EXECUTIONS && s / n >= MIN_SUCCESS_RATE) return k;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* d: { workflow: { id, name }, executions: [...completed and failed], def }
|
|
66
|
+
* -> view { box, rows }
|
|
67
|
+
*/
|
|
68
|
+
export function crystalCard(d, cols) {
|
|
69
|
+
const w = boxWidth(cols, 66), inner = w - 4, nw = w < 58;
|
|
70
|
+
const wf = d.workflow || {};
|
|
71
|
+
const recs = toRecords(d.executions || [], d.def);
|
|
72
|
+
const a = analyseExecutionHistory(recs);
|
|
73
|
+
const n = recs.length, s = recs.filter(r => r.success).length;
|
|
74
|
+
const pct = n ? Math.round((s / n) * 100) : 0;
|
|
75
|
+
const okRuns = recs.filter(r => r.success);
|
|
76
|
+
const sigs = new Set(okRuns.map(r => r.actionLog.map(x => x.action).join(',')));
|
|
77
|
+
const unknownLogs = okRuns.some(r => !r.logKnown);
|
|
78
|
+
const rows = [];
|
|
79
|
+
const check = (pass, name, value) => [seg(pass === null ? '○' : pass ? '✔' : '✖', pass === null ? 'faint' : pass ? 'ok' : 'warn'), seg(' '), seg(name.padEnd(nw ? 10 : 14), pass === null ? 'muted' : 'value'), seg(fit(value, inner - (nw ? 12 : 16)), 'muted')];
|
|
80
|
+
rows.push(check(n >= MIN_EXECUTIONS, 'runs', nw ? `${n} (needs ≥ ${MIN_EXECUTIONS})` : `${n}`.padEnd(11) + `needs ≥ ${MIN_EXECUTIONS}`));
|
|
81
|
+
rows.push(check(n ? s / n >= MIN_SUCCESS_RATE : false, nw ? 'success' : 'success rate', nw ? `${pct}% ${s}/${n} (≥ 80%)` : `${pct}% ${s}/${n} needs ≥ 80%`));
|
|
82
|
+
if (!okRuns.length) rows.push(check(null, 'structure', 'no successful run to compare'));
|
|
83
|
+
else if (unknownLogs && sigs.size === 1) rows.push(check(true, 'structure', nw ? `${okRuns.length}/${okRuns.length} same steps` : `${okRuns.length}/${okRuns.length} same steps (definition, no run log)`));
|
|
84
|
+
else rows.push(check(sigs.size === 1, 'structure', sigs.size === 1 ? (nw ? `${okRuns.length}/${okRuns.length} identical` : `${okRuns.length}/${okRuns.length} runs, identical action sequence`) : `${sigs.size} different action patterns`));
|
|
85
|
+
const vars = a.ready ? extractVariables(recs) : [];
|
|
86
|
+
if (a.ready) rows.push(check(true, 'variables', vars.length ? `${vars.map(v => v.name).join(', ')}${nw ? '' : ' (vary between runs)'}` : 'none vary between runs'));
|
|
87
|
+
|
|
88
|
+
const crystal = (d.executions || []).find(e => e?.output?.crystal_id);
|
|
89
|
+
let verdict;
|
|
90
|
+
if (crystal) verdict = [seg(fit(`Already runs as Crystal ${short(crystal.output.crystal_id, 10)}${crystal.output.execution_mode ? ` (${crystal.output.execution_mode} replay)` : ''}`, inner), 'teal', true)];
|
|
91
|
+
else if (a.ready) verdict = [seg(nw ? 'Eligible · meets the gate' : 'Eligible · meets the crystalliser gate', 'teal', true)];
|
|
92
|
+
else if (/Inconsistent/.test(a.reason || '')) verdict = [seg(fit(`Not eligible: runs follow ${sigs.size} different action patterns`, inner), 'warn', true)];
|
|
93
|
+
else {
|
|
94
|
+
const k = runsNeeded(n, s);
|
|
95
|
+
verdict = [seg(fit(k == null ? `Not eligible yet: ${a.reason}` : `Not eligible yet: needs ${plural(k, 'more successful run')}`, inner), 'warn', true)];
|
|
96
|
+
}
|
|
97
|
+
rows.push(verdict);
|
|
98
|
+
rows.push([seg(fit(nw ? `from ${plural(n, 'recorded run')}` : `from ${plural(n, 'recorded run')} · quoxflow's rule, ≤ 500 per status`, inner), 'faint')]);
|
|
99
|
+
return { box: { width: w, title: nw ? 'Crystal candidate' : `Crystal candidate · ${wf.name || wf.id || ''}`, right: '', border: 'teal' }, rows, analysis: a };
|
|
100
|
+
}
|