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,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LucidDeck: the full-screen live LUCID situation deck (`quox lucid deck`).
|
|
3
|
+
*
|
|
4
|
+
* One vertical cursor. Up/Down (j/k) move through the focused pane's list,
|
|
5
|
+
* Tab / Shift-Tab switch pane (Needs you, Since you looked, Areas), Enter
|
|
6
|
+
* opens / acts, Esc backs out, ? is help, q quits. Every action echoes what it
|
|
7
|
+
* did and why in a toast; keys that do nothing say so.
|
|
8
|
+
*
|
|
9
|
+
* Props are injected (api, intervalMs, intro) so the component is testable
|
|
10
|
+
* without a network. api = { situation(), measure(ref, id), teach(body), ack(seq) }.
|
|
11
|
+
* Never shows an empty all-quiet view on failure: errors render in the header
|
|
12
|
+
* and the last good data stays on screen, marked STALE.
|
|
13
|
+
*/
|
|
14
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
15
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
16
|
+
import {
|
|
17
|
+
GLYPH, STATE_HEX, URGENCY, PALETTE, SPINNER, wrap, clip, ageOf, meter, clockLondonSec,
|
|
18
|
+
whenText, changedWords, blindSpots,
|
|
19
|
+
} from '../../lucid/fmt.js';
|
|
20
|
+
import { LucidIntro } from '../../lucid/LucidIntro.jsx';
|
|
21
|
+
|
|
22
|
+
const PANES = ['needs', 'changed', 'areas'];
|
|
23
|
+
const PANE_TITLE = { needs: 'NEEDS YOU', changed: 'SINCE YOU LOOKED', areas: 'AREAS' };
|
|
24
|
+
const PANE_WORD = { needs: 'Needs you', changed: 'Since you looked', areas: 'Areas' };
|
|
25
|
+
const BAR_BG = '#1E3A8A'; // selected row bar
|
|
26
|
+
const BAR_BG_DIM = '#27272A'; // selected row in an unfocused pane
|
|
27
|
+
const NEW_MS = 4000;
|
|
28
|
+
const GHOST_MS = 4000;
|
|
29
|
+
const TOAST_MS = 3000;
|
|
30
|
+
const GRAD = ['#38BDF8', '#6366F1', '#8B5CF6', '#C084FC', '#8B5CF6'];
|
|
31
|
+
|
|
32
|
+
const pad = (s, w) => { const a = [...String(s)]; return a.length >= w ? a.join('') : s + ' '.repeat(w - a.length); };
|
|
33
|
+
const nm = (x) => (typeof x === 'string' ? x : (x && (x.label || x.name || x.L0 || x.id)) || '');
|
|
34
|
+
|
|
35
|
+
export function itemsLines(it, expanded, w, base = 1) {
|
|
36
|
+
if (!expanded) return base;
|
|
37
|
+
const a = it.L1 || {};
|
|
38
|
+
let n = base;
|
|
39
|
+
for (const v of [a.who, a.what, a.why, whenText(a.when)]) if (v) n += wrap(v, w - 8).length;
|
|
40
|
+
if ((a.affected || []).length) n += 1;
|
|
41
|
+
const ms = (it.L2 && it.L2.measures) || [];
|
|
42
|
+
if (ms.length) n += 1 + ms.length + (ms.some((m) => m.command) ? 1 : 0);
|
|
43
|
+
else n += 1;
|
|
44
|
+
return n + 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Window of items to render so the selected one is visible within `rows` lines. */
|
|
48
|
+
export function windowItems(items, sel, expandedRef, rows, w, base) {
|
|
49
|
+
const heights = items.map((it) => itemsLines(it, it.ref === expandedRef, w, base ? base(it) : 1));
|
|
50
|
+
let start = 0;
|
|
51
|
+
const fits = (s) => {
|
|
52
|
+
let used = 0;
|
|
53
|
+
for (let i = s; i <= sel; i++) used += heights[i];
|
|
54
|
+
return used <= rows;
|
|
55
|
+
};
|
|
56
|
+
while (start < sel && !fits(start)) start++;
|
|
57
|
+
let used = 0; let end = start;
|
|
58
|
+
while (end < items.length && used + heights[end] <= rows) { used += heights[end]; end++; }
|
|
59
|
+
if (end === start && items.length) end = start + 1;
|
|
60
|
+
return { start, end };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Window by rendered line heights; when the list overflows, 2 lines are kept for the "N more" markers. */
|
|
64
|
+
export function fitWindow(items, sel, expandedRef, rows, w, base) {
|
|
65
|
+
let win = windowItems(items, sel, expandedRef, rows, w, base);
|
|
66
|
+
if (win.start > 0 || win.end < items.length) win = windowItems(items, sel, expandedRef, Math.max(1, rows - 2), w, base);
|
|
67
|
+
return win;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Simple window for single-line lists. */
|
|
71
|
+
function winLines(n, sel, rows) {
|
|
72
|
+
rows = Math.max(1, rows);
|
|
73
|
+
if (n <= rows) return { start: 0, end: n };
|
|
74
|
+
let start = Math.max(0, Math.min(sel - Math.floor(rows / 2), n - rows));
|
|
75
|
+
return { start, end: start + rows };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function signature(sit) {
|
|
79
|
+
if (!sit) return '';
|
|
80
|
+
return JSON.stringify([
|
|
81
|
+
(sit.needs_you || []).map((i) => i.ref + i.urgency),
|
|
82
|
+
(sit.changed || []).map((c) => c.ref + c.ts),
|
|
83
|
+
(sit.areas || []).map((a) => a.state),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Pill({ urgency }) {
|
|
88
|
+
const u = URGENCY[urgency] || URGENCY.whenever;
|
|
89
|
+
const hot = urgency === 'now' || urgency === 'today' || urgency === 'this week';
|
|
90
|
+
const label = ` ${u.label} `;
|
|
91
|
+
return hot
|
|
92
|
+
? <Text backgroundColor={u.hex} color="#0B1220" bold>{pad(label, 11)}</Text>
|
|
93
|
+
: <Text color={u.hex}>{pad(label, 11)}</Text>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Hints({ items, cols }) {
|
|
97
|
+
return (
|
|
98
|
+
<Text wrap="truncate" color={PALETTE.dim}>
|
|
99
|
+
{items.map(([k, l], i) => (
|
|
100
|
+
<Text key={k + l}>{i ? ' ' : ''}<Text bold color={PALETTE.brand}>{k}</Text><Text color={PALETTE.muted}> {l}</Text></Text>
|
|
101
|
+
))}
|
|
102
|
+
</Text>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Panel({ title, count, focused, width, height, note, children }) {
|
|
107
|
+
return (
|
|
108
|
+
<Box flexDirection="column" width={width} height={height} borderStyle="round"
|
|
109
|
+
borderColor={focused ? PALETTE.brand : '#3F3F46'} paddingX={1}>
|
|
110
|
+
<Box>
|
|
111
|
+
{focused
|
|
112
|
+
? <Text backgroundColor={PALETTE.brand} color="#0B1220" bold> ▶ {title} </Text>
|
|
113
|
+
: <Text color={PALETTE.dim} bold> {title} </Text>}
|
|
114
|
+
<Text color={focused ? PALETTE.text : PALETTE.dim}> {count}</Text>
|
|
115
|
+
{note ? <Text color={PALETTE.dim}> {note}</Text> : null}
|
|
116
|
+
</Box>
|
|
117
|
+
{children}
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function LucidDeck({ api, intervalMs = 10000, now = () => Date.now(), onExit, intro = false }) {
|
|
123
|
+
const { exit } = useApp();
|
|
124
|
+
const { stdout } = useStdout();
|
|
125
|
+
const [, setSizeTick] = useState(0);
|
|
126
|
+
const cols = (stdout && stdout.columns) || 100;
|
|
127
|
+
const rows = (stdout && stdout.rows) || 30;
|
|
128
|
+
|
|
129
|
+
const [phase, setPhase] = useState(intro ? 'intro' : 'deck');
|
|
130
|
+
const [slide, setSlide] = useState(intro ? 3 : 0);
|
|
131
|
+
const [data, setData] = useState(null); // last good response
|
|
132
|
+
const [err, setErr] = useState(null);
|
|
133
|
+
const [fetchedAt, setFetchedAt] = useState(null);
|
|
134
|
+
const [fetching, setFetching] = useState(false);
|
|
135
|
+
const [tick, setTick] = useState(0);
|
|
136
|
+
const [spin, setSpin] = useState(0);
|
|
137
|
+
const [pane, setPane] = useState('needs');
|
|
138
|
+
const [filter, setFilter] = useState(null); // area key or null
|
|
139
|
+
const [sel, setSel] = useState(0);
|
|
140
|
+
const [chSel, setChSel] = useState(0);
|
|
141
|
+
const [arSel, setArSel] = useState(0); // 0 = All areas, 1.. = areas
|
|
142
|
+
const [expanded, setExpanded] = useState(null);
|
|
143
|
+
const [mSel, setMSel] = useState(0);
|
|
144
|
+
const [confirm, setConfirm] = useState(null); // {text, extra, run}
|
|
145
|
+
const [note, setNote] = useState(null); // {text, tone, at}
|
|
146
|
+
const [help, setHelp] = useState(false);
|
|
147
|
+
const [hinted, setHinted] = useState(true); // first-launch hint until first key
|
|
148
|
+
const [arrived, setArrived] = useState(0);
|
|
149
|
+
const sigRef = useRef('');
|
|
150
|
+
const prevNeeds = useRef(null);
|
|
151
|
+
const newAt = useRef(new Map());
|
|
152
|
+
const ghosts = useRef([]);
|
|
153
|
+
const inflight = useRef(false);
|
|
154
|
+
|
|
155
|
+
const flash = useCallback((text, tone = 'ok') => {
|
|
156
|
+
setNote({ text, tone, at: Date.now() });
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!stdout || !stdout.on) return undefined;
|
|
161
|
+
const f = () => setSizeTick((x) => x + 1);
|
|
162
|
+
stdout.on('resize', f);
|
|
163
|
+
return () => stdout.off && stdout.off('resize', f);
|
|
164
|
+
}, [stdout]);
|
|
165
|
+
|
|
166
|
+
// Props held in refs so `refresh` is created once: a default `now = () => Date.now()` is a new
|
|
167
|
+
// function every render, and depending on it restarted the fetch loop on every redraw (the
|
|
168
|
+
// deck fetched continuously and ran out of memory after 8.5h, 2026-10-11).
|
|
169
|
+
const apiRef = useRef(api);
|
|
170
|
+
const nowRef = useRef(now);
|
|
171
|
+
apiRef.current = api;
|
|
172
|
+
nowRef.current = now;
|
|
173
|
+
|
|
174
|
+
const refresh = useCallback(async () => {
|
|
175
|
+
if (inflight.current) return;
|
|
176
|
+
inflight.current = true;
|
|
177
|
+
setFetching(true);
|
|
178
|
+
try {
|
|
179
|
+
const res = await apiRef.current.situation();
|
|
180
|
+
const sit = res && res.situation;
|
|
181
|
+
const cur = (sit && sit.needs_you) || [];
|
|
182
|
+
if (prevNeeds.current) {
|
|
183
|
+
const have = new Set(prevNeeds.current.map((i) => i.ref));
|
|
184
|
+
const nowRefs = new Set(cur.map((i) => i.ref));
|
|
185
|
+
let fresh = 0;
|
|
186
|
+
for (const i of cur) if (!have.has(i.ref)) { newAt.current.set(i.ref, Date.now()); fresh++; }
|
|
187
|
+
for (const i of prevNeeds.current) if (!nowRefs.has(i.ref)) ghosts.current.push({ item: i, at: Date.now() });
|
|
188
|
+
if (fresh) setArrived((n) => n + fresh);
|
|
189
|
+
}
|
|
190
|
+
prevNeeds.current = cur;
|
|
191
|
+
sigRef.current = signature(sit);
|
|
192
|
+
setData(res);
|
|
193
|
+
setErr(null);
|
|
194
|
+
setFetchedAt(nowRef.current());
|
|
195
|
+
} catch (e) {
|
|
196
|
+
setErr(e);
|
|
197
|
+
} finally {
|
|
198
|
+
inflight.current = false;
|
|
199
|
+
setFetching(false);
|
|
200
|
+
}
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
refresh();
|
|
205
|
+
const t = setInterval(refresh, intervalMs);
|
|
206
|
+
const h = setInterval(() => setTick((x) => x + 1), 500);
|
|
207
|
+
return () => { clearInterval(t); clearInterval(h); };
|
|
208
|
+
}, [refresh, intervalMs]);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!fetching) return undefined;
|
|
212
|
+
const s = setInterval(() => setSpin((x) => x + 1), 120);
|
|
213
|
+
return () => clearInterval(s);
|
|
214
|
+
}, [fetching]);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (phase !== 'deck' || slide <= 0) return undefined;
|
|
218
|
+
const s = setTimeout(() => setSlide((x) => x - 1), 60);
|
|
219
|
+
return () => clearTimeout(s);
|
|
220
|
+
}, [phase, slide]);
|
|
221
|
+
|
|
222
|
+
const introDone = useCallback(() => setPhase((p) => (p === 'intro' ? 'deck' : p)), []);
|
|
223
|
+
|
|
224
|
+
const sit = (data && data.situation) || null;
|
|
225
|
+
const areas = (sit && sit.areas) || [];
|
|
226
|
+
const allNeeds = (sit && sit.needs_you) || [];
|
|
227
|
+
const changed = (sit && sit.changed) || [];
|
|
228
|
+
const needs = useMemo(() => (filter ? allNeeds.filter((i) => i.area === filter) : allNeeds), [allNeeds, filter]);
|
|
229
|
+
const filterLabel = filter ? ((areas.find((a) => a.area === filter) || {}).label || filter) : null;
|
|
230
|
+
const safeSel = Math.min(sel, Math.max(0, needs.length - 1));
|
|
231
|
+
const safeCh = Math.min(chSel, Math.max(0, changed.length - 1));
|
|
232
|
+
const safeAr = Math.min(arSel, areas.length);
|
|
233
|
+
const cur = needs[safeSel];
|
|
234
|
+
const curMeasures = (cur && cur.L2 && cur.L2.measures) || [];
|
|
235
|
+
const isOpen = Boolean(cur && expanded === cur.ref);
|
|
236
|
+
const expandedRef = isOpen ? expanded : null;
|
|
237
|
+
|
|
238
|
+
// A resolved/vanished expanded item collapses itself.
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (expanded && !allNeeds.some((i) => i.ref === expanded)) setExpanded(null);
|
|
241
|
+
}, [allNeeds, expanded]);
|
|
242
|
+
|
|
243
|
+
const runConfirmed = () => {
|
|
244
|
+
const c = confirm; setConfirm(null);
|
|
245
|
+
if (c) c.run();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const askMeasure = (item, m) => {
|
|
249
|
+
setConfirm({
|
|
250
|
+
text: `Do ${m.label}?`,
|
|
251
|
+
extra: [m.risk && m.risk !== 'safe' ? m.risk : null, m.kind === 'run' ? 'This files an approval' : null].filter(Boolean).join(' · '),
|
|
252
|
+
run: () => {
|
|
253
|
+
flash(`Running "${m.label}"...`, 'busy');
|
|
254
|
+
api.measure(item.ref, m.id).then((r) => {
|
|
255
|
+
const bits = [r.say, r.command && `run: ${r.command}`, r.url && `open: ${r.url}`,
|
|
256
|
+
r.status === 'filed' ? 'Filed for approval. Approve it in Approvals.' : null].filter(Boolean);
|
|
257
|
+
flash(bits.join(' ') || `Done: ${m.label} (status ${r.status}).`, 'ok');
|
|
258
|
+
refresh();
|
|
259
|
+
}).catch((e) => flash(`"${m.label}" failed: ${e.status ? 'HTTP ' + e.status : e.message}`, 'bad'));
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
flash('Asked first, nothing has run yet.', 'dim');
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const askTeach = (text, body, okMsg) => {
|
|
266
|
+
setConfirm({
|
|
267
|
+
text,
|
|
268
|
+
extra: '',
|
|
269
|
+
run: () => {
|
|
270
|
+
api.teach(body)
|
|
271
|
+
.then(() => { flash(okMsg, 'ok'); refresh(); })
|
|
272
|
+
.catch((e) => flash(`Teach failed: ${e.status ? 'HTTP ' + e.status : e.message}`, 'bad'));
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const gotoPane = (delta) => {
|
|
278
|
+
const i = PANES.indexOf(pane);
|
|
279
|
+
const nxt = PANES[(i + delta + PANES.length) % PANES.length];
|
|
280
|
+
setPane(nxt);
|
|
281
|
+
flash(`Switched to ${PANE_WORD[nxt]}. Up/Down now moves through it.`, 'dim');
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const move = (delta) => {
|
|
285
|
+
if (pane === 'needs') {
|
|
286
|
+
if (!needs.length) { flash(filter ? 'Nothing to move through: no needs in this area (Esc clears the filter).' : 'Nothing needs you, so there is nothing to select.', 'dim'); return; }
|
|
287
|
+
if (isOpen && curMeasures.length) { setMSel((m) => Math.max(0, Math.min(curMeasures.length - 1, m + delta))); return; }
|
|
288
|
+
if (isOpen) { flash('This item is open and has no actions. Esc closes it.', 'dim'); return; }
|
|
289
|
+
setSel((x) => Math.max(0, Math.min(needs.length - 1, Math.min(x, needs.length - 1) + delta))); setMSel(0);
|
|
290
|
+
} else if (pane === 'changed') {
|
|
291
|
+
if (!changed.length) { flash('Nothing has changed since you last looked.', 'dim'); return; }
|
|
292
|
+
setChSel((x) => Math.max(0, Math.min(changed.length - 1, Math.min(x, changed.length - 1) + delta)));
|
|
293
|
+
} else {
|
|
294
|
+
setArSel((x) => Math.max(0, Math.min(areas.length, Math.min(x, areas.length) + delta)));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const activate = () => {
|
|
299
|
+
if (pane === 'needs') {
|
|
300
|
+
if (!cur) { flash('Nothing is selected, so Enter has nothing to open.', 'dim'); return; }
|
|
301
|
+
if (!isOpen) { setExpanded(cur.ref); setMSel(0); return; }
|
|
302
|
+
if (curMeasures[mSel]) { askMeasure(cur, curMeasures[mSel]); return; }
|
|
303
|
+
setExpanded(null); flash('No actions on this item, closed it.', 'dim');
|
|
304
|
+
} else if (pane === 'areas') {
|
|
305
|
+
if (safeAr === 0 || (areas[safeAr - 1] && areas[safeAr - 1].area === filter)) {
|
|
306
|
+
setFilter(null); flash('Showing all areas.', 'ok');
|
|
307
|
+
} else {
|
|
308
|
+
const a = areas[safeAr - 1];
|
|
309
|
+
setFilter(a.area); flash(`Filtered Needs you to ${a.label}. Esc clears.`, 'ok');
|
|
310
|
+
}
|
|
311
|
+
setPane('needs'); setSel(0); setExpanded(null);
|
|
312
|
+
} else {
|
|
313
|
+
const c = changed[safeCh];
|
|
314
|
+
if (!c) { flash('Nothing is selected.', 'dim'); return; }
|
|
315
|
+
flash(`${c.L0 || 'Change'}${changedWords(c) ? ': ' + changedWords(c) : ''}`, 'ok');
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const onKey = (input, key) => {
|
|
320
|
+
if (key.ctrl && input === 'c') { if (onExit) onExit(); exit(); return; }
|
|
321
|
+
if (phase === 'intro') { introDone(); return; }
|
|
322
|
+
setHinted(false);
|
|
323
|
+
if (input === 'q' && !confirm) { if (onExit) onExit(); exit(); return; }
|
|
324
|
+
if (help) { if (input === '?' || key.escape || input === 'q') setHelp(false); return; }
|
|
325
|
+
if (confirm) {
|
|
326
|
+
if (input === 'y' || input === 'Y') runConfirmed();
|
|
327
|
+
else if (input === 'n' || input === 'N' || key.escape) { setConfirm(null); flash('Cancelled, nothing was changed.', 'dim'); }
|
|
328
|
+
else flash('Answer y to do it or n to cancel.', 'dim');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (input === '?') { setHelp(true); return; }
|
|
332
|
+
if (input === 'r') { flash('Refreshing now...', 'busy'); refresh(); return; }
|
|
333
|
+
if (key.tab) { gotoPane(key.shift ? -1 : 1); return; }
|
|
334
|
+
if (key.upArrow || input === 'k') { move(-1); return; }
|
|
335
|
+
if (key.downArrow || input === 'j') { move(1); return; }
|
|
336
|
+
if (key.return) { activate(); return; }
|
|
337
|
+
if (key.escape) {
|
|
338
|
+
if (isOpen) { setExpanded(null); flash('Closed the item.', 'dim'); }
|
|
339
|
+
else if (filter) { setFilter(null); setSel(0); flash('Filter cleared, showing all areas.', 'ok'); }
|
|
340
|
+
else flash('Nothing to close. Press q to quit.', 'dim');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (input === 'd') {
|
|
344
|
+
if (pane !== 'needs') { flash('d acts on Needs you. Tab to that pane first.', 'dim'); return; }
|
|
345
|
+
if (!cur) { flash('Nothing selected to act on.', 'dim'); return; }
|
|
346
|
+
if (!isOpen) {
|
|
347
|
+
if (!curMeasures.length) { flash('This item has no actions I can offer.', 'dim'); return; }
|
|
348
|
+
setExpanded(cur.ref); setMSel(0); flash('Opened. Up/Down picks an action, Enter or d does it.', 'dim'); return;
|
|
349
|
+
}
|
|
350
|
+
if (!curMeasures[mSel]) { flash('This item has no actions I can offer.', 'dim'); return; }
|
|
351
|
+
askMeasure(cur, curMeasures[mSel]); return;
|
|
352
|
+
}
|
|
353
|
+
if (input === 'u' || input === 'c') {
|
|
354
|
+
if (pane !== 'needs' || !cur) { flash(`${input} acts on a Needs you item. Select one first.`, 'dim'); return; }
|
|
355
|
+
if (input === 'u') {
|
|
356
|
+
askTeach(`Treat all ${cur.kind} items as not urgent? y/n`,
|
|
357
|
+
{ scope: 'kind', target: cur.kind, field: 'urgency', value: 'whenever', reason: 'not useful (deck)' },
|
|
358
|
+
`Noted: ${cur.kind} items are "whenever" from now on.`);
|
|
359
|
+
} else {
|
|
360
|
+
askTeach(`Mark "${clip(cur.L0 || '', 40)}" as something you care about? y/n`,
|
|
361
|
+
{ scope: 'subject', target: cur.ref, field: 'watched', value: true, reason: 'I care (deck)' },
|
|
362
|
+
'Noted: you care about this one, LUCID will keep it in view.');
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (input === 'm') {
|
|
367
|
+
if (!sit || !sit.cursor) { flash('Nothing to mark: no cursor from LUCID yet.', 'dim'); return; }
|
|
368
|
+
const n = changed.length;
|
|
369
|
+
api.ack(sit.cursor.to)
|
|
370
|
+
.then(() => { flash(`Marked ${n} change${n === 1 ? '' : 's'} as seen, so "since you looked" restarts from now.`, 'ok'); refresh(); })
|
|
371
|
+
.catch((e) => flash(`Ack failed: ${e.status ? 'HTTP ' + e.status : e.message}`, 'bad'));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
flash(`"${input || 'that key'}" does nothing here. Press ? for keys.`, 'dim');
|
|
375
|
+
};
|
|
376
|
+
// Always dispatch to the latest closure: Ink re-subscribes in a passive effect,
|
|
377
|
+
// which can lag a render and would otherwise hand a key to stale state.
|
|
378
|
+
const keyRef = useRef(onKey);
|
|
379
|
+
keyRef.current = onKey;
|
|
380
|
+
useInput((input, key) => keyRef.current(input, key));
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if (phase === 'intro') {
|
|
384
|
+
return (
|
|
385
|
+
<Box flexDirection="column" height={Math.max(10, rows - 1)} width={cols}>
|
|
386
|
+
<LucidIntro data={data} err={err} onDone={introDone} rows={rows} />
|
|
387
|
+
</Box>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const wall = Date.now();
|
|
392
|
+
const t = now();
|
|
393
|
+
const cov = (sit && sit.cannot_see && sit.cannot_see.coverage) || { live: 0, expected: 0 };
|
|
394
|
+
const agoS = fetchedAt ? Math.max(0, Math.floor((t - fetchedAt) / 1000)) : null;
|
|
395
|
+
const stale = Boolean(err && data);
|
|
396
|
+
const noteLive = note && wall - note.at < (note.tone === 'bad' ? 6000 : TOAST_MS) ? note : null;
|
|
397
|
+
const noteHex = noteLive ? ({ ok: STATE_HEX.ok, bad: STATE_HEX.blind, busy: PALETTE.brand, dim: PALETTE.muted }[noteLive.tone]) : null;
|
|
398
|
+
const spots = blindSpots(sit && sit.cannot_see);
|
|
399
|
+
const dau = (sit && sit.doesnt_add_up) || [];
|
|
400
|
+
const flashOn = tick % 2 === 0;
|
|
401
|
+
ghosts.current = ghosts.current.filter((g) => wall - g.at < GHOST_MS);
|
|
402
|
+
const liveGhosts = ghosts.current.filter((g) => !filter || g.item.area === filter);
|
|
403
|
+
|
|
404
|
+
// ── layout ──────────────────────────────────────────────────────────
|
|
405
|
+
const wide = cols >= 80;
|
|
406
|
+
const big = cols >= 110;
|
|
407
|
+
const leftW = big ? 24 : 22;
|
|
408
|
+
const avail = cols - 2 - leftW;
|
|
409
|
+
const rightW = big ? avail - Math.round(avail * 0.55) : 0; // below 110 cols Since-you-looked stacks under Needs-you
|
|
410
|
+
const mainW = avail - rightW - (slide ? slide * 4 : 0);
|
|
411
|
+
const bannerOn = Boolean(err) || hinted;
|
|
412
|
+
const headerH = 3 + (err ? 1 : 0);
|
|
413
|
+
const hintH = hinted && !err ? 1 : 0;
|
|
414
|
+
const footerH = 3;
|
|
415
|
+
const bodyH = Math.max(8, rows - headerH - hintH - footerH - 1);
|
|
416
|
+
const listRows = bodyH - 3;
|
|
417
|
+
const innerOf = (w) => Math.max(10, w - 4);
|
|
418
|
+
|
|
419
|
+
// ── header ──────────────────────────────────────────────────────────
|
|
420
|
+
const covHex = cov.expected && cov.live < cov.expected ? STATE_HEX.attention : STATE_HEX.ok;
|
|
421
|
+
const badgeBg = allNeeds.length ? (arrived > 0 && !flashOn ? '#B45309' : STATE_HEX.attention) : null;
|
|
422
|
+
const header = (
|
|
423
|
+
<Box flexDirection="column">
|
|
424
|
+
<Box borderStyle="round" borderColor={err ? (stale ? STATE_HEX.attention : STATE_HEX.blind) : '#3F3F46'} paddingX={1}>
|
|
425
|
+
<Text wrap="truncate">
|
|
426
|
+
{[...'◉ LUCID'].map((ch, i) => <Text key={i} bold color={GRAD[i % GRAD.length]}>{ch}</Text>)}
|
|
427
|
+
<Text color={PALETTE.dim}> │ </Text>
|
|
428
|
+
<Text bold>{clockLondonSec(t)}</Text><Text color={PALETTE.dim}> London</Text>
|
|
429
|
+
<Text color={PALETTE.dim}> │ </Text>
|
|
430
|
+
{sit ? <Text color={covHex}>{meter(cov.live, cov.expected)} {cov.live}/{cov.expected}</Text> : <Text color={PALETTE.dim}>▱▱▱▱▱ -/-</Text>}
|
|
431
|
+
<Text color={PALETTE.dim}> │ </Text>
|
|
432
|
+
{!sit ? <Text color={PALETTE.dim}>- need you</Text> : badgeBg
|
|
433
|
+
? <Text backgroundColor={badgeBg} color="#0B1220" bold> {allNeeds.length} need you </Text>
|
|
434
|
+
: <Text bold color={STATE_HEX.ok}>0 need you</Text>}
|
|
435
|
+
{filterLabel ? <Text bold color={PALETTE.accent}> │ filter: {filterLabel}</Text> : null}
|
|
436
|
+
{arrived > 0 ? <Text bold color={STATE_HEX.ok}> +{arrived} new</Text> : null}
|
|
437
|
+
<Text color={PALETTE.dim}> │ </Text>
|
|
438
|
+
<Text color={PALETTE.muted}>
|
|
439
|
+
{fetching ? <Text color={PALETTE.brand}>{SPINNER[spin % SPINNER.length]} </Text> : null}
|
|
440
|
+
{agoS == null ? 'connecting' : `updated ${agoS}s ago`}
|
|
441
|
+
</Text>
|
|
442
|
+
</Text>
|
|
443
|
+
</Box>
|
|
444
|
+
{err ? (
|
|
445
|
+
<Text bold wrap="truncate" color={stale ? STATE_HEX.attention : STATE_HEX.blind}>
|
|
446
|
+
{stale ? `⚠ STALE ${agoS}s` : '✖ NO DATA'} · LUCID unreachable: {err.status || err.code || err.message} · retrying, {stale ? 'showing the last good data' : 'nothing is known yet (this is not "all quiet")'}
|
|
447
|
+
</Text>
|
|
448
|
+
) : null}
|
|
449
|
+
{hinted && !err ? (
|
|
450
|
+
<Text wrap="truncate" backgroundColor={PALETTE.brand} color="#0B1220" bold>{pad(' ↑↓ move · Enter open · Tab switch panel · ? help ', cols - 2)}</Text>
|
|
451
|
+
) : null}
|
|
452
|
+
</Box>
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (!sit) {
|
|
456
|
+
return (
|
|
457
|
+
<Box flexDirection="column" paddingX={1}>
|
|
458
|
+
{header}
|
|
459
|
+
<Box marginTop={1}>
|
|
460
|
+
<Text color={err ? STATE_HEX.blind : PALETTE.dim}>
|
|
461
|
+
{err ? 'No data yet. LUCID cannot be reached, so nothing is known (this is not "all quiet").' : 'Connecting to LUCID...'}
|
|
462
|
+
</Text>
|
|
463
|
+
</Box>
|
|
464
|
+
<Box marginTop={1}><Text color={PALETTE.dim}>r refresh · q quit</Text></Box>
|
|
465
|
+
</Box>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── panes ───────────────────────────────────────────────────────────
|
|
470
|
+
const focus = (p) => pane === p;
|
|
471
|
+
|
|
472
|
+
const needsBody = (w, rowsAvail) => {
|
|
473
|
+
const iw = innerOf(w);
|
|
474
|
+
const f = focus('needs');
|
|
475
|
+
if (needs.length === 0 && !liveGhosts.length) {
|
|
476
|
+
return (
|
|
477
|
+
<Text color={STATE_HEX.ok}>{filter ? 'Nothing needs you in this area.' : 'Nothing needs you right now.'}{spots.length ? <Text color={PALETTE.dim}> (check what I can't see)</Text> : null}</Text>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const tw = iw - 14;
|
|
481
|
+
const isNewOf = (it) => newAt.current.has(it.ref) && wall - newAt.current.get(it.ref) < NEW_MS;
|
|
482
|
+
const textOf = (it) => (isNewOf(it) ? '● ' : '') + (it.L0 || '');
|
|
483
|
+
const nGhost = Math.min(2, liveGhosts.length);
|
|
484
|
+
const win = fitWindow(needs, safeSel, expandedRef, rowsAvail - nGhost, iw, (it) => wrap(textOf(it), tw).length);
|
|
485
|
+
const above = win.start;
|
|
486
|
+
const below = needs.length - win.end;
|
|
487
|
+
return (
|
|
488
|
+
<>
|
|
489
|
+
{above > 0 ? <Text color={PALETTE.dim}> ↑ {above} more</Text> : null}
|
|
490
|
+
{needs.slice(win.start, win.end).map((it, k) => {
|
|
491
|
+
const i = win.start + k;
|
|
492
|
+
const on = i === safeSel;
|
|
493
|
+
const open = expandedRef === it.ref;
|
|
494
|
+
const a = it.L1 || {};
|
|
495
|
+
const ms = (it.L2 && it.L2.measures) || [];
|
|
496
|
+
const isNew = isNewOf(it);
|
|
497
|
+
const bg = on ? (f ? BAR_BG : BAR_BG_DIM) : undefined;
|
|
498
|
+
const lines = wrap(textOf(it), tw);
|
|
499
|
+
const fg = isNew ? STATE_HEX.ok : on ? '#FFFFFF' : PALETTE.muted;
|
|
500
|
+
return (
|
|
501
|
+
<Box key={it.ref} flexDirection="column">
|
|
502
|
+
{lines.map((ln, li) => (
|
|
503
|
+
<Text key={li} wrap="truncate" backgroundColor={bg} bold={on && f} dimColor={!f && !on}>
|
|
504
|
+
<Text bold color={on ? (f ? '#FFFFFF' : PALETTE.muted) : PALETTE.dim}>{li === 0 ? (on ? '▶ ' : ' ') : ' '}</Text>
|
|
505
|
+
{li === 0 ? <Pill urgency={it.urgency} /> : <Text>{' '.repeat(11)}</Text>}
|
|
506
|
+
<Text color={fg}> {pad(ln, tw)}</Text>
|
|
507
|
+
</Text>
|
|
508
|
+
))}
|
|
509
|
+
{open ? (
|
|
510
|
+
<Box flexDirection="column" marginLeft={3} marginBottom={1}>
|
|
511
|
+
{[['Who', a.who], ['What', a.what], ['Why', a.why], ['When', whenText(a.when, t)]].filter(([, v]) => v).map(([lbl, v]) => (
|
|
512
|
+
<Box key={lbl}><Box width={6}><Text color={PALETTE.dim}>{lbl}</Text></Box><Box width={iw - 9}><Text color={PALETTE.text}>{wrap(v, iw - 9).join('\n')}</Text></Box></Box>
|
|
513
|
+
))}
|
|
514
|
+
{(a.affected || []).length ? (
|
|
515
|
+
<Text wrap="truncate" color={PALETTE.dim}>{pad('Affects', 6)} <Text color={PALETTE.muted}>{clip(a.affected.map(nm).filter(Boolean).join(', '), iw - 14)}</Text></Text>
|
|
516
|
+
) : null}
|
|
517
|
+
{ms.length ? <Text bold color={PALETTE.accent}>What you can do</Text> : <Text color={PALETTE.dim}>No actions to offer here. Esc closes.</Text>}
|
|
518
|
+
{ms.map((m, mi) => {
|
|
519
|
+
const mOn = mi === mSel;
|
|
520
|
+
return (
|
|
521
|
+
<Text key={m.id} wrap="truncate" backgroundColor={mOn ? '#6D28D9' : undefined} bold={mOn}>
|
|
522
|
+
<Text color={mOn ? '#FFFFFF' : PALETTE.dim}>{mOn ? '❯ ' : ' '}</Text>
|
|
523
|
+
<Text color={mOn ? '#FFFFFF' : PALETTE.text}>{pad(clip(m.say || m.label, iw - 24), iw - 24)}</Text>
|
|
524
|
+
<Text color={m.risk === 'dangerous' ? STATE_HEX.blind : m.risk === 'gated' ? STATE_HEX.attention : mOn ? '#FFFFFF' : PALETTE.dim}> {m.risk}</Text>
|
|
525
|
+
</Text>
|
|
526
|
+
);
|
|
527
|
+
})}
|
|
528
|
+
{ms.some((m) => m.command) && ms[mSel] && ms[mSel].command
|
|
529
|
+
? <Text wrap="truncate" color={PALETTE.code}> $ {clip(ms[mSel].command, iw - 12)}</Text> : (ms.some((m) => m.command) ? <Text> </Text> : null)}
|
|
530
|
+
</Box>
|
|
531
|
+
) : null}
|
|
532
|
+
</Box>
|
|
533
|
+
);
|
|
534
|
+
})}
|
|
535
|
+
{liveGhosts.slice(0, 2).map((g) => (
|
|
536
|
+
<Text key={'g' + g.item.ref} wrap="truncate" color={PALETTE.dim} dimColor> ✓ resolved {clip(g.item.L0 || '', iw - 14)}</Text>
|
|
537
|
+
))}
|
|
538
|
+
{below > 0 ? <Text color={PALETTE.dim}> ↓ {below} more</Text> : null}
|
|
539
|
+
</>
|
|
540
|
+
);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const changedBody = (w, rowsAvail) => {
|
|
544
|
+
const iw = innerOf(w);
|
|
545
|
+
const f = focus('changed');
|
|
546
|
+
if (!changed.length) return <Text color={PALETTE.dim}>Nothing has changed.</Text>;
|
|
547
|
+
const tw = iw - 7;
|
|
548
|
+
const textOf = (c) => {
|
|
549
|
+
const words = changedWords(c);
|
|
550
|
+
return (c.L0 || '') + (words ? ` · ${words}` : '');
|
|
551
|
+
};
|
|
552
|
+
const win = fitWindow(changed, safeCh, null, rowsAvail, iw, (c) => wrap(textOf(c), tw).length);
|
|
553
|
+
const above = win.start;
|
|
554
|
+
const below = changed.length - win.end;
|
|
555
|
+
return (
|
|
556
|
+
<>
|
|
557
|
+
{above > 0 ? <Text color={PALETTE.dim}> ↑ {above} more</Text> : null}
|
|
558
|
+
{changed.slice(win.start, win.end).map((c, k) => {
|
|
559
|
+
const i = win.start + k;
|
|
560
|
+
const on = i === safeCh && f;
|
|
561
|
+
return (
|
|
562
|
+
<Box key={c.ref + c.ts} flexDirection="column">
|
|
563
|
+
{wrap(textOf(c), tw).map((ln, li) => (
|
|
564
|
+
<Text key={li} wrap="truncate" backgroundColor={on ? BAR_BG : undefined} bold={on} dimColor={!f}>
|
|
565
|
+
<Text color={on ? '#FFFFFF' : PALETTE.dim}>{li === 0 ? `${on ? '▶ ' : ' '}${pad(ageOf(c.ts, t), 3)} ` : ' '.repeat(7)}</Text>
|
|
566
|
+
<Text color={on ? '#FFFFFF' : PALETTE.text}>{pad(ln, tw)}</Text>
|
|
567
|
+
</Text>
|
|
568
|
+
))}
|
|
569
|
+
</Box>
|
|
570
|
+
);
|
|
571
|
+
})}
|
|
572
|
+
{below > 0 ? <Text color={PALETTE.dim}> ↓ {below} more</Text> : null}
|
|
573
|
+
</>
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const areasBody = (w, rowsAvail) => {
|
|
578
|
+
const iw = innerOf(w);
|
|
579
|
+
const f = focus('areas');
|
|
580
|
+
const list = [{ area: null, label: 'All areas', state: null }, ...areas];
|
|
581
|
+
const win = winLines(list.length, safeAr, rowsAvail);
|
|
582
|
+
return list.slice(win.start, win.end).map((a, k) => {
|
|
583
|
+
const i = win.start + k;
|
|
584
|
+
const on = i === safeAr && f;
|
|
585
|
+
const active = (a.area || null) === filter;
|
|
586
|
+
const n = a.area ? allNeeds.filter((x) => x.area === a.area).length : allNeeds.length;
|
|
587
|
+
return (
|
|
588
|
+
<Text key={a.area || 'all'} wrap="truncate" backgroundColor={on ? BAR_BG : undefined} bold={on} dimColor={!f && !active}>
|
|
589
|
+
<Text color={on ? '#FFFFFF' : PALETTE.dim}>{on ? '▶ ' : ' '}</Text>
|
|
590
|
+
<Text color={a.state ? (STATE_HEX[a.state] || PALETTE.muted) : PALETTE.muted}>{a.state ? (GLYPH[a.state] || '?') : '◉'} </Text>
|
|
591
|
+
<Text color={on ? '#FFFFFF' : PALETTE.text}>{pad(clip(a.label || a.area, iw - 9), iw - 9)}</Text>
|
|
592
|
+
<Text color={n ? STATE_HEX.attention : PALETTE.dim}> {n ? String(n).padStart(2) : ' ·'}</Text>
|
|
593
|
+
<Text color={PALETTE.accent}>{active ? ' ●' : ' '}</Text>
|
|
594
|
+
</Text>
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const counts = {
|
|
600
|
+
needs: needs.length + (filter ? `/${allNeeds.length}` : ''),
|
|
601
|
+
changed: changed.length,
|
|
602
|
+
areas: areas.length,
|
|
603
|
+
};
|
|
604
|
+
const bodies = { needs: needsBody, changed: changedBody, areas: areasBody };
|
|
605
|
+
|
|
606
|
+
let body;
|
|
607
|
+
if (help) {
|
|
608
|
+
const K = (k, d) => (
|
|
609
|
+
<Text key={k}><Text bold color={PALETTE.brand}>{pad(k, 14)}</Text><Text color={PALETTE.text}>{d}</Text></Text>
|
|
610
|
+
);
|
|
611
|
+
body = (
|
|
612
|
+
<Panel title="KEYS" count="" focused width={cols - 2} height={bodyH}>
|
|
613
|
+
{K('↑ ↓ j k', 'move the highlighted row (inside an open item: pick an action)')}
|
|
614
|
+
{K('Tab / S-Tab', 'switch panel: Needs you, Since you looked, Areas')}
|
|
615
|
+
{K('Enter', 'open an item / do the highlighted action / filter by area')}
|
|
616
|
+
{K('d', 'do something about the selected item (asks y/n first)')}
|
|
617
|
+
{K('Esc', 'close the item, cancel a question, clear the area filter')}
|
|
618
|
+
{K('u', 'teach: treat this kind of item as not urgent (asks y/n)')}
|
|
619
|
+
{K('c', 'teach: you care about this item (asks y/n)')}
|
|
620
|
+
{K('m', 'mark everything since you looked as seen')}
|
|
621
|
+
{K('r', 'refresh now (it also refreshes every 10s)')}
|
|
622
|
+
{K('?', 'close this help')}
|
|
623
|
+
{K('q Ctrl-C', 'quit')}
|
|
624
|
+
<Text color={PALETTE.dim}>Press ? or Esc to go back.</Text>
|
|
625
|
+
</Panel>
|
|
626
|
+
);
|
|
627
|
+
} else if (wide) {
|
|
628
|
+
const sl = slide ? slide * 4 : 0;
|
|
629
|
+
const needsH = big ? bodyH : Math.ceil(bodyH * 0.6);
|
|
630
|
+
const chH = bodyH - needsH;
|
|
631
|
+
body = (
|
|
632
|
+
<Box width={cols - 2 - sl} marginLeft={sl}>
|
|
633
|
+
<Panel title={PANE_TITLE.areas} count={counts.areas} focused={focus('areas')} width={leftW} height={bodyH}>
|
|
634
|
+
{areasBody(leftW, listRows)}
|
|
635
|
+
</Panel>
|
|
636
|
+
{big ? (
|
|
637
|
+
<>
|
|
638
|
+
<Panel title={PANE_TITLE.needs} count={counts.needs} focused={focus('needs')} width={mainW + sl} height={bodyH} note={filterLabel ? `· ${filterLabel}` : ''}>
|
|
639
|
+
{needsBody(mainW, listRows)}
|
|
640
|
+
</Panel>
|
|
641
|
+
<Panel title={PANE_TITLE.changed} count={counts.changed} focused={focus('changed')} width={rightW} height={bodyH}>
|
|
642
|
+
{changedBody(rightW, listRows)}
|
|
643
|
+
</Panel>
|
|
644
|
+
</>
|
|
645
|
+
) : (
|
|
646
|
+
<Box flexDirection="column">
|
|
647
|
+
<Panel title={PANE_TITLE.needs} count={counts.needs} focused={focus('needs')} width={mainW + sl} height={needsH} note={filterLabel ? `· ${filterLabel}` : ''}>
|
|
648
|
+
{needsBody(mainW, needsH - 3)}
|
|
649
|
+
</Panel>
|
|
650
|
+
<Panel title={PANE_TITLE.changed} count={counts.changed} focused={focus('changed')} width={mainW + sl} height={chH}>
|
|
651
|
+
{changedBody(mainW, chH - 3)}
|
|
652
|
+
</Panel>
|
|
653
|
+
</Box>
|
|
654
|
+
)}
|
|
655
|
+
</Box>
|
|
656
|
+
);
|
|
657
|
+
} else {
|
|
658
|
+
const stackW = cols - 2;
|
|
659
|
+
const others = PANES.filter((p) => p !== pane);
|
|
660
|
+
const focH = Math.max(6, bodyH - others.length);
|
|
661
|
+
body = (
|
|
662
|
+
<Box flexDirection="column">
|
|
663
|
+
<Panel title={PANE_TITLE[pane]} count={counts[pane]} focused width={stackW} height={focH} note={filterLabel && pane === 'needs' ? `· ${filterLabel}` : ''}>
|
|
664
|
+
{bodies[pane](stackW, focH - 3)}
|
|
665
|
+
</Panel>
|
|
666
|
+
{others.map((p) => (
|
|
667
|
+
<Text key={p} wrap="truncate" color={PALETTE.dim}> ▸ {PANE_TITLE[p]} {counts[p]} (Tab)</Text>
|
|
668
|
+
))}
|
|
669
|
+
</Box>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ── status line (always says what you are on and what Enter does) ───
|
|
674
|
+
let where; let act;
|
|
675
|
+
if (confirm) {
|
|
676
|
+
where = 'Waiting for your answer'; act = [['y', 'yes, do it'], ['n', 'cancel']];
|
|
677
|
+
} else if (help) {
|
|
678
|
+
where = 'Help'; act = [['?', 'back'], ['q', 'quit']];
|
|
679
|
+
} else if (pane === 'needs') {
|
|
680
|
+
if (!cur) { where = `▶ Needs you · ${filter ? 'none in ' + filterLabel : 'nothing needs you'}`; act = [['Tab', 'switch panel'], ['?', 'help']]; }
|
|
681
|
+
else if (isOpen) {
|
|
682
|
+
const m = curMeasures[mSel];
|
|
683
|
+
where = `▶ Needs you · ${safeSel + 1} of ${needs.length} · open: ${clip(cur.L0 || '', 50)}`;
|
|
684
|
+
act = m ? [['↑↓', 'pick action'], ['Enter', `do "${clip(m.label, 18)}" (asks first)`], ['Esc', 'close']] : [['Esc', 'close']];
|
|
685
|
+
} else {
|
|
686
|
+
where = `▶ Needs you · ${safeSel + 1} of ${needs.length} · ${clip(cur.L0 || '', 60)}`;
|
|
687
|
+
act = [['Enter', 'open'], ['d', 'do something'], ['u', 'not urgent'], ['c', 'I care'], ['m', 'mark all seen']];
|
|
688
|
+
}
|
|
689
|
+
} else if (pane === 'changed') {
|
|
690
|
+
const c = changed[safeCh];
|
|
691
|
+
where = c ? `▶ Since you looked · ${safeCh + 1} of ${changed.length} · ${clip(c.L0 || '', 60)}` : '▶ Since you looked · nothing changed';
|
|
692
|
+
act = [['Enter', 'show detail'], ['m', 'mark all seen'], ['Tab', 'next panel']];
|
|
693
|
+
} else {
|
|
694
|
+
const a = areas[safeAr - 1];
|
|
695
|
+
where = `▶ Areas · ${safeAr + 1} of ${areas.length + 1} · ${a ? a.label + ': ' + clip(a.L0 || '', 50) : 'all areas'}`;
|
|
696
|
+
act = [['Enter', a ? `filter Needs you to ${a.label}` : 'show everything'], ['Tab', 'next panel']];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const hints = confirm ? [['y', 'yes'], ['n', 'no'], ['Esc', 'cancel']]
|
|
700
|
+
: help ? [['?/Esc', 'close help'], ['q', 'quit']]
|
|
701
|
+
: isOpen ? [['↑↓', 'pick action'], ['Enter/d', 'do it'], ['Esc', 'close'], ['?', 'help'], ['q', 'quit']]
|
|
702
|
+
: [['↑↓', 'move'], ['Enter', pane === 'areas' ? 'filter' : 'open'], ['Tab', 'switch panel'], ['d', 'do'], ['m', 'mark seen'], ['?', 'help'], ['q', 'quit']];
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<Box flexDirection="column" paddingX={1}>
|
|
706
|
+
{header}
|
|
707
|
+
{body}
|
|
708
|
+
{confirm ? (
|
|
709
|
+
<Text wrap="truncate" backgroundColor={STATE_HEX.attention} color="#0B1220" bold> {confirm.text.endsWith('y/n') ? confirm.text : confirm.text + ' y/n'}{confirm.extra ? ' · ' + confirm.extra : ''} </Text>
|
|
710
|
+
) : noteLive ? (
|
|
711
|
+
<Text wrap="truncate" bold color={noteHex}>{noteLive.tone === 'ok' ? '✔ ' : noteLive.tone === 'bad' ? '✖ ' : noteLive.tone === 'busy' ? '◐ ' : 'ℹ '}{noteLive.text}</Text>
|
|
712
|
+
) : (
|
|
713
|
+
<Text wrap="truncate" color={spots.length || dau.length ? STATE_HEX.attention : PALETTE.dim}>
|
|
714
|
+
{dau.length ? `doesn't add up (${dau.length}): ${clip(dau[0].L0 || '', 60)} · ` : ''}{spots.length ? `can't see: ${spots.map((s) => s.text).join(' · ')}` : 'Every expected source is speaking.'}
|
|
715
|
+
</Text>
|
|
716
|
+
)}
|
|
717
|
+
<Text wrap="truncate">
|
|
718
|
+
<Text bold color={PALETTE.accent}>{where}</Text>
|
|
719
|
+
<Text color={PALETTE.dim}> · </Text>
|
|
720
|
+
{act.map(([k, l], i) => (
|
|
721
|
+
<Text key={k}>{i ? ' ' : ''}<Text bold color={PALETTE.brand}>{k}:</Text><Text color={PALETTE.muted}> {l}</Text></Text>
|
|
722
|
+
))}
|
|
723
|
+
</Text>
|
|
724
|
+
<Hints items={hints} cols={cols} />
|
|
725
|
+
</Box>
|
|
726
|
+
);
|
|
727
|
+
}
|