quoxcli 0.3.1 → 0.5.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/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
package/lib/quoxon/render.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* QUOXON view layer (Ink 6, React.createElement). Width-aware everywhere,
|
|
3
3
|
* horizontal rules only (no side borders), semantic colour, mono-safe.
|
|
4
4
|
*/
|
|
5
|
+
import { toolCardLines } from './toolCards.js';
|
|
5
6
|
import React from 'react';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import { Box, Text, useStdout } from 'ink';
|
|
@@ -23,11 +24,13 @@ import { dividerRuleEl } from './dividerRule.js';
|
|
|
23
24
|
import * as themeNs from './theme.js';
|
|
24
25
|
import { qArt, qArtFrame } from '../core/banner.js';
|
|
25
26
|
import { cmd } from './palette.js';
|
|
26
|
-
import { themeRole,
|
|
27
|
+
import { themeRole, layoutRows, expandLines, limitGrid } from './rows.js';
|
|
28
|
+
import { wrapHang } from './objects/model.js';
|
|
27
29
|
import { Markdown, wrapCell, markdownRows, wrapRows, WText, Heading } from './markdown.js';
|
|
28
30
|
import { parseAsk, keysLabel, askText } from './ask.js';
|
|
29
31
|
import { parseAsideNote } from './asideView.js';
|
|
30
32
|
import { formatSlotBody, inlineable, codeRows } from './slotFormat.js';
|
|
33
|
+
import { BACKGROUND_NOTE } from './backgroundWork.js';
|
|
31
34
|
import { termRows } from './term.js';
|
|
32
35
|
import { cellWidth, ambiguousWide } from './cells.js';
|
|
33
36
|
import { welcomeModel } from './welcome.js';
|
|
@@ -41,6 +44,8 @@ import { CLIPS as DRAGON_CLIPS, clipCells } from './qdragonScenes.js';
|
|
|
41
44
|
import { dragonLayout } from './qdragon.js';
|
|
42
45
|
import { ObjectView } from './objects/view.js';
|
|
43
46
|
import { aoclLineSegs, aoclTableLines } from './aocl.js';
|
|
47
|
+
import { cardLines, wrapText, loopPinned } from './loopCard.js';
|
|
48
|
+
import { orgLabel } from './orgLabel.js';
|
|
44
49
|
export { COLORS, MODE_COLOR, MIN_COLS, clampCols, clip, reducedMotion };
|
|
45
50
|
|
|
46
51
|
// ---- working indicator (the one animated line) -------------------------------------------------
|
|
@@ -254,15 +259,24 @@ export function ToolRow({ b, cols, fx = null, pulse = null, ignite = 0 }) {
|
|
|
254
259
|
const running = b.status === 'running';
|
|
255
260
|
// the facts after the result: duration, then witnessing state
|
|
256
261
|
let after = [];
|
|
262
|
+
let envExtra = null; // an id never shares a clipped line: when it does not fit it wraps onto its own line(s)
|
|
257
263
|
if (!running && b.receipt) {
|
|
258
264
|
const v = b.receipt.verified;
|
|
259
265
|
after = [dur, v === true ? 'witnessed' : v === false ? 'unverified' : 'verification pending', `receipt ward:${b.receipt.ward_seq}`].filter(Boolean);
|
|
266
|
+
if (b.envelopeId) envExtra = `envelope ${b.envelopeId}`;
|
|
260
267
|
} else if (!running) {
|
|
261
|
-
after = [dur, b.envelopeId ? `envelope ${
|
|
268
|
+
after = [dur, b.envelopeId ? `envelope ${b.envelopeId}` : '', b.envelopeId ? 'witnessing pending' : ''].filter(Boolean);
|
|
262
269
|
}
|
|
263
270
|
const result = running ? '' : deleg && deleg.result ? deleg.result : (b.resultSummary ? `${b.status === 'unknown' ? 'unknown: ' : ''}${b.resultSummary}` : b.status);
|
|
264
271
|
const bodyStr = clipWords(body0, running || !result ? room : Math.max(6, Math.min(gw(body0), Math.floor(room / 2))));
|
|
265
272
|
const tailRoom = room - gw(bodyStr) - 3;
|
|
273
|
+
if (!running && !b.receipt && b.envelopeId && gw(after.join(' · ')) + 3 > Math.max(0, tailRoom)) {
|
|
274
|
+
envExtra = `envelope ${b.envelopeId} · witnessing pending`;
|
|
275
|
+
after = after.filter(f => !/^envelope /.test(f) && f !== 'witnessing pending');
|
|
276
|
+
}
|
|
277
|
+
const extra = envExtra ? wrapHang(envExtra, Math.max(12, cols - 6), 2).map((l, i) => h(Text, { key: `e${i}`, color: c('muted') }, ` ${l}`)) : [];
|
|
278
|
+
if (!running) extra.push(...toolCardLines(b, cols).map((l, i) => h(Text, { key: `c${i}`, color: c(l.head ? 'muted' : 'prose'), wrap: 'truncate-end' }, ` ${l.text}`))); // HITL-INBOX-BAR: a list answer as a table
|
|
279
|
+
const withExtra = (node) => (extra.length ? h(Box, { flexDirection: 'column' }, node, ...extra) : node);
|
|
266
280
|
let tail = '';
|
|
267
281
|
if (!running && tailRoom > 0) {
|
|
268
282
|
const facts = after.join(' · ');
|
|
@@ -285,9 +299,9 @@ export function ToolRow({ b, cols, fx = null, pulse = null, ignite = 0 }) {
|
|
|
285
299
|
if (fx?.settle != null && !running) {
|
|
286
300
|
const flash = fx.settle < 0.12 && !mono();
|
|
287
301
|
if (flash) head[1] = { t: glyph, color: '#ffffff', bold: true };
|
|
288
|
-
return h(Box, null, ...fxNodes(head, null), ...fxNodes(tailSegs, { e: fx.settle, timing: TIMING.settle }, 7));
|
|
302
|
+
return withExtra(h(Box, null, ...fxNodes(head, null), ...fxNodes(tailSegs, { e: fx.settle, timing: TIMING.settle }, 7)));
|
|
289
303
|
}
|
|
290
|
-
return h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3));
|
|
304
|
+
return withExtra(h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3)));
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
/** Compact protocol detail: drop the quox.chat./quox. intent prefix and keep only the tail of long correlation ids, so the line fits instead of clipping. */
|
|
@@ -381,6 +395,7 @@ export function HelpView({ model, cols }) {
|
|
|
381
395
|
const keyW = Math.min(24, Math.max(...model.filter(m => m.key).map(m => [...m.key].length), 8) + 2);
|
|
382
396
|
return h(Box, { flexDirection: 'column', marginTop: 1, width: cols }, ...model.map((m, i) => {
|
|
383
397
|
if (m.kind === 'gap') return h(Text, { key: i }, ' ');
|
|
398
|
+
if (m.kind === 'subhead') return h(Heading, { key: i, text: m.text });
|
|
384
399
|
if (m.kind === 'head') return m.dim ? h(Text, { key: i, bold: true, color: c('muted') }, m.text) : h(Heading, { key: i, text: m.text });
|
|
385
400
|
if (m.kind === 'row' || (m.kind === 'dim' && m.key)) {
|
|
386
401
|
const dim = m.kind === 'dim';
|
|
@@ -511,6 +526,7 @@ const GROUP = { tool: 'tools', trace: 'tools', note: 'notice', error: 'notice',
|
|
|
511
526
|
const FOOTER = new Set(['aocl', 'summary']);
|
|
512
527
|
export function historyGap(prev, item, rows = null) {
|
|
513
528
|
if (!item || SELF_SPACED.has(item.kind)) return 0;
|
|
529
|
+
if (item.kind === 'loopcard' && loopPinned(item.card)) return 0; // drawn above the composer, not here
|
|
514
530
|
if (item.kind === 'user') return 1;
|
|
515
531
|
if (!prev) return item.kind === 'text' ? 1 : 0;
|
|
516
532
|
if (rows != null && rows < 20) return 0;
|
|
@@ -546,6 +562,7 @@ export function HistoryItem({ item, cols, live = false, fx = null, pulse = null,
|
|
|
546
562
|
case 'error': return h(ErrorView, { item, cols, fx });
|
|
547
563
|
case 'note': return h(NoteView, { item, cols, fx });
|
|
548
564
|
case 'slotmsg': return h(SlotMsgView, { item, cols });
|
|
565
|
+
case 'loopcard': return loopPinned(item.card) ? null : h(LoopCardView, { item, cols });
|
|
549
566
|
case 'tip': return h(TipView, { item, cols });
|
|
550
567
|
case 'whatsnew': return h(WhatsNewView, { item, cols });
|
|
551
568
|
case 'bcard': return h(BroadcastCardView, { item, cols });
|
|
@@ -572,7 +589,10 @@ export function noteLines(text, width) {
|
|
|
572
589
|
}
|
|
573
590
|
/** A note line that is one bare URL longer than the row: never hard-wrapped (a break inside a token breaks copy and click). */
|
|
574
591
|
const longUrl = (l, width) => { const t = stripMarks(l); return /^https?:\/\/\S+$/.test(t) && graphemes(t).length > width; };
|
|
575
|
-
|
|
592
|
+
/** A note line that opens with a status glyph and wraps hangs under its text, not under the glyph (the glyph keeps its own 2 cells). */
|
|
593
|
+
const GLYPH_LEAD = /^(?:▶|✔|✖|⚠|∞) /u;
|
|
594
|
+
const hangs = (l, width) => GLYPH_LEAD.test(l) && !longUrl(l, width) && wrapRows(stripMarks(l), width) > 1;
|
|
595
|
+
export const noteRows = (text, cols, tone) => noteLines(toneText(text, tone), cols - 2).reduce((n, l) => n + (longUrl(l, cols - 2) ? 1 : hangs(l, cols - 2) ? wrapRows(stripMarks(l).slice(2), cols - 4) : wrapRows(stripMarks(l), cols - 2)), 0);
|
|
576
596
|
|
|
577
597
|
/**
|
|
578
598
|
* Z2H-7B3: a failure message can be several lines (what ran, what did not, the advice). Every line shows,
|
|
@@ -618,6 +638,8 @@ function NoteView({ item, cols, fx = null }) {
|
|
|
618
638
|
// While it reveals, a line that fits decodes in; a line that wraps keeps plain wrapping (never mis-measured).
|
|
619
639
|
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...noteLines(text, cols - 2).map((l, i) => longUrl(l, cols - 2)
|
|
620
640
|
? h(Box, { key: i, width: graphemes(stripMarks(l)).length, flexShrink: 0 }, h(Text, { color: c('link'), wrap: 'truncate-end' }, stripMarks(l)))
|
|
641
|
+
: hangs(l, cols - 2)
|
|
642
|
+
? h(Box, { key: i }, h(Box, { width: 2, flexShrink: 0 }, h(Text, null, ...runs(paintLine(l.slice(0, 2), item.tone)))), h(Box, { flexGrow: 1, flexShrink: 1 }, h(WText, { color, bold: item.tone === 'ask' }, ...runs(paintLine(l.slice(2), item.tone)))))
|
|
621
643
|
: (fx && wrapRows(stripMarks(l), cols - 2) === 1
|
|
622
644
|
? h(Text, { key: i }, ...fxNodes(paintLine(l, item.tone).map(r => (typeof r === 'string' ? { t: r, color: 'prose' } : { t: r.text, color: r.role || 'prose' })), { e: fx.e - i * 0.05, timing: fx.timing }, 17 + i))
|
|
623
645
|
: h(WText, { key: i, color, bold: item.tone === 'ask' }, ...runs(paintLine(l, item.tone))))));
|
|
@@ -628,6 +650,19 @@ function NoteView({ item, cols, fx = null }) {
|
|
|
628
650
|
* outgoing "→ quox2 04:56 hello ✔" (sky: you). A file shows its name, three lines and a /slot log pointer;
|
|
629
651
|
* /slot log prints the whole file (`full`).
|
|
630
652
|
*/
|
|
653
|
+
/** QLOOP-LIVE: the live loop card. The card object is updated in place by loopCards.js; this reads it on every draw. */
|
|
654
|
+
export const loopCardLines = (card, cols, now = Date.now(), hint = false) => cardLines(card.state, { cols, now, frame: Math.floor(now / 250), expanded: card.expanded, focused: card.focused, confirm: card.confirm, hint });
|
|
655
|
+
const cardMemo = new WeakMap(); // card -> { key, lines }: re-laid-out only when what it draws changed
|
|
656
|
+
export function LoopCardView({ item, cols, pinned = false }) {
|
|
657
|
+
const card = item.card, now = Date.now(), st = card.state;
|
|
658
|
+
const key = [cols, pinned, card.focused, card.expanded, card.confirm, Math.floor(now / 250), Math.floor(now / 1000), st, st.observed?.length, st.spool?.length].map(String).join('|');
|
|
659
|
+
let hit = cardMemo.get(card);
|
|
660
|
+
if (!hit || hit.st !== st || hit.key !== key) { hit = { st, key, lines: loopCardLines(card, cols, now, pinned) }; cardMemo.set(card, hit); }
|
|
661
|
+
const lines = hit.lines;
|
|
662
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 2, width: cols },
|
|
663
|
+
...lines.map((rs, i) => h(Text, { key: i }, ...rs.map((r, j) => h(Text, { key: j, color: c(r.role), bold: r.bold || undefined }, r.t)))));
|
|
664
|
+
}
|
|
665
|
+
|
|
631
666
|
function SlotMsgView({ item, cols }) {
|
|
632
667
|
const l = item.line || {};
|
|
633
668
|
if (l.roomLine) { // QUOXON-ROOMS: `#group-chat QUOXON: text`, a stable colour per sender
|
|
@@ -699,10 +734,12 @@ function TipView({ item, cols }) {
|
|
|
699
734
|
const body = wrapPlain(item.text, inner).map((l, i) => row(`t${i}`, [h(Text, { key: 0, color: c('prose') }, l)], [...l].length));
|
|
700
735
|
const cmdText = clip(item.cmd, inner - 5);
|
|
701
736
|
const tryRow = row('try', [h(Text, { key: 0, color: c('prose'), bold: true }, 'Try: '), h(Text, { key: 1, color: c('command') }, cmdText)], 5 + [...cmdText].length);
|
|
702
|
-
const
|
|
737
|
+
const hintPlain = stripMarks(TIP_HINT); // too long for the frame: wrap it inside, never let the terminal wrap the border
|
|
738
|
+
const hintRows = [...hintPlain].length <= inner ? [row('hint', runs(paintRuns(TIP_HINT, 'quiet')), [...hintPlain].length)]
|
|
739
|
+
: wrapPlain(hintPlain, inner).map((l, i) => row(`hint${i}`, [h(Text, { key: 0, color: c('quiet') }, l)], [...l].length));
|
|
703
740
|
return h(Box, { paddingLeft: 2, flexDirection: 'column' },
|
|
704
741
|
h(Text, edge, '╭─ ', h(Text, { bold: true }, 'Tip'), ' ', '─'.repeat(W - 8), '╮'),
|
|
705
|
-
...body, tryRow,
|
|
742
|
+
...body, tryRow, ...hintRows,
|
|
706
743
|
h(Text, edge, '╰', '─'.repeat(W - 2), '╯'));
|
|
707
744
|
}
|
|
708
745
|
|
|
@@ -730,9 +767,13 @@ function WhatsNewView({ item, cols }) {
|
|
|
730
767
|
return out;
|
|
731
768
|
});
|
|
732
769
|
if (more) rows.push(row('more', [h(Text, { key: 0, color: c('prose') }, lead), h(Text, { key: 1, color: c('prose'), bold: true }, 'Try: '), h(Text, { key: 2, color: c('command') }, '/whatsnew')], [...more].length));
|
|
770
|
+
// a head that does not fit in the top edge (the compact "N new commands (...)" line at a narrow width) goes inside the
|
|
771
|
+
// frame, wrapped, under a bare top edge: the terminal must never be the one to wrap a border row
|
|
772
|
+
const headFits = [...HEAD].length <= W - 6;
|
|
773
|
+
if (!headFits) rows.unshift(...wrapPlain(HEAD, inner).map((l, i) => row(`h${i}`, [h(Text, { key: 0, color: c('prose'), bold: true }, l)], [...l].length)));
|
|
733
774
|
if (item.pad) { rows.unshift(row('p0', [], 0)); rows.push(row('p1', [], 0)); }
|
|
734
775
|
return h(Box, { paddingLeft: 2, flexDirection: 'column' },
|
|
735
|
-
h(Text, edge, '╭─ ', h(Text, { bold: true }, HEAD), ' ', '─'.repeat(
|
|
776
|
+
headFits ? h(Text, edge, '╭─ ', h(Text, { bold: true }, HEAD), ' ', '─'.repeat(W - 5 - [...HEAD].length), '╮') : h(Text, edge, '╭', '─'.repeat(W - 2), '╮'),
|
|
736
777
|
...rows,
|
|
737
778
|
h(Text, edge, '╰', '─'.repeat(W - 2), '╯'));
|
|
738
779
|
}
|
|
@@ -770,6 +811,8 @@ export function liveRows(blocks, cols) {
|
|
|
770
811
|
case 'summary': return n + noteLines(b.text, cols - 4).length;
|
|
771
812
|
case 'error': return n + errorRows(b.text, cols);
|
|
772
813
|
case 'slotmsg': return n + (b.line?.run && !b.line.run.settled ? 9 + Math.ceil(String(b.line.run.cmd || '').length / Math.max(10, cols - 8)) : 1); // ASK-SETTLE: an armed run card is held live
|
|
814
|
+
case 'tool': return n + 1 + (b.status === 'running' ? 0 : toolCardLines(b, cols).length);
|
|
815
|
+
case 'loopcard': return n + (loopPinned(b.card) ? 0 : 1 + loopCardLines(b.card, cols).length);
|
|
773
816
|
default: return n + 1;
|
|
774
817
|
}
|
|
775
818
|
}, 0);
|
|
@@ -790,10 +833,11 @@ export const Grad = ({ text }) => {
|
|
|
790
833
|
* never wraps: full, then a shortened provider (`anth claude-sonnet-5-5`), then the model alone, then the
|
|
791
834
|
* model clipped. Leaves at least four rule cells on the left. Below 30 cols, or no model: ''.
|
|
792
835
|
*/
|
|
793
|
-
export function modelRuleLabel(cols, model) {
|
|
836
|
+
export function modelRuleLabel(cols, model, reserve = 0) {
|
|
794
837
|
const full = String(model || '').trim();
|
|
795
838
|
if (!full || cols < 30) return '';
|
|
796
|
-
const room = cols - 7; // ' label ' + one trailing dash + four leading dashes
|
|
839
|
+
const room = cols - 7 - reserve; // ' label ' + one trailing dash + four leading dashes (+ the approvals indicator)
|
|
840
|
+
if (reserve && room < 6) return '';
|
|
797
841
|
const fits = (t) => cellWidth(t) <= room; // cells under the ambiguous-width policy (cells.js)
|
|
798
842
|
if (fits(full)) return full;
|
|
799
843
|
const segs = full.split(' · ');
|
|
@@ -803,7 +847,43 @@ export function modelRuleLabel(cols, model) {
|
|
|
803
847
|
const provider = segs.length > 1 ? segs[0] : words.length > 1 ? words[0] : '';
|
|
804
848
|
if (provider) { const short = `${provider.slice(0, 4)} ${name}`; if (fits(short)) return short; }
|
|
805
849
|
if (fits(name)) return name;
|
|
806
|
-
return clip(name, room);
|
|
850
|
+
return reserve ? '' : clip(name, room); // with the approvals indicator the model is dropped before the indicator is cut
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Pure: a failed approvals poll goes stale (footer `?`) only after 3 consecutive misses or 60 s without a good read. */
|
|
854
|
+
export function approvalsPollStale({ fails, lastOkAt, now }) {
|
|
855
|
+
if (!(fails > 0)) return false;
|
|
856
|
+
return fails >= 3 || (typeof lastOkAt === 'number' && now - lastOkAt > 60000);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Pure: the permanent approvals indicator on the bottom rule. `a` = { count, error, signedOut }.
|
|
861
|
+
* Never a false 0: signed out, an unreachable inbox, or a count not read yet is `approvals ?` (warn).
|
|
862
|
+
*/
|
|
863
|
+
export function approvalsIndicator(a) {
|
|
864
|
+
if (!a) return null;
|
|
865
|
+
const n = a.count;
|
|
866
|
+
if (!a.signedOut && a.error && a.slow) return { text: 'approvals ? (server slow)', role: 'warn' }; // sustained failure, never an all-clear
|
|
867
|
+
if (a.signedOut || a.error || typeof n !== 'number') return { text: 'approvals ?', role: 'warn' };
|
|
868
|
+
// Never an all-clear next to outstanding items or an unseen source: scope the claim to approvals and say what else is open.
|
|
869
|
+
const out = Number(a.outstanding) || 0, blind = Number(a.blind) || 0;
|
|
870
|
+
if (n === 0 && (out > 0 || blind > 0)) {
|
|
871
|
+
const more = [out > 0 ? `${out} other item${out === 1 ? '' : 's'} open` : null, blind > 0 ? `can't see ${blind} source${blind === 1 ? '' : 's'}` : null].filter(Boolean).join(' · ');
|
|
872
|
+
return { text: `no approvals in this view · ${more}`, role: 'warn' };
|
|
873
|
+
}
|
|
874
|
+
// Other orgs the user belongs to (cached counts, core/crossOrgApprovals): never an all-clear beside a waiting approval elsewhere.
|
|
875
|
+
const others = (Array.isArray(a.others) ? a.others : []).filter((o) => o && o.count > 0);
|
|
876
|
+
const elsewhere = others.reduce((t, o) => t + o.count, 0);
|
|
877
|
+
if (n === 0 && elsewhere > 0) return { text: others.length === 1 ? `⏸ ${elsewhere} in ${others[0].name} · /approvals` : `⏸ ${elsewhere} in ${others.length} other orgs · /approvals`, role: 'warn' };
|
|
878
|
+
if (n === 0) return { text: '✔ nothing waiting', role: 'ok' };
|
|
879
|
+
return { text: `⏸ ${n} approval${n === 1 ? '' : 's'} waiting${elsewhere ? ` · +${elsewhere} in ${others.length === 1 ? others[0].name : `${others.length} other orgs`}` : ''}`, role: 'warn' };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Pure: the labels for this width. The indicator sits top-left, the model bottom-right: independent of each other. */
|
|
883
|
+
export function barLabels(cols, model, a) {
|
|
884
|
+
const ind = cols >= 30 ? approvalsIndicator(a) : null;
|
|
885
|
+
if (ind && cols - cellWidth(` ${ind.text} `) - 5 < 4) return { model: modelRuleLabel(cols, model), ind: null }; // no room for the whole indicator
|
|
886
|
+
return { model: modelRuleLabel(cols, model), ind };
|
|
807
887
|
}
|
|
808
888
|
|
|
809
889
|
/** Pure: the rule split around a right-aligned session name: `───── name ─`. Below 30 cols, or no name: plain. */
|
|
@@ -836,10 +916,23 @@ function ringString(str, rgbs, from = 0) {
|
|
|
836
916
|
* A composer rule. `ring` (motion only, never under NO_COLOR) = { state, top }: the rule becomes half of the
|
|
837
917
|
* light ring (motion/ring.js); the session name stays bright on top of it.
|
|
838
918
|
*/
|
|
839
|
-
export const Rule = ({ cols, color, name, ring = null, labelColor = null }) => {
|
|
919
|
+
export const Rule = ({ cols, color, name, ring = null, labelColor = null, extra = null }) => {
|
|
840
920
|
const p = ruleParts(cols, name);
|
|
841
921
|
// `labelColor` (the model on the bottom rule): its own colour, not bold, so it reads as context, not a title
|
|
842
922
|
const lab = labelColor ? { color: labelColor } : null;
|
|
923
|
+
if (extra) { // TUI-BAR: the approvals indicator at the LEFT, the session name (if any) at the RIGHT: `─ ✔ nothing waiting ──── name ─`
|
|
924
|
+
const d = cellWidth('─');
|
|
925
|
+
const el = ` ${extra.text} `;
|
|
926
|
+
const nm = name ? ` ${name} ` : '';
|
|
927
|
+
const fill = '─'.repeat(Math.max(1, Math.floor((cols - cellWidth(nm) - cellWidth(el) - 2 * d) / d)));
|
|
928
|
+
const ex = { color: c(extra.role), bold: true };
|
|
929
|
+
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
930
|
+
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
931
|
+
const fl = [...fill].length, ln = [...el].length;
|
|
932
|
+
return h(Text, null, ringString('─', rgbs), h(Text, ex, el), ringString(fill, rgbs, 1 + ln), nm ? h(Text, lab || { color: c('violet'), bold: true }, nm) : null, ringString('─', rgbs, 1 + ln + fl + [...nm].length));
|
|
933
|
+
}
|
|
934
|
+
return h(Text, null, h(Text, { color: c(color) }, '─'), h(Text, ex, el), h(Text, { color: c(color) }, fill), nm ? h(Text, lab || { color: c('violet'), bold: true }, nm) : null, h(Text, { color: c(color) }, '─'));
|
|
935
|
+
}
|
|
843
936
|
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
844
937
|
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
845
938
|
if (!p.label) return h(Text, null, ringString(p.left, rgbs));
|
|
@@ -889,7 +982,13 @@ export function endsInRule({ card = null, asks = [], background = 0, pending = [
|
|
|
889
982
|
return Boolean(card);
|
|
890
983
|
}
|
|
891
984
|
|
|
892
|
-
export function Composer({ buf, cols, mode, busy, placeholder, name, model = null, ring = null, divider = null, now = 0, topRule = true }) {
|
|
985
|
+
export function Composer({ buf, cols, mode, busy, placeholder, name, model = null, ring = null, divider = null, now = 0, topRule = true, approvals = null }) {
|
|
986
|
+
const bar = barLabels(cols, model, approvals);
|
|
987
|
+
// TUI-BAR (owner 2026-10-23): the indicator once, top-left; the session name top-right (it drops before the indicator
|
|
988
|
+
// is cut); the model bottom-right. The top rule is drawn whenever the indicator is, even above a card's closing rule.
|
|
989
|
+
const topExtra = bar.ind;
|
|
990
|
+
const nameRoom = topExtra ? cols - 4 - cellWidth(` ${topExtra.text} `) - 4 : cols - 8;
|
|
991
|
+
const topName = name && topExtra ? (nameRoom >= 6 ? clip(name, nameRoom) : '') : name;
|
|
893
992
|
const empty = buf.text === '';
|
|
894
993
|
const lines = composerLines(buf);
|
|
895
994
|
const body = empty
|
|
@@ -902,10 +1001,10 @@ export function Composer({ buf, cols, mode, busy, placeholder, name, model = nul
|
|
|
902
1001
|
? h(Text, { color: c('input') }, l.text || ' ')
|
|
903
1002
|
: h(Text, { color: c('input') }, l.before, h(Text, { inverse: true }, l.at), l.after)));
|
|
904
1003
|
return h(Box, { flexDirection: 'column', width: cols },
|
|
905
|
-
topRule ? h(Rule, { cols, color: MODE_COLOR[mode] || 'sky', name, ring: ring ? { state: ring, top: true } : null }) : null,
|
|
1004
|
+
(topRule || topExtra) ? h(Rule, { cols, color: MODE_COLOR[mode] || 'sky', name: topName, extra: topExtra, labelColor: topExtra ? c('violet') : null, ring: ring ? { state: ring, top: true } : null }) : null,
|
|
906
1005
|
...body,
|
|
907
|
-
(ring && divider && dividerRuleEl({ cols, label:
|
|
908
|
-
|| h(Rule, { cols, color: 'dim', name:
|
|
1006
|
+
(ring && divider && dividerRuleEl({ cols, label: bar.model, extra: null, divider: divider.d, now, ruleColor: c('dim'), labelColor: modelColor(model), mono: mono(), reduced: cellWidth('─') !== 1, slow: divider.slow }))
|
|
1007
|
+
|| h(Rule, { cols, color: 'dim', name: bar.model, labelColor: modelColor(model), ring: ring ? { state: ring, top: false } : null }));
|
|
909
1008
|
}
|
|
910
1009
|
|
|
911
1010
|
/** The model label's colour: its provider's brand colour, else the label role (never a grey). */
|
|
@@ -948,17 +1047,26 @@ export function WaitingNotice({ items = [], cols }) {
|
|
|
948
1047
|
* a rule, a bold title saying who is asking, label/value rows (what a yes does, in full, and why), then the
|
|
949
1048
|
* "▶ question? keys" footer in the ask colour, and a closing rule. `arming` replaces the footer until keys are live.
|
|
950
1049
|
*/
|
|
951
|
-
|
|
1050
|
+
/** The "▶ question? keys" line: the glyph keeps its own 2 cells and a wrapped question hangs under its text, not under the glyph. */
|
|
1051
|
+
const HANG_ASK = /^▶ /;
|
|
1052
|
+
export function AskFoot({ footer }) {
|
|
1053
|
+
const f = String(footer ?? '');
|
|
1054
|
+
if (!HANG_ASK.test(f)) return h(Text, { color: c('prose'), bold: true, wrap: 'wrap' }, ' ', ...runs(paintLine(f, 'ask')));
|
|
1055
|
+
return h(Box, { paddingLeft: 2 }, h(Box, { width: 2, flexShrink: 0 }, h(Text, { bold: true }, ...runs(paintLine('▶ ', 'ask')))), h(Box, { flexGrow: 1, flexShrink: 1 }, h(Text, { color: c('prose'), bold: true, wrap: 'wrap' }, ...runs(paintLine(f.slice(2), 'ask')))));
|
|
1056
|
+
}
|
|
1057
|
+
const footRows = (footer, cols) => (HANG_ASK.test(footer) ? wrapRows(footer.slice(2), cols - 4) : wrapRows(footer, cols - 2));
|
|
1058
|
+
export function NeedsYouCard({ cols, title, rows = [], block = [], notes = [], footer, arming = null, motionE = null, lead = null }) {
|
|
952
1059
|
const e = motionE;
|
|
953
1060
|
return h(Box, { flexDirection: 'column', width: cols, marginTop: 1 },
|
|
954
1061
|
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }),
|
|
955
1062
|
e == null ? h(Text, { color: c('warn'), bold: true, wrap: 'wrap' }, title) : h(Text, null, ...fxNodes([{ t: title, color: 'warn', bold: true }], { e: e - 0.1, timing: TIMING.title }, 23)),
|
|
1063
|
+
lead ? h(Text, { color: c('warn'), bold: true, wrap: 'wrap' }, ` ${lead}`) : null, // a destructive approval leads with what it deletes
|
|
956
1064
|
// APPROVAL-WHAT: wrap at word boundaries under the label instead of cutting the action or target off
|
|
957
|
-
...rows.flatMap(([k, v]) => wrapPlain(String(v), Math.max(10, cols - 10)).slice(0, 8).map((line, i) => h(Text, { key: `${k}${i}`, color: c('text') }, ' ', h(Text, { color: c('label') }, (i ? '' : k).padEnd(7)), h(Text, { color: c(k === 'risk' ? 'warn' : 'value') }, line)))),
|
|
1065
|
+
...rows.flatMap(([k, v], ri) => wrapPlain(String(v), Math.max(10, cols - 10)).slice(0, 8).map((line, i) => h(Text, { key: `${ri}${k}${i}`, color: c('text') }, ' ', h(Text, { color: c('label') }, (i ? '' : k).padEnd(7)), h(Text, { color: c(k === 'risk' ? 'warn' : 'value') }, line)))),
|
|
958
1066
|
// block: pre-shaped segment rows shown verbatim (a command to run: never word-wrapped, never clipped)
|
|
959
1067
|
...block.map((segs, i) => h(Box, { key: `b${i}`, paddingLeft: 2, width: cols }, h(Text, null, ...segs.map((sg, j) => h(Text, { key: j, color: c(sg.role || 'text'), bold: sg.bold || undefined }, sg.t))))),
|
|
960
1068
|
...notes.map((n, i) => h(Text, { key: `n${i}`, color: c('warn'), bold: true, wrap: 'wrap' }, ` ${n}`)),
|
|
961
|
-
arming || h(
|
|
1069
|
+
arming || h(AskFoot, { footer }),
|
|
962
1070
|
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }));
|
|
963
1071
|
}
|
|
964
1072
|
|
|
@@ -974,29 +1082,43 @@ export function askCardModel(text) {
|
|
|
974
1082
|
export function AskCard({ text, cols }) {
|
|
975
1083
|
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return null; // an empty ask draws nothing: no rules, no blank line
|
|
976
1084
|
const m = askCardModel(text);
|
|
977
|
-
if (m.plain) return h(Box, { width: cols, marginTop: 1 }, h(
|
|
1085
|
+
if (m.plain) return h(Box, { width: cols, marginTop: 1, flexDirection: 'column' }, h(AskFoot, { footer: m.footer }));
|
|
978
1086
|
return h(NeedsYouCard, { cols, title: m.title, rows: m.rows, footer: m.footer });
|
|
979
1087
|
}
|
|
980
1088
|
/** Rows an AskCard takes (sizes the live region). */
|
|
981
1089
|
export const askCardRows = (text, cols) => {
|
|
982
1090
|
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return 0;
|
|
983
1091
|
const m = askCardModel(text);
|
|
984
|
-
if (m.plain) return 1 +
|
|
985
|
-
return 4 + wrapRows(m.title, cols) + m.rows.reduce((n, [, v]) => n + Math.min(8, wrapPlain(String(v), Math.max(10, cols - 10)).length), 0) +
|
|
1092
|
+
if (m.plain) return 1 + footRows(m.footer, cols);
|
|
1093
|
+
return 4 + wrapRows(m.title, cols) + m.rows.reduce((n, [, v]) => n + Math.min(8, wrapPlain(String(v), Math.max(10, cols - 10)).length), 0) + footRows(m.footer, cols);
|
|
986
1094
|
};
|
|
987
1095
|
|
|
988
|
-
|
|
1096
|
+
/** APPROVAL-BURST: one card for 2+ approvals from one agent for one tool (approvalBatch.batchModel). */
|
|
1097
|
+
export function BatchCard({ model, cols, armed, motionE = null }) {
|
|
1098
|
+
const e = motionE;
|
|
1099
|
+
const block = model.lines.map((l) => [{ t: `• ${clip(l, Math.max(10, cols - 8))}`, role: 'text' }]);
|
|
1100
|
+
if (model.more) block.push([{ t: model.more, role: 'dim' }]);
|
|
1101
|
+
const footer = askText(`Approve all ${model.n}?`, { keys: model.keys });
|
|
1102
|
+
return h(NeedsYouCard, { cols, title: model.title, rows: [], block, notes: model.notes, footer, arming: armed ? null : (e == null ? h(Text, { color: c('dim') }, ' arming…') : null), motionE: e });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export function ApprovalCard({ card, org, cols, armed, draftHint = null, motionE = null, confirm = false, position = null, signedOut = false }) {
|
|
989
1106
|
const when = card.askedAt ? String(card.askedAt).replace('T', ' ').slice(0, 16) + ' UTC' : null;
|
|
990
1107
|
const rows = card.undescribable ? [['what', card.what], ['from', card.askedBy || card.origin], ['asked', when], ['org', org]].filter(r => r[1]) : [
|
|
991
|
-
['what', card.what && card.what !== card.tool ? card.what : null], ['tool', card.tool], ['target', card.target], ['scope', card.scope], ['risk', card.risk], ['from', card.foreign ? card.origin : null], ['org', org]
|
|
1108
|
+
['what', card.what && card.what !== card.tool ? card.what : null], ['tool', card.tool], ['target', card.target], ['scope', card.scope], ['risk', card.risk], ['from', card.foreign ? card.origin : null], ['org', card.plan && (card.plan.orgName || card.plan.orgId) ? orgLabel(card.plan.orgName, card.plan.orgId) : org]
|
|
992
1109
|
].filter(r => r[1]);
|
|
1110
|
+
if (card.plan) { // the plan itself, so the approver sees what they are approving
|
|
1111
|
+
if (card.plan.lines.length) card.plan.lines.forEach((l, i) => rows.push([i ? '' : 'plan', l]));
|
|
1112
|
+
else rows.push(['plan', card.plan.loaded ? 'this objective has no steps recorded' : 'could not load the plan steps, deny or check `quox objective get` first']);
|
|
1113
|
+
}
|
|
993
1114
|
if (card.privileged) rows.push(['grants', `PRIVILEGED: ${[card.tool, card.target].filter(Boolean).join(' ')}`]);
|
|
994
1115
|
// APPROVAL-WWWH: what exactly (verbatim), where, who, why, when, read-only or write. The command is a block: never word-wrapped.
|
|
995
1116
|
const f = card.facts || null; const block = [];
|
|
996
1117
|
if (f && !card.undescribable) {
|
|
997
1118
|
if (f.where && /^\d+ (hosts|targets):/.test(f.where)) { for (const k of ['target', 'what']) { const i = rows.findIndex(r => r[0] === k && (k === 'target' || String(r[1]).includes(f.hostList[1]))); if (i >= 0) rows.splice(i, 1); } }
|
|
1119
|
+
if (f.background) { const i = rows.findIndex(r => r[0] === 'what'); const note = BACKGROUND_NOTE[0].toUpperCase() + BACKGROUND_NOTE.slice(1); if (i >= 0) rows[i] = ['what', `${rows[i][1]}. ${note}`]; else rows.unshift(['what', note]); }
|
|
998
1120
|
if (f.command) { block.push([{ t: f.command.kind === 'input' ? 'input:' : 'command:', role: 'label' }]); block.push(...codeRows(f.command.shown.split('\n'), cols - 4)); }
|
|
999
|
-
for (const r of [['where', f.where], ['who', f.who], ['why', f.why], ['asked', [f.asked && `"${f.asked}"
|
|
1121
|
+
for (const r of [['where', f.where], ['who', f.who], ['why', f.why], ['asked', [f.asked && (f.askedRich ? f.asked : `"${f.asked}"`), f.when].filter(Boolean).join(' · ') || null], ['class', f.access]]) if (r[1]) rows.push(r);
|
|
1000
1122
|
}
|
|
1001
1123
|
const e = motionE;
|
|
1002
1124
|
const title = `⚠ Approval needed${position && position.n > 1 ? ` (${position.i} of ${position.n})` : ''}: execution is held`;
|
|
@@ -1004,12 +1126,15 @@ export function ApprovalCard({ card, org, cols, armed, draftHint = null, motionE
|
|
|
1004
1126
|
const arming = e == null || mono()
|
|
1005
1127
|
? h(Text, { color: c('dim') }, e == null ? ' arming…' : ` arming… ${'▰'.repeat(filled)}${'▱'.repeat(ARM_BAR - filled)}`)
|
|
1006
1128
|
: h(Text, null, h(Text, { color: c('dim') }, ' arming… '), h(Text, { color: c('warn') }, '▰'.repeat(filled)), h(Text, { color: c('faint') }, '▱'.repeat(ARM_BAR - filled)));
|
|
1007
|
-
const footer =
|
|
1129
|
+
const footer = signedOut ? askText('Read-only while signed out. Sign in to answer?', { keys: 'y signs in' })
|
|
1130
|
+
: card.privileged && confirm ? `▶ Grant ${[card.tool, card.target].filter(Boolean).join(' ')}? press y again to confirm, any other key cancels`
|
|
1008
1131
|
: card.undescribable && confirm ? '▶ Quox could not describe this action. Approve it anyway? press y again to confirm, any other key cancels'
|
|
1132
|
+
: draftHint === 'reason' ? askText('Why deny it?', { keys: 'type a reason · Enter denies · Esc goes back to y/n' })
|
|
1009
1133
|
: draftHint === 'focus' ? askText('Approve it?', { keys: 'y approve · n deny · card focused (Esc returns to your draft)' }) // RESTORE-TUI: every y/n goes through askText (same text)
|
|
1010
1134
|
: draftHint === 'draft' ? askText('Approve it?', { keys: 'y/n answer once your draft is empty · Tab to answer the approval card (your draft is kept)' })
|
|
1011
|
-
: '▶
|
|
1012
|
-
|
|
1135
|
+
: card.destructive ? '▶ Delete it? y then type the name · n deny · c chat · Tab later · /approvals all'
|
|
1136
|
+
: '▶ Approve it? y approve · n deny · c chat · Tab later · /approvals all';
|
|
1137
|
+
return h(NeedsYouCard, { cols, title, rows, block, lead: card.destructive ? `DELETES ${card.destructive.label}` : null, notes: [...(f && f.askedNone ? ['nothing in your request asks for this'] : []), ...(card.undescribable && !confirm ? ['Quox could not describe this action'] : []), ...(position && position.others > 0 ? [`other sessions: ${position.others} waiting · /approvals`] : [])], footer, arming: armed ? null : arming, motionE: e });
|
|
1013
1138
|
}
|
|
1014
1139
|
|
|
1015
1140
|
/** Columns the status line's Q takes: 2 for the Braille glyph, 1 for the Ⓠ fallback (2 where Ambiguous is wide). */
|
|
@@ -1046,8 +1171,10 @@ export function statusRows({ cols, slot, agent, groups = [], parts, hint, qw = s
|
|
|
1046
1171
|
// every context item says what it is (owner: "profile moo .. ask?? what?"); /help status explains each in a line
|
|
1047
1172
|
const word = { profile: 'profile ', org: 'org ', mode: 'mode ' };
|
|
1048
1173
|
const val = (k) => k === 'mode' ? (MODE_WORDS[p.mode] || null) : k === 'auto' ? '↻ auto-restart' : String(p[k]);
|
|
1049
|
-
for (const k of ['profile', 'org', 'mode', 'rules', 'auto'].filter(k => p[k])) tokens.push({ pre: ' · ',
|
|
1050
|
-
segs:
|
|
1174
|
+
for (const k of ['profile', 'org', 'server', 'remote', 'mode', 'rules', 'auto'].filter(k => p[k])) tokens.push({ pre: ' · ',
|
|
1175
|
+
segs: k === 'server' ? [['⬡ ', 'icon'], ['server ', 'label'], [p.server.name, 'value'], ...(p.server.role ? [[` (${p.server.role})`, 'label']] : [])]
|
|
1176
|
+
: k === 'remote' ? [['→ ', 'icon'], [String(p.remote), 'value']]
|
|
1177
|
+
: word[k] && val(k) ? [[word[k], 'label'], [val(k), k === 'mode' && p.mode === 'plan' ? 'violet' : 'value']] : [[k === 'mode' ? String(p[k]) : val(k), 'label']] });
|
|
1051
1178
|
const tlen = (tk) => tk.segs.reduce((n, sg) => n + len(sg[0]), 0);
|
|
1052
1179
|
// split a token too wide for a row into row-sized pieces (whole characters by cell width, never an ellipsis)
|
|
1053
1180
|
const fit = (tk) => {
|
|
@@ -1097,20 +1224,21 @@ export function statusRows({ cols, slot, agent, groups = [], parts, hint, qw = s
|
|
|
1097
1224
|
}
|
|
1098
1225
|
|
|
1099
1226
|
/** QUOXON-FW17: custom rows, one truncated line each, just above the status line. */
|
|
1100
|
-
export function RowsBar({ rows, cols }) {
|
|
1227
|
+
export function RowsBar({ rows, cols, limit }) {
|
|
1101
1228
|
if (!rows?.length) return null;
|
|
1102
|
-
|
|
1103
|
-
|
|
1229
|
+
// TUI-QPE: layoutRows fits cells rows by keep (whole items drop, never mid-word), lines up a grid's columns and
|
|
1230
|
+
// leaves out rows below their min_cols; plain rows take the classic rowParts + clipParts path unchanged.
|
|
1231
|
+
// /qpe N keeps the first N lines of a grid; it applies to drawn lines (a row can bring several), so expand first.
|
|
1232
|
+
const laid = layoutRows(limit ? limitGrid(expandLines(rows), limit.grid, limit.n) : rows, cols);
|
|
1233
|
+
if (!laid.length) return null;
|
|
1104
1234
|
return h(Box, { flexDirection: 'column', width: cols },
|
|
1105
|
-
...
|
|
1106
|
-
const parts = clipParts(rowParts(r, { labelW }), Math.max(1, cols - 2));
|
|
1235
|
+
...laid.map(({ st: r, parts }) =>
|
|
1107
1236
|
// ROWS: label, value, tone and trend as their own runs; every role is a palette colour (no grey, owner rule).
|
|
1108
|
-
|
|
1109
|
-
}));
|
|
1237
|
+
h(Text, { key: r.row.id, wrap: 'truncate-end' }, ' ', ...parts.map(([t, role, bold], i) => h(Text, { key: i, color: c(themeRole(role)), bold: Boolean(bold) }, t)))));
|
|
1110
1238
|
}
|
|
1111
1239
|
|
|
1112
1240
|
export function StatusLine({ cols, slot, agent, groups = [], parts, right }) {
|
|
1113
|
-
const p = Array.isArray(parts) ? { profile: parts[0], org: parts[1], model: parts[2], mode: parts[3], rules: parts[4], auto: parts[5] } : parts;
|
|
1241
|
+
const p = Array.isArray(parts) ? { profile: parts[0], org: parts[1], model: parts[2], mode: parts[3], rules: parts[4], auto: parts[5], server: parts[6], remote: parts[7] } : parts;
|
|
1114
1242
|
const qw = statusGlyphWidth();
|
|
1115
1243
|
const rows = statusRows({ cols, slot, agent, groups, parts: p, hint: right, qw });
|
|
1116
1244
|
// The still Q (qglyph.js): two Braille cells, or Ⓠ under NO_COLOR / no Braille. statusRows reserved its width.
|
|
@@ -1131,7 +1259,24 @@ function hintNodes(hint) {
|
|
|
1131
1259
|
}
|
|
1132
1260
|
|
|
1133
1261
|
/** Live, actionable counts under the status line. Never a transcript message. */
|
|
1134
|
-
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false }) {
|
|
1262
|
+
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false, inbox = '', spinner = '', away = false, awayCount = 0, dnd = false, dndCount = 0, awaySince = 0, dndUntil = 0, awayUnknown = false }) {
|
|
1263
|
+
const hm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
|
|
1264
|
+
if (away && dnd && !signedOut) { // DND: one persistent quiet line, shown even at 0 waiting so the mode is never invisible
|
|
1265
|
+
const head = dndUntil ? `🌙 do not disturb until ${hm(dndUntil)}` : '🌙 do not disturb';
|
|
1266
|
+
return h(Text, { color: c('ask'), bold: true }, ' ' + clip(`${head}${dndCount ? ` · ${dndCount} waiting` : ''} · /morningreport`, cols - 3));
|
|
1267
|
+
}
|
|
1268
|
+
if (away && !signedOut) { // AFK-QUIET-CARDS: away shows one quiet line, never a prompt
|
|
1269
|
+
const n = awayCount || approvals || 0;
|
|
1270
|
+
const head = awaySince ? `🌙 away since ${hm(awaySince)} · /back` : awayUnknown ? '🌙 away (since before this window opened) · /back' : '🌙 away · /back';
|
|
1271
|
+
return h(Text, { color: c('ask'), bold: true }, ' ' + clip(n ? `⏸ ${n} approval${n === 1 ? '' : 's'} waiting · /back to review` : head, cols - 3)); // >0 keeps the existing wording (asserted elsewhere)
|
|
1272
|
+
}
|
|
1273
|
+
const base = NotifyBarBase({ approvals, error, cols, signedOut, alerts, hideCount });
|
|
1274
|
+
const line = inbox && !signedOut ? h(Text, { key: 'inbox', color: c('ask'), bold: true }, ' ' + clip(inbox, cols - 3)) : null; // HITL-INBOX-BAR: 📨 N new · newest title
|
|
1275
|
+
const spin = spinner ? h(Text, { key: 'spin', color: c('ask') }, ' ' + clip(spinner, cols - 3)) : null; // INSTANT-FEEDBACK: ⠋ <verb>…
|
|
1276
|
+
const parts = [base, line, spin].filter(Boolean);
|
|
1277
|
+
return parts.length > 1 ? h(Box, { flexDirection: 'column' }, ...parts) : (parts[0] || base);
|
|
1278
|
+
}
|
|
1279
|
+
function NotifyBarBase({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false }) {
|
|
1135
1280
|
if (signedOut) return h(Text, { color: c('warn') }, ' ' + clip('not signed in: approvals unavailable until you sign in', cols - 2));
|
|
1136
1281
|
const count = approvals && !hideCount ? h(Text, { key: 'n', color: c('ask'), bold: true }, ' ' + clip(`▶ ${approvals} approval${approvals === 1 ? '' : 's'} waiting for you${error ? ' (last known)' : ''} · /approvals`, cols - 3)) : null;
|
|
1137
1282
|
// Background workflows that keep failing: one deduped line each (max 2), never roster rows.
|
|
@@ -1155,13 +1300,14 @@ export function Roster({ view, peek = [], confirm = null, hint = null, cols, fx
|
|
|
1155
1300
|
// glyph keeps the state colour; the rest of the line steps down in tone with position and (for finished rows) age
|
|
1156
1301
|
...view.lines.map((l, i) => {
|
|
1157
1302
|
const e = fx && l.key != null ? fx(l.key) : null; // a row that just appeared decodes in (new rows land on top)
|
|
1158
|
-
|
|
1303
|
+
const row = h(Text, { key: 'r' + i, bold: l.selected, wrap: 'truncate-end' },
|
|
1159
1304
|
h(Text, { color: c(l.selected ? 'text' : (l.glyphRole || ROSTER_TONE[l.state])) }, ' ' + l.marker + l.glyph),
|
|
1160
1305
|
e == null ? h(Text, { color: c(l.selected ? 'text' : l.tone) }, l.rest) : h(Text, null, ...fxNodes([{ t: l.rest, color: l.selected ? 'text' : l.tone }], { e, timing: TIMING.roster }, 29 + i)),
|
|
1161
1306
|
l.pulse ? h(Text, { color: c(l.pulseRole === 'dim' && l.selected ? 'text' : l.pulseRole) }, l.pulse) : null);
|
|
1307
|
+
return l.extra ? h(Box, { key: 'rx' + i, flexDirection: 'column' }, row, ...l.extra.map((t, j) => h(Text, { key: j, color: c('error') }, ' ' + t))) : row; // the stop reason, whole and wrapped
|
|
1162
1308
|
}),
|
|
1163
1309
|
view.more ? h(Text, { key: 'more', color: c('dim') }, ` +${view.more} more · /roster`) : null,
|
|
1164
|
-
...peek.
|
|
1310
|
+
...peek.flatMap((p, i) => wrapText(p, cols - 5).map((t, j) => h(Text, { key: `p${i}.${j}`, color: c('dim') }, (j ? ' ' : ' ') + t))), // QLOOP-LIVE: a peek line wraps, it is never cut
|
|
1165
1311
|
confirm ? h(Text, { key: 'cf', color: c('warn') }, ' ' + clip(confirm, cols - 2)) : null,
|
|
1166
1312
|
hint ? h(Text, { key: 'h', color: c('dim') }, ' ' + clip(hint, cols - 2)) : null);
|
|
1167
1313
|
}
|