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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Q face for the web (cli.quox.ai): the SAME pure frames the CLI draws (qart.js, qglyph.js, qhello.js),
|
|
3
|
+
* rendered into <pre> elements as coloured spans. Plays Orbit & Lock, then the Q says hello (the line
|
|
4
|
+
* decodes in from katakana), then idles: the mark breathes, the status Q breathes beside "Ready", and now
|
|
5
|
+
* and then the Dragon's Eye opens and blinks. prefers-reduced-motion: the settled frame only.
|
|
6
|
+
*
|
|
7
|
+
* Browser ESM, no dependencies, no build: import it next to ../qart.js, ../qglyph.js, ../qhello.js.
|
|
8
|
+
* import { mountQFace } from './qface.js';
|
|
9
|
+
* mountQFace(document.getElementById('face'), { hello: "Hi, I'm Quox!" });
|
|
10
|
+
*/
|
|
11
|
+
import { introPlan, restCellsAt, eyeCellsAt, INTRO_MS, EYE_MS } from '../qart.js';
|
|
12
|
+
import { qGlyph } from '../qglyph.js';
|
|
13
|
+
import { helloCells, helloGradient, helloMs } from '../qhello.js';
|
|
14
|
+
|
|
15
|
+
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
16
|
+
|
|
17
|
+
/** One row of cells -> HTML spans, merging runs of the same colour. */
|
|
18
|
+
export function rowToHTML(row) {
|
|
19
|
+
let html = '', run = '', key = null;
|
|
20
|
+
const flush = () => { if (!run) return; html += key ? `<span style="${key}">${esc(run)}</span>` : esc(run); run = ''; };
|
|
21
|
+
for (const c of row) {
|
|
22
|
+
const k = c.rgb ? `color:rgb(${c.rgb.join(',')})${c.bold ? ';font-weight:700' : ''}` : (c.bold ? 'font-weight:700' : null);
|
|
23
|
+
if (k !== key) { flush(); key = k; }
|
|
24
|
+
run += c.ch;
|
|
25
|
+
}
|
|
26
|
+
flush();
|
|
27
|
+
return html;
|
|
28
|
+
}
|
|
29
|
+
// Blank cells in Braille art are drawn as U+2800 (the empty Braille pattern), not a space: a browser can
|
|
30
|
+
// give a space and a Braille glyph different advances (font fallback), which shears the Q row by row.
|
|
31
|
+
const BLANK = '\u2800';
|
|
32
|
+
export const cellsToHTML = (rows) => rows.map((r) => rowToHTML(r.map((c) => (c.ch === ' ' ? { ...c, ch: BLANK } : c)))).join('\n');
|
|
33
|
+
|
|
34
|
+
/** Which Q size fits a container of `px` CSS pixels at a monospace advance of `cw` px. */
|
|
35
|
+
export function sizeForWidth(px, cw = 8.4) {
|
|
36
|
+
const cols = Math.floor(px / cw);
|
|
37
|
+
return cols >= 34 ? 'large' : cols >= 22 ? 'mid' : 'small';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Timeline state at t ms (pure): which picture, the hello progress, and whether the eye is open.
|
|
42
|
+
* eyeAt: list of start times (ms) for idle eye blinks.
|
|
43
|
+
*/
|
|
44
|
+
export function faceFrame(t, { size, hello, eyeAt = [], color = true }) {
|
|
45
|
+
const plan = introPlan(size, { color, quant: false });
|
|
46
|
+
const hT = t - INTRO_MS, settledAt = INTRO_MS + helloMs(hello);
|
|
47
|
+
let q;
|
|
48
|
+
const eye = eyeAt.find((s) => t >= s && t < s + EYE_MS + 400);
|
|
49
|
+
if (t < INTRO_MS) q = plan.at(t);
|
|
50
|
+
else if (eye != null) q = eyeCellsAt(size, t - eye, { color, quant: false });
|
|
51
|
+
else q = restCellsAt(size, Math.max(0, t - settledAt), { color, quant: false });
|
|
52
|
+
const line = helloCells(hello, hT, { fg: helloGradient(hello), mono: !color });
|
|
53
|
+
const glyph = qGlyph('resting', Math.max(0, t - settledAt), { mono: !color });
|
|
54
|
+
return { q, line, glyph, showStatus: t >= settledAt };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mountQFace(root, { hello = "Hi, I'm Quox!", status = 'Ready', seed = 7, reduced = null, eyeEveryMs = [9000, 16000] } = {}) {
|
|
58
|
+
const doc = root.ownerDocument, win = doc.defaultView;
|
|
59
|
+
root.classList.add('qface');
|
|
60
|
+
root.innerHTML = '<pre class="qface-q" aria-hidden="true"></pre><div class="qface-side"><pre class="qface-hello"></pre><pre class="qface-status"></pre></div>';
|
|
61
|
+
root.setAttribute('role', 'img');
|
|
62
|
+
root.setAttribute('aria-label', `The Quox Q says: ${hello}`);
|
|
63
|
+
const [qEl, hEl, sEl] = ['.qface-q', '.qface-hello', '.qface-status'].map((s) => root.querySelector(s));
|
|
64
|
+
const still = reduced ?? Boolean(win.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
65
|
+
let r = seed; const rand = () => { r = (r * 16807) % 2147483647; return r / 2147483647; };
|
|
66
|
+
const eyeAt = [];
|
|
67
|
+
let next = INTRO_MS + helloMs(hello) + 2500; // the first blink comes soon after hello, then rarely
|
|
68
|
+
for (let i = 0; i < 40; i++) { eyeAt.push(next); next += EYE_MS + eyeEveryMs[0] + rand() * (eyeEveryMs[1] - eyeEveryMs[0]); }
|
|
69
|
+
const size = () => sizeForWidth(Math.min(root.clientWidth || 600, win.innerWidth || 600) * (root.clientWidth > 520 ? 0.55 : 1));
|
|
70
|
+
const draw = (t) => {
|
|
71
|
+
const f = faceFrame(t, { size: size(), hello, eyeAt });
|
|
72
|
+
qEl.innerHTML = cellsToHTML(f.q);
|
|
73
|
+
hEl.innerHTML = rowToHTML(f.line);
|
|
74
|
+
sEl.innerHTML = f.showStatus ? `${rowToHTML(f.glyph.cells)} <span class="qface-word">${esc(status)}</span>` : ' ';
|
|
75
|
+
};
|
|
76
|
+
if (still) { draw(INTRO_MS + helloMs(hello) + 10); return { stop() {} }; }
|
|
77
|
+
let raf = 0, t0 = null, stopped = false;
|
|
78
|
+
const tick = (now) => { if (stopped) return; t0 ??= now; draw(now - t0); raf = win.requestAnimationFrame(tick); };
|
|
79
|
+
raf = win.requestAnimationFrame(tick);
|
|
80
|
+
return { stop() { stopped = true; win.cancelAnimationFrame(raf); }, seek: draw };
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run notice for bare `quox`: who we are, legal, docs. Pure model + a tiny "seen" marker kept in the
|
|
3
|
+
* CLI's existing config dir (~/.config/quox, same place as config.json). Links live ONLY here.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const DOCS_URL = 'https://github.com/quoxai/quoxcore'; // will become https://cli.quox.ai
|
|
10
|
+
export const TERMS_URL = 'https://quox.ai/terms';
|
|
11
|
+
export const PRIVACY_URL = 'https://quox.ai/privacy';
|
|
12
|
+
export const LICENCE = 'Quox Community Source Licence 1.0 (source-available)';
|
|
13
|
+
export const WELCOME_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
export function packageVersion() {
|
|
16
|
+
try { return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version || '0.1.0'; } catch { return '0.1.0'; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** [{ kind: 'title'|'sub'|'rule'|'field'|'para'|'gap'|'hint', ... }] */
|
|
20
|
+
export function welcomeModel(version = packageVersion()) {
|
|
21
|
+
return [
|
|
22
|
+
{ kind: 'title', text: 'Welcome to Quox' },
|
|
23
|
+
{ kind: 'sub', text: `Quox CLI ${version}` },
|
|
24
|
+
{ kind: 'para', text: 'The main front door to Quox, your self-hosted control plane for AI agents.' },
|
|
25
|
+
{ kind: 'rule' },
|
|
26
|
+
{ kind: 'field', label: 'Docs', value: DOCS_URL, link: true },
|
|
27
|
+
{ kind: 'field', label: 'Terms', value: TERMS_URL, link: true },
|
|
28
|
+
{ kind: 'field', label: 'Privacy', value: PRIVACY_URL, link: true },
|
|
29
|
+
{ kind: 'field', label: 'Licence', value: LICENCE },
|
|
30
|
+
{ kind: 'rule' },
|
|
31
|
+
{ kind: 'para', text: 'Your prompts go to your own QuoxCORE server and to the AI provider you connect to it.' },
|
|
32
|
+
{ kind: 'para', text: 'Risky actions ask you first, and the actions Quox takes leave receipts you can verify.' },
|
|
33
|
+
{ kind: 'para', text: 'This CLI sends no telemetry to Quox.' },
|
|
34
|
+
{ kind: 'para', text: 'By using Quox CLI you agree to the Terms.' },
|
|
35
|
+
{ kind: 'hint', text: 'Enter to continue · /about to see this again' }
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const welcomeStatePath = () => join(homedir(), '.config', 'quox', 'welcome.json');
|
|
40
|
+
|
|
41
|
+
export function welcomeSeen(path = welcomeStatePath()) {
|
|
42
|
+
try { return (JSON.parse(readFileSync(path, 'utf8')).version || 0) >= WELCOME_VERSION; } catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
export function markWelcomeSeen(path = welcomeStatePath()) {
|
|
45
|
+
try { mkdirSync(join(path, '..'), { recursive: true }); writeFileSync(path, JSON.stringify({ version: WELCOME_VERSION, seenAt: new Date().toISOString() }) + '\n'); return true; } catch { return false; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** First interactive launch only: a TTY, not suppressed, not seen at this notice version. */
|
|
49
|
+
export function shouldShowWelcome({ env = process.env, isTTY = process.stdout.isTTY && process.stdin.isTTY, print = false, path } = {}) {
|
|
50
|
+
if (print || !isTTY || env.QUOX_NO_WELCOME === '1') return false;
|
|
51
|
+
return !welcomeSeen(path);
|
|
52
|
+
}
|
package/lib/tui/app.jsx
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxCLI TUI — Root Application Component
|
|
3
|
+
*
|
|
4
|
+
* Manages the view state machine:
|
|
5
|
+
* intro -> menu -> specific view (fleet, agents, tools, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Provides TuiContext to all descendants with navigation helpers,
|
|
8
|
+
* the CLI context (config, auth, client), and the app exit function.
|
|
9
|
+
*
|
|
10
|
+
* ── Input-mode registry ────────────────────────────────────────────────
|
|
11
|
+
* `inputMode` on the context tells the global keybinding handler whether
|
|
12
|
+
* it's safe to steal single-character keys like `?` (help) and `q` (quit):
|
|
13
|
+
* 'nav' - default; global shortcuts are active
|
|
14
|
+
* 'input' - a text field (chat box, search box) has focus; global
|
|
15
|
+
* single-char shortcuts are suppressed so typing works
|
|
16
|
+
* 'overlay' - the help overlay or command palette is open; it owns its
|
|
17
|
+
* own close handler and (for the palette) all keystrokes
|
|
18
|
+
* Escape/back always works regardless of mode (text inputs intentionally
|
|
19
|
+
* ignore Escape so navigation can still happen).
|
|
20
|
+
*
|
|
21
|
+
* ── Command palette + global jump keys ─────────────────────────────────
|
|
22
|
+
* Ctrl+K or ':' (nav mode only) opens the palette (components/Palette.jsx)
|
|
23
|
+
* over the current screen, content-swapped like the help overlay. It's a
|
|
24
|
+
* fuzzy search over lib/tui/registry.js's SCREEN_REGISTRY. Separately,
|
|
25
|
+
* whenever inputMode is 'nav' and a sub-view is active (screen !== 'menu'),
|
|
26
|
+
* pressing a screen's hotkey (1-0 / o i v g h — same as MainMenu's number
|
|
27
|
+
* keys) jumps straight there without returning to the menu first
|
|
28
|
+
* (k9s-style). Both are gated on inputMode === 'nav' so they never steal
|
|
29
|
+
* keystrokes from a focused chat/search box.
|
|
30
|
+
*
|
|
31
|
+
* ── Keep-alive views ───────────────────────────────────────────────────
|
|
32
|
+
* A screen is mounted the first time it's visited and then stays mounted
|
|
33
|
+
* (hidden via `display: none`) so its state — chat history, filters,
|
|
34
|
+
* selections — survives navigating away and back. Views only fetch data
|
|
35
|
+
* on their first activation (see each view's data hook), so this does
|
|
36
|
+
* NOT boot-fetch all screens at startup.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import React, { useState, useCallback, createContext, useContext } from 'react';
|
|
40
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
41
|
+
import { matchKey } from './keys.js';
|
|
42
|
+
import { colors } from './theme.js';
|
|
43
|
+
import { MainMenu } from './views/MainMenu.jsx';
|
|
44
|
+
import { FleetView } from './views/FleetView.jsx';
|
|
45
|
+
import { AgentView } from './views/AgentView.jsx';
|
|
46
|
+
import { ToolView } from './views/ToolView.jsx';
|
|
47
|
+
import { OrgView } from './views/OrgView.jsx';
|
|
48
|
+
import { MemoryView } from './views/MemoryView.jsx';
|
|
49
|
+
import { BrainView } from './views/BrainView.jsx';
|
|
50
|
+
import { WorkflowView } from './views/WorkflowView.jsx';
|
|
51
|
+
import { ChatView } from './views/ChatView.jsx';
|
|
52
|
+
import { AdminView } from './views/AdminView.jsx';
|
|
53
|
+
import { FilesView } from './views/FilesView.jsx';
|
|
54
|
+
import { BackupView } from './views/BackupView.jsx';
|
|
55
|
+
import { ObjectivesView } from './views/ObjectivesView.jsx';
|
|
56
|
+
import { InboxView } from './views/InboxView.jsx';
|
|
57
|
+
import { VaultView } from './views/VaultView.jsx';
|
|
58
|
+
import { GovernanceView } from './views/GovernanceView.jsx';
|
|
59
|
+
import { HealthView } from './views/HealthView.jsx';
|
|
60
|
+
import { StatusBar } from './components/StatusBar.jsx';
|
|
61
|
+
import { HelpOverlay } from './components/HelpOverlay.jsx';
|
|
62
|
+
import { Palette } from './components/Palette.jsx';
|
|
63
|
+
import { Toast } from './components/Toast.jsx';
|
|
64
|
+
import { SCREEN_BY_HOTKEY } from './registry.js';
|
|
65
|
+
|
|
66
|
+
// ── TUI Context ───────────────────────────────────────────────────────
|
|
67
|
+
export const TuiContext = createContext(null);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access TUI context from any descendant component.
|
|
71
|
+
* @returns {{ config, auth, client, navigate, goBack, screen, history, exit, inputMode, setInputMode, orgVersion, onOrgChange }}
|
|
72
|
+
*/
|
|
73
|
+
export const useTui = () => useContext(TuiContext);
|
|
74
|
+
|
|
75
|
+
// ── Screen registry ───────────────────────────────────────────────────
|
|
76
|
+
const SCREENS = [
|
|
77
|
+
'menu',
|
|
78
|
+
'fleet', 'agents', 'tools', 'orgs',
|
|
79
|
+
'memory', 'brain', 'workflows', 'chat',
|
|
80
|
+
'admin', 'files', 'backups',
|
|
81
|
+
'objectives', 'inbox', 'vault', 'governance', 'health',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Sub-views rendered (keep-alive) alongside the menu. Keyed by screen id.
|
|
85
|
+
const VIEW_COMPONENTS = {
|
|
86
|
+
fleet: FleetView,
|
|
87
|
+
agents: AgentView,
|
|
88
|
+
tools: ToolView,
|
|
89
|
+
orgs: OrgView,
|
|
90
|
+
memory: MemoryView,
|
|
91
|
+
brain: BrainView,
|
|
92
|
+
workflows: WorkflowView,
|
|
93
|
+
chat: ChatView,
|
|
94
|
+
admin: AdminView,
|
|
95
|
+
files: FilesView,
|
|
96
|
+
backups: BackupView,
|
|
97
|
+
objectives: ObjectivesView,
|
|
98
|
+
inbox: InboxView,
|
|
99
|
+
vault: VaultView,
|
|
100
|
+
governance: GovernanceView,
|
|
101
|
+
health: HealthView,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ── Root component ────────────────────────────────────────────────────
|
|
105
|
+
export function App({ ctx, initialScreen = null }) {
|
|
106
|
+
const { exit } = useApp();
|
|
107
|
+
// Deep-link: `quox tui <screen>` (or the web QuoxCLIView ?screen= param) can
|
|
108
|
+
// open directly on a view instead of the menu. Guard against unknown names.
|
|
109
|
+
const startScreen =
|
|
110
|
+
initialScreen && initialScreen !== 'menu' && SCREENS.includes(initialScreen)
|
|
111
|
+
? initialScreen
|
|
112
|
+
: 'menu';
|
|
113
|
+
const [screen, setScreen] = useState(startScreen);
|
|
114
|
+
const [history, setHistory] = useState([]);
|
|
115
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
116
|
+
const [showPalette, setShowPalette] = useState(false);
|
|
117
|
+
const [toast, setToast] = useState(null);
|
|
118
|
+
const [inputMode, setInputMode] = useState('nav');
|
|
119
|
+
const [orgVersion, setOrgVersion] = useState(0);
|
|
120
|
+
|
|
121
|
+
// Screens that have been visited at least once — kept mounted (hidden
|
|
122
|
+
// via display:none when not current) so their state survives navigation.
|
|
123
|
+
const [visited, setVisited] = useState(() =>
|
|
124
|
+
startScreen === 'menu' ? new Set(['menu']) : new Set(['menu', startScreen])
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Navigation helpers
|
|
128
|
+
const navigate = (target) => {
|
|
129
|
+
if (!SCREENS.includes(target)) return;
|
|
130
|
+
setHistory((prev) => [...prev, screen]);
|
|
131
|
+
setVisited((prev) => (prev.has(target) ? prev : new Set(prev).add(target)));
|
|
132
|
+
setScreen(target);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const goBack = () => {
|
|
136
|
+
if (history.length > 0) {
|
|
137
|
+
const prev = history[history.length - 1];
|
|
138
|
+
setHistory((h) => h.slice(0, -1));
|
|
139
|
+
setScreen(prev);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Toast helper
|
|
144
|
+
const showToast = useCallback((message, type = 'info') => {
|
|
145
|
+
setToast({ message, type });
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
const dismissToast = useCallback(() => {
|
|
149
|
+
setToast(null);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
// Called by OrgView after switching the active org — bumps a version
|
|
153
|
+
// counter that org-scoped views include in their fetch deps so they
|
|
154
|
+
// refetch automatically.
|
|
155
|
+
const onOrgChange = useCallback(() => {
|
|
156
|
+
setOrgVersion((v) => v + 1);
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const closeHelp = useCallback(() => {
|
|
160
|
+
setShowHelp(false);
|
|
161
|
+
setInputMode('nav');
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const closePalette = useCallback(() => {
|
|
165
|
+
setShowPalette(false);
|
|
166
|
+
setInputMode('nav');
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
// Build context value
|
|
170
|
+
const tuiCtx = {
|
|
171
|
+
...(ctx || {}),
|
|
172
|
+
navigate,
|
|
173
|
+
goBack,
|
|
174
|
+
screen,
|
|
175
|
+
history,
|
|
176
|
+
exit,
|
|
177
|
+
showToast,
|
|
178
|
+
inputMode,
|
|
179
|
+
setInputMode,
|
|
180
|
+
orgVersion,
|
|
181
|
+
onOrgChange,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ── Global keybindings ──────────────────────────────────────────────
|
|
185
|
+
useInput((input, key) => {
|
|
186
|
+
// While the help overlay is open, it owns ? and Esc via its own listener.
|
|
187
|
+
if (showHelp) return;
|
|
188
|
+
|
|
189
|
+
// While the command palette is open, it owns all keystrokes (typing,
|
|
190
|
+
// arrows, Enter, Esc) via its own useInput listener.
|
|
191
|
+
if (showPalette) return;
|
|
192
|
+
|
|
193
|
+
// Single-character global shortcuts (help, quit, palette) and the
|
|
194
|
+
// global jump keys only fire in 'nav' mode — otherwise typing into a
|
|
195
|
+
// chat/search box would be hijacked.
|
|
196
|
+
if (inputMode === 'nav') {
|
|
197
|
+
if (matchKey('help', input, key)) {
|
|
198
|
+
setShowHelp(true);
|
|
199
|
+
setInputMode('overlay');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Command palette: Ctrl+K (can't be expressed via matchKey's plain
|
|
204
|
+
// string comparison) or ':' (a normal KEYS binding).
|
|
205
|
+
if ((key.ctrl && input === 'k') || matchKey('palette', input, key)) {
|
|
206
|
+
setShowPalette(true);
|
|
207
|
+
setInputMode('overlay');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (screen === 'menu' && matchKey('quit', input, key)) {
|
|
212
|
+
exit();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Global jump keys (k9s-style): from any non-menu screen, a screen's
|
|
217
|
+
// hotkey (1-0 / o i v g h — see SCREEN_BY_HOTKEY) jumps straight
|
|
218
|
+
// there, mirroring MainMenu's own number-key shortcuts.
|
|
219
|
+
if (screen !== 'menu') {
|
|
220
|
+
const target = SCREEN_BY_HOTKEY[input];
|
|
221
|
+
if (target) {
|
|
222
|
+
navigate(target);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Escape goes back from any sub-view — works regardless of input mode
|
|
229
|
+
// (text inputs deliberately ignore Escape so this can still fire).
|
|
230
|
+
if (screen !== 'menu' && matchKey('back', input, key)) {
|
|
231
|
+
goBack();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── Render ──────────────────────────────────────────────────────────
|
|
236
|
+
return (
|
|
237
|
+
<TuiContext.Provider value={tuiCtx}>
|
|
238
|
+
<Box flexDirection="column" width="100%" height="100%">
|
|
239
|
+
{/* Toast notification */}
|
|
240
|
+
{toast && (
|
|
241
|
+
<Toast
|
|
242
|
+
message={toast.message}
|
|
243
|
+
type={toast.type}
|
|
244
|
+
duration={3000}
|
|
245
|
+
onDismiss={dismissToast}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Main content area — the palette and help overlay swap in place
|
|
250
|
+
of the screens (no layout shift) rather than pushing them down
|
|
251
|
+
as a sibling. Palette takes priority if somehow both are open. */}
|
|
252
|
+
<Box flexGrow={1}>
|
|
253
|
+
{showPalette ? (
|
|
254
|
+
<Palette
|
|
255
|
+
onSelect={(id) => {
|
|
256
|
+
navigate(id);
|
|
257
|
+
closePalette();
|
|
258
|
+
}}
|
|
259
|
+
onClose={closePalette}
|
|
260
|
+
/>
|
|
261
|
+
) : showHelp ? (
|
|
262
|
+
<HelpOverlay visible={showHelp} onClose={closeHelp} />
|
|
263
|
+
) : (
|
|
264
|
+
<>
|
|
265
|
+
<Box display={screen === 'menu' ? 'flex' : 'none'} width="100%" height="100%">
|
|
266
|
+
<MainMenu />
|
|
267
|
+
</Box>
|
|
268
|
+
{Object.entries(VIEW_COMPONENTS).map(([name, ViewComponent]) => (
|
|
269
|
+
visited.has(name) && (
|
|
270
|
+
<Box
|
|
271
|
+
key={name}
|
|
272
|
+
display={screen === name ? 'flex' : 'none'}
|
|
273
|
+
width="100%"
|
|
274
|
+
height="100%"
|
|
275
|
+
>
|
|
276
|
+
<ViewComponent />
|
|
277
|
+
</Box>
|
|
278
|
+
)
|
|
279
|
+
))}
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</Box>
|
|
283
|
+
|
|
284
|
+
{/* Status bar */}
|
|
285
|
+
<StatusBar />
|
|
286
|
+
</Box>
|
|
287
|
+
</TuiContext.Provider>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb — Navigation trail showing current position
|
|
3
|
+
*
|
|
4
|
+
* Renders a breadcrumb path like: Main > Fleet > Agent Details
|
|
5
|
+
* where the last segment is highlighted in the primary color
|
|
6
|
+
* and preceding segments are dimmed.
|
|
7
|
+
*
|
|
8
|
+
* Props:
|
|
9
|
+
* path - array of strings, e.g. ['Main', 'Fleet', 'Agent Details']
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { Box, Text } from 'ink';
|
|
14
|
+
import { colors } from '../theme.js';
|
|
15
|
+
|
|
16
|
+
export function Breadcrumb({ path = [] }) {
|
|
17
|
+
if (path.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box>
|
|
21
|
+
{path.map((segment, i) => {
|
|
22
|
+
const isLast = i === path.length - 1;
|
|
23
|
+
return (
|
|
24
|
+
<React.Fragment key={i}>
|
|
25
|
+
{/* Separator */}
|
|
26
|
+
{i > 0 && (
|
|
27
|
+
<Text color={colors.dim}> {'>'} </Text>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
{/* Segment */}
|
|
31
|
+
<Text
|
|
32
|
+
color={isLast ? colors.primary : colors.dim}
|
|
33
|
+
bold={isLast}
|
|
34
|
+
>
|
|
35
|
+
{segment}
|
|
36
|
+
</Text>
|
|
37
|
+
</React.Fragment>
|
|
38
|
+
);
|
|
39
|
+
})}
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatInput — Presentational input line + agent picker overlay for ChatView
|
|
3
|
+
*
|
|
4
|
+
* Pure rendering component: ChatView owns all input state (draft text,
|
|
5
|
+
* cursor position, history, agent selection) and passes it down as props.
|
|
6
|
+
* Kept separate so the caret-rendering and agent-picker markup don't
|
|
7
|
+
* clutter ChatView's streaming/keyboard logic.
|
|
8
|
+
*
|
|
9
|
+
* Props:
|
|
10
|
+
* input - current draft text
|
|
11
|
+
* cursor - caret index into `input` (0..input.length)
|
|
12
|
+
* disabled - true while a response is streaming (dims the border)
|
|
13
|
+
* agents - array of agent objects/strings for the picker list
|
|
14
|
+
* pickerOpen - whether the agent picker overlay is visible
|
|
15
|
+
* pickerIndex - highlighted row in the picker
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { Box, Text } from 'ink';
|
|
20
|
+
import { colors } from '../theme.js';
|
|
21
|
+
|
|
22
|
+
function agentLabel(a) {
|
|
23
|
+
if (typeof a === 'string') return a;
|
|
24
|
+
return a?.name || a?.display_name || a?.id || String(a);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ChatInput({
|
|
28
|
+
input = '',
|
|
29
|
+
cursor = 0,
|
|
30
|
+
disabled = false,
|
|
31
|
+
agents = [],
|
|
32
|
+
pickerOpen = false,
|
|
33
|
+
pickerIndex = 0,
|
|
34
|
+
}) {
|
|
35
|
+
const safeCursor = Math.max(0, Math.min(cursor, input.length));
|
|
36
|
+
const pre = input.slice(0, safeCursor);
|
|
37
|
+
const rest = input.slice(safeCursor);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Box flexDirection="column">
|
|
41
|
+
<Box borderStyle="single" borderColor={disabled ? colors.subtle : colors.primary} paddingX={1}>
|
|
42
|
+
<Text color={colors.cyberBlue}>{'> '}</Text>
|
|
43
|
+
<Text>{pre}</Text>
|
|
44
|
+
<Text color={colors.dim}>{'█'}</Text>
|
|
45
|
+
<Text>{rest}</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
|
|
48
|
+
{pickerOpen && (
|
|
49
|
+
<Box flexDirection="column" borderStyle="round" borderColor={colors.accent} paddingX={1}>
|
|
50
|
+
<Text color={colors.accent} bold>Select agent — Tab/↑↓ move, Enter confirm</Text>
|
|
51
|
+
{agents.length === 0 ? (
|
|
52
|
+
<Text color={colors.dim}>No agents found</Text>
|
|
53
|
+
) : (
|
|
54
|
+
agents.map((a, i) => (
|
|
55
|
+
<Text
|
|
56
|
+
key={`${agentLabel(a)}-${i}`}
|
|
57
|
+
color={i === pickerIndex ? colors.cyberBlue : colors.textMuted}
|
|
58
|
+
bold={i === pickerIndex}
|
|
59
|
+
>
|
|
60
|
+
{i === pickerIndex ? '➤ ' : ' '}{agentLabel(a)}
|
|
61
|
+
</Text>
|
|
62
|
+
))
|
|
63
|
+
)}
|
|
64
|
+
</Box>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm — Confirmation dialog component
|
|
3
|
+
*
|
|
4
|
+
* Shows an inline confirmation prompt with y/n keybindings.
|
|
5
|
+
*
|
|
6
|
+
* Props:
|
|
7
|
+
* message - question text (e.g. "Delete backup XYZ?")
|
|
8
|
+
* onConfirm - called when user presses y
|
|
9
|
+
* onCancel - called when user presses n or Esc
|
|
10
|
+
* active - whether dialog is shown/active (default true)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Box, Text, useInput } from 'ink';
|
|
15
|
+
import { colors } from '../theme.js';
|
|
16
|
+
|
|
17
|
+
export function Confirm({ message, onConfirm, onCancel, active = true }) {
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (!active) return;
|
|
20
|
+
if (input === 'y' || input === 'Y') { onConfirm?.(); return; }
|
|
21
|
+
if (input === 'n' || input === 'N' || key.escape) { onCancel?.(); return; }
|
|
22
|
+
}, { isActive: active });
|
|
23
|
+
|
|
24
|
+
if (!active) return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Box borderStyle="single" borderColor={colors.warning} paddingX={2} paddingY={1}>
|
|
28
|
+
<Text color={colors.warning}>{message} </Text>
|
|
29
|
+
<Text color={colors.dim}>(y/n)</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
}
|