quoxcli 0.3.1 → 0.4.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/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +20 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -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/index.js +12 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +173 -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/v1.js +34 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +46 -7
- package/lib/commands/approvals.js +5 -0
- package/lib/commands/ask.js +3 -1
- package/lib/commands/blog.js +110 -0
- package/lib/commands/chat.js +127 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +3 -32
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/loop.js +291 -0
- package/lib/commands/objective.js +48 -1
- package/lib/commands/org.js +21 -4
- package/lib/commands/provider.js +58 -0
- 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 +18 -1
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +113 -0
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +20 -4
- package/lib/commands/vm.js +2 -2
- 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/client.js +10 -2
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +1 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +1 -3
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +36 -7
- package/lib/core/output.js +7 -7
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- 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 +23 -0
- package/lib/quoxon/api.js +192 -6
- package/lib/quoxon/app.js +655 -89
- package/lib/quoxon/approvalFacts.js +8 -6
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +63 -4
- package/lib/quoxon/commandShort.js +15 -3
- package/lib/quoxon/commands.js +75 -10
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/events.js +24 -3
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +12 -3
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/objectiveOffer.js +82 -0
- 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/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +1 -1
- package/lib/quoxon/orgSwitch.js +27 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +6 -3
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +130 -33
- package/lib/quoxon/roster.js +26 -12
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +76 -1
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/palette.js +1 -1
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +25 -1
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/suggestCommand.js +33 -2
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/whatsnew.jsonl +45 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/package.json +2 -1
- 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/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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -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/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/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,7 +24,8 @@ 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';
|
|
@@ -41,6 +43,8 @@ import { CLIPS as DRAGON_CLIPS, clipCells } from './qdragonScenes.js';
|
|
|
41
43
|
import { dragonLayout } from './qdragon.js';
|
|
42
44
|
import { ObjectView } from './objects/view.js';
|
|
43
45
|
import { aoclLineSegs, aoclTableLines } from './aocl.js';
|
|
46
|
+
import { cardLines, wrapText, loopPinned } from './loopCard.js';
|
|
47
|
+
import { orgLabel } from './orgLabel.js';
|
|
44
48
|
export { COLORS, MODE_COLOR, MIN_COLS, clampCols, clip, reducedMotion };
|
|
45
49
|
|
|
46
50
|
// ---- working indicator (the one animated line) -------------------------------------------------
|
|
@@ -254,15 +258,24 @@ export function ToolRow({ b, cols, fx = null, pulse = null, ignite = 0 }) {
|
|
|
254
258
|
const running = b.status === 'running';
|
|
255
259
|
// the facts after the result: duration, then witnessing state
|
|
256
260
|
let after = [];
|
|
261
|
+
let envExtra = null; // an id never shares a clipped line: when it does not fit it wraps onto its own line(s)
|
|
257
262
|
if (!running && b.receipt) {
|
|
258
263
|
const v = b.receipt.verified;
|
|
259
264
|
after = [dur, v === true ? 'witnessed' : v === false ? 'unverified' : 'verification pending', `receipt ward:${b.receipt.ward_seq}`].filter(Boolean);
|
|
265
|
+
if (b.envelopeId) envExtra = `envelope ${b.envelopeId}`;
|
|
260
266
|
} else if (!running) {
|
|
261
|
-
after = [dur, b.envelopeId ? `envelope ${
|
|
267
|
+
after = [dur, b.envelopeId ? `envelope ${b.envelopeId}` : '', b.envelopeId ? 'witnessing pending' : ''].filter(Boolean);
|
|
262
268
|
}
|
|
263
269
|
const result = running ? '' : deleg && deleg.result ? deleg.result : (b.resultSummary ? `${b.status === 'unknown' ? 'unknown: ' : ''}${b.resultSummary}` : b.status);
|
|
264
270
|
const bodyStr = clipWords(body0, running || !result ? room : Math.max(6, Math.min(gw(body0), Math.floor(room / 2))));
|
|
265
271
|
const tailRoom = room - gw(bodyStr) - 3;
|
|
272
|
+
if (!running && !b.receipt && b.envelopeId && gw(after.join(' · ')) + 3 > Math.max(0, tailRoom)) {
|
|
273
|
+
envExtra = `envelope ${b.envelopeId} · witnessing pending`;
|
|
274
|
+
after = after.filter(f => !/^envelope /.test(f) && f !== 'witnessing pending');
|
|
275
|
+
}
|
|
276
|
+
const extra = envExtra ? wrapHang(envExtra, Math.max(12, cols - 6), 2).map((l, i) => h(Text, { key: `e${i}`, color: c('muted') }, ` ${l}`)) : [];
|
|
277
|
+
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
|
|
278
|
+
const withExtra = (node) => (extra.length ? h(Box, { flexDirection: 'column' }, node, ...extra) : node);
|
|
266
279
|
let tail = '';
|
|
267
280
|
if (!running && tailRoom > 0) {
|
|
268
281
|
const facts = after.join(' · ');
|
|
@@ -285,9 +298,9 @@ export function ToolRow({ b, cols, fx = null, pulse = null, ignite = 0 }) {
|
|
|
285
298
|
if (fx?.settle != null && !running) {
|
|
286
299
|
const flash = fx.settle < 0.12 && !mono();
|
|
287
300
|
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));
|
|
301
|
+
return withExtra(h(Box, null, ...fxNodes(head, null), ...fxNodes(tailSegs, { e: fx.settle, timing: TIMING.settle }, 7)));
|
|
289
302
|
}
|
|
290
|
-
return h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3));
|
|
303
|
+
return withExtra(h(Box, null, ...fxNodes([...head, ...tailSegs], fx, 3)));
|
|
291
304
|
}
|
|
292
305
|
|
|
293
306
|
/** 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 +394,7 @@ export function HelpView({ model, cols }) {
|
|
|
381
394
|
const keyW = Math.min(24, Math.max(...model.filter(m => m.key).map(m => [...m.key].length), 8) + 2);
|
|
382
395
|
return h(Box, { flexDirection: 'column', marginTop: 1, width: cols }, ...model.map((m, i) => {
|
|
383
396
|
if (m.kind === 'gap') return h(Text, { key: i }, ' ');
|
|
397
|
+
if (m.kind === 'subhead') return h(Heading, { key: i, text: m.text });
|
|
384
398
|
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
399
|
if (m.kind === 'row' || (m.kind === 'dim' && m.key)) {
|
|
386
400
|
const dim = m.kind === 'dim';
|
|
@@ -511,6 +525,7 @@ const GROUP = { tool: 'tools', trace: 'tools', note: 'notice', error: 'notice',
|
|
|
511
525
|
const FOOTER = new Set(['aocl', 'summary']);
|
|
512
526
|
export function historyGap(prev, item, rows = null) {
|
|
513
527
|
if (!item || SELF_SPACED.has(item.kind)) return 0;
|
|
528
|
+
if (item.kind === 'loopcard' && loopPinned(item.card)) return 0; // drawn above the composer, not here
|
|
514
529
|
if (item.kind === 'user') return 1;
|
|
515
530
|
if (!prev) return item.kind === 'text' ? 1 : 0;
|
|
516
531
|
if (rows != null && rows < 20) return 0;
|
|
@@ -546,6 +561,7 @@ export function HistoryItem({ item, cols, live = false, fx = null, pulse = null,
|
|
|
546
561
|
case 'error': return h(ErrorView, { item, cols, fx });
|
|
547
562
|
case 'note': return h(NoteView, { item, cols, fx });
|
|
548
563
|
case 'slotmsg': return h(SlotMsgView, { item, cols });
|
|
564
|
+
case 'loopcard': return loopPinned(item.card) ? null : h(LoopCardView, { item, cols });
|
|
549
565
|
case 'tip': return h(TipView, { item, cols });
|
|
550
566
|
case 'whatsnew': return h(WhatsNewView, { item, cols });
|
|
551
567
|
case 'bcard': return h(BroadcastCardView, { item, cols });
|
|
@@ -572,7 +588,10 @@ export function noteLines(text, width) {
|
|
|
572
588
|
}
|
|
573
589
|
/** 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
590
|
const longUrl = (l, width) => { const t = stripMarks(l); return /^https?:\/\/\S+$/.test(t) && graphemes(t).length > width; };
|
|
575
|
-
|
|
591
|
+
/** 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). */
|
|
592
|
+
const GLYPH_LEAD = /^(?:▶|✔|✖|⚠|∞) /u;
|
|
593
|
+
const hangs = (l, width) => GLYPH_LEAD.test(l) && !longUrl(l, width) && wrapRows(stripMarks(l), width) > 1;
|
|
594
|
+
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
595
|
|
|
577
596
|
/**
|
|
578
597
|
* Z2H-7B3: a failure message can be several lines (what ran, what did not, the advice). Every line shows,
|
|
@@ -618,6 +637,8 @@ function NoteView({ item, cols, fx = null }) {
|
|
|
618
637
|
// While it reveals, a line that fits decodes in; a line that wraps keeps plain wrapping (never mis-measured).
|
|
619
638
|
return h(Box, { paddingLeft: 2, flexDirection: 'column', width: cols }, ...noteLines(text, cols - 2).map((l, i) => longUrl(l, cols - 2)
|
|
620
639
|
? h(Box, { key: i, width: graphemes(stripMarks(l)).length, flexShrink: 0 }, h(Text, { color: c('link'), wrap: 'truncate-end' }, stripMarks(l)))
|
|
640
|
+
: hangs(l, cols - 2)
|
|
641
|
+
? 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
642
|
: (fx && wrapRows(stripMarks(l), cols - 2) === 1
|
|
622
643
|
? 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
644
|
: h(WText, { key: i, color, bold: item.tone === 'ask' }, ...runs(paintLine(l, item.tone))))));
|
|
@@ -628,6 +649,19 @@ function NoteView({ item, cols, fx = null }) {
|
|
|
628
649
|
* outgoing "→ quox2 04:56 hello ✔" (sky: you). A file shows its name, three lines and a /slot log pointer;
|
|
629
650
|
* /slot log prints the whole file (`full`).
|
|
630
651
|
*/
|
|
652
|
+
/** QLOOP-LIVE: the live loop card. The card object is updated in place by loopCards.js; this reads it on every draw. */
|
|
653
|
+
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 });
|
|
654
|
+
const cardMemo = new WeakMap(); // card -> { key, lines }: re-laid-out only when what it draws changed
|
|
655
|
+
export function LoopCardView({ item, cols, pinned = false }) {
|
|
656
|
+
const card = item.card, now = Date.now(), st = card.state;
|
|
657
|
+
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('|');
|
|
658
|
+
let hit = cardMemo.get(card);
|
|
659
|
+
if (!hit || hit.st !== st || hit.key !== key) { hit = { st, key, lines: loopCardLines(card, cols, now, pinned) }; cardMemo.set(card, hit); }
|
|
660
|
+
const lines = hit.lines;
|
|
661
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 2, width: cols },
|
|
662
|
+
...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)))));
|
|
663
|
+
}
|
|
664
|
+
|
|
631
665
|
function SlotMsgView({ item, cols }) {
|
|
632
666
|
const l = item.line || {};
|
|
633
667
|
if (l.roomLine) { // QUOXON-ROOMS: `#group-chat QUOXON: text`, a stable colour per sender
|
|
@@ -770,6 +804,8 @@ export function liveRows(blocks, cols) {
|
|
|
770
804
|
case 'summary': return n + noteLines(b.text, cols - 4).length;
|
|
771
805
|
case 'error': return n + errorRows(b.text, cols);
|
|
772
806
|
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
|
|
807
|
+
case 'tool': return n + 1 + (b.status === 'running' ? 0 : toolCardLines(b, cols).length);
|
|
808
|
+
case 'loopcard': return n + (loopPinned(b.card) ? 0 : 1 + loopCardLines(b.card, cols).length);
|
|
773
809
|
default: return n + 1;
|
|
774
810
|
}
|
|
775
811
|
}, 0);
|
|
@@ -790,10 +826,11 @@ export const Grad = ({ text }) => {
|
|
|
790
826
|
* never wraps: full, then a shortened provider (`anth claude-sonnet-5-5`), then the model alone, then the
|
|
791
827
|
* model clipped. Leaves at least four rule cells on the left. Below 30 cols, or no model: ''.
|
|
792
828
|
*/
|
|
793
|
-
export function modelRuleLabel(cols, model) {
|
|
829
|
+
export function modelRuleLabel(cols, model, reserve = 0) {
|
|
794
830
|
const full = String(model || '').trim();
|
|
795
831
|
if (!full || cols < 30) return '';
|
|
796
|
-
const room = cols - 7; // ' label ' + one trailing dash + four leading dashes
|
|
832
|
+
const room = cols - 7 - reserve; // ' label ' + one trailing dash + four leading dashes (+ the approvals indicator)
|
|
833
|
+
if (reserve && room < 6) return '';
|
|
797
834
|
const fits = (t) => cellWidth(t) <= room; // cells under the ambiguous-width policy (cells.js)
|
|
798
835
|
if (fits(full)) return full;
|
|
799
836
|
const segs = full.split(' · ');
|
|
@@ -803,7 +840,26 @@ export function modelRuleLabel(cols, model) {
|
|
|
803
840
|
const provider = segs.length > 1 ? segs[0] : words.length > 1 ? words[0] : '';
|
|
804
841
|
if (provider) { const short = `${provider.slice(0, 4)} ${name}`; if (fits(short)) return short; }
|
|
805
842
|
if (fits(name)) return name;
|
|
806
|
-
return clip(name, room);
|
|
843
|
+
return reserve ? '' : clip(name, room); // with the approvals indicator the model is dropped before the indicator is cut
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Pure: the permanent approvals indicator on the bottom rule. `a` = { count, error, signedOut }.
|
|
848
|
+
* Never a false 0: signed out, an unreachable inbox, or a count not read yet is `approvals ?` (warn).
|
|
849
|
+
*/
|
|
850
|
+
export function approvalsIndicator(a) {
|
|
851
|
+
if (!a) return null;
|
|
852
|
+
const n = a.count;
|
|
853
|
+
if (a.signedOut || a.error || typeof n !== 'number') return { text: 'approvals ?', role: 'warn' };
|
|
854
|
+
if (n === 0) return { text: '✔ nothing waiting', role: 'ok' };
|
|
855
|
+
return { text: `⏸ ${n} approval${n === 1 ? '' : 's'} waiting`, role: 'warn' };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Pure: the labels for this width. The indicator sits top-left, the model bottom-right: independent of each other. */
|
|
859
|
+
export function barLabels(cols, model, a) {
|
|
860
|
+
const ind = cols >= 30 ? approvalsIndicator(a) : null;
|
|
861
|
+
if (ind && cols - cellWidth(` ${ind.text} `) - 5 < 4) return { model: modelRuleLabel(cols, model), ind: null }; // no room for the whole indicator
|
|
862
|
+
return { model: modelRuleLabel(cols, model), ind };
|
|
807
863
|
}
|
|
808
864
|
|
|
809
865
|
/** Pure: the rule split around a right-aligned session name: `───── name ─`. Below 30 cols, or no name: plain. */
|
|
@@ -836,10 +892,23 @@ function ringString(str, rgbs, from = 0) {
|
|
|
836
892
|
* A composer rule. `ring` (motion only, never under NO_COLOR) = { state, top }: the rule becomes half of the
|
|
837
893
|
* light ring (motion/ring.js); the session name stays bright on top of it.
|
|
838
894
|
*/
|
|
839
|
-
export const Rule = ({ cols, color, name, ring = null, labelColor = null }) => {
|
|
895
|
+
export const Rule = ({ cols, color, name, ring = null, labelColor = null, extra = null }) => {
|
|
840
896
|
const p = ruleParts(cols, name);
|
|
841
897
|
// `labelColor` (the model on the bottom rule): its own colour, not bold, so it reads as context, not a title
|
|
842
898
|
const lab = labelColor ? { color: labelColor } : null;
|
|
899
|
+
if (extra) { // TUI-BAR: the approvals indicator at the LEFT, the session name (if any) at the RIGHT: `─ ✔ nothing waiting ──── name ─`
|
|
900
|
+
const d = cellWidth('─');
|
|
901
|
+
const el = ` ${extra.text} `;
|
|
902
|
+
const nm = name ? ` ${name} ` : '';
|
|
903
|
+
const fill = '─'.repeat(Math.max(1, Math.floor((cols - cellWidth(nm) - cellWidth(el) - 2 * d) / d)));
|
|
904
|
+
const ex = { color: c(extra.role), bold: true };
|
|
905
|
+
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
906
|
+
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
907
|
+
const fl = [...fill].length, ln = [...el].length;
|
|
908
|
+
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));
|
|
909
|
+
}
|
|
910
|
+
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) }, '─'));
|
|
911
|
+
}
|
|
843
912
|
if (ring && ring.state && ring.state.k > 0 && !mono()) {
|
|
844
913
|
const rgbs = ruleColors(cols, ring.top, ring.state, COLORS[color] || color);
|
|
845
914
|
if (!p.label) return h(Text, null, ringString(p.left, rgbs));
|
|
@@ -889,7 +958,13 @@ export function endsInRule({ card = null, asks = [], background = 0, pending = [
|
|
|
889
958
|
return Boolean(card);
|
|
890
959
|
}
|
|
891
960
|
|
|
892
|
-
export function Composer({ buf, cols, mode, busy, placeholder, name, model = null, ring = null, divider = null, now = 0, topRule = true }) {
|
|
961
|
+
export function Composer({ buf, cols, mode, busy, placeholder, name, model = null, ring = null, divider = null, now = 0, topRule = true, approvals = null }) {
|
|
962
|
+
const bar = barLabels(cols, model, approvals);
|
|
963
|
+
// TUI-BAR (owner 2026-10-23): the indicator once, top-left; the session name top-right (it drops before the indicator
|
|
964
|
+
// is cut); the model bottom-right. The top rule is drawn whenever the indicator is, even above a card's closing rule.
|
|
965
|
+
const topExtra = bar.ind;
|
|
966
|
+
const nameRoom = topExtra ? cols - 4 - cellWidth(` ${topExtra.text} `) - 4 : cols - 8;
|
|
967
|
+
const topName = name && topExtra ? (nameRoom >= 6 ? clip(name, nameRoom) : '') : name;
|
|
893
968
|
const empty = buf.text === '';
|
|
894
969
|
const lines = composerLines(buf);
|
|
895
970
|
const body = empty
|
|
@@ -902,10 +977,10 @@ export function Composer({ buf, cols, mode, busy, placeholder, name, model = nul
|
|
|
902
977
|
? h(Text, { color: c('input') }, l.text || ' ')
|
|
903
978
|
: h(Text, { color: c('input') }, l.before, h(Text, { inverse: true }, l.at), l.after)));
|
|
904
979
|
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,
|
|
980
|
+
(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
981
|
...body,
|
|
907
|
-
(ring && divider && dividerRuleEl({ cols, label:
|
|
908
|
-
|| h(Rule, { cols, color: 'dim', name:
|
|
982
|
+
(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 }))
|
|
983
|
+
|| h(Rule, { cols, color: 'dim', name: bar.model, labelColor: modelColor(model), ring: ring ? { state: ring, top: false } : null }));
|
|
909
984
|
}
|
|
910
985
|
|
|
911
986
|
/** The model label's colour: its provider's brand colour, else the label role (never a grey). */
|
|
@@ -948,17 +1023,25 @@ export function WaitingNotice({ items = [], cols }) {
|
|
|
948
1023
|
* a rule, a bold title saying who is asking, label/value rows (what a yes does, in full, and why), then the
|
|
949
1024
|
* "▶ question? keys" footer in the ask colour, and a closing rule. `arming` replaces the footer until keys are live.
|
|
950
1025
|
*/
|
|
1026
|
+
/** The "▶ question? keys" line: the glyph keeps its own 2 cells and a wrapped question hangs under its text, not under the glyph. */
|
|
1027
|
+
const HANG_ASK = /^▶ /;
|
|
1028
|
+
export function AskFoot({ footer }) {
|
|
1029
|
+
const f = String(footer ?? '');
|
|
1030
|
+
if (!HANG_ASK.test(f)) return h(Text, { color: c('prose'), bold: true, wrap: 'wrap' }, ' ', ...runs(paintLine(f, 'ask')));
|
|
1031
|
+
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')))));
|
|
1032
|
+
}
|
|
1033
|
+
const footRows = (footer, cols) => (HANG_ASK.test(footer) ? wrapRows(footer.slice(2), cols - 4) : wrapRows(footer, cols - 2));
|
|
951
1034
|
export function NeedsYouCard({ cols, title, rows = [], block = [], notes = [], footer, arming = null, motionE = null }) {
|
|
952
1035
|
const e = motionE;
|
|
953
1036
|
return h(Box, { flexDirection: 'column', width: cols, marginTop: 1 },
|
|
954
1037
|
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }),
|
|
955
1038
|
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)),
|
|
956
1039
|
// 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)))),
|
|
1040
|
+
...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
1041
|
// block: pre-shaped segment rows shown verbatim (a command to run: never word-wrapped, never clipped)
|
|
959
1042
|
...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
1043
|
...notes.map((n, i) => h(Text, { key: `n${i}`, color: c('warn'), bold: true, wrap: 'wrap' }, ` ${n}`)),
|
|
961
|
-
arming || h(
|
|
1044
|
+
arming || h(AskFoot, { footer }),
|
|
962
1045
|
e == null ? h(Rule, { cols, color: 'warn' }) : h(DrawRule, { cols, color: 'warn', e }));
|
|
963
1046
|
}
|
|
964
1047
|
|
|
@@ -974,22 +1057,26 @@ export function askCardModel(text) {
|
|
|
974
1057
|
export function AskCard({ text, cols }) {
|
|
975
1058
|
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return null; // an empty ask draws nothing: no rules, no blank line
|
|
976
1059
|
const m = askCardModel(text);
|
|
977
|
-
if (m.plain) return h(Box, { width: cols, marginTop: 1 }, h(
|
|
1060
|
+
if (m.plain) return h(Box, { width: cols, marginTop: 1, flexDirection: 'column' }, h(AskFoot, { footer: m.footer }));
|
|
978
1061
|
return h(NeedsYouCard, { cols, title: m.title, rows: m.rows, footer: m.footer });
|
|
979
1062
|
}
|
|
980
1063
|
/** Rows an AskCard takes (sizes the live region). */
|
|
981
1064
|
export const askCardRows = (text, cols) => {
|
|
982
1065
|
if (!String(text ?? '').replace(/^\s*▶?\s*/, '').trim()) return 0;
|
|
983
1066
|
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) +
|
|
1067
|
+
if (m.plain) return 1 + footRows(m.footer, cols);
|
|
1068
|
+
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
1069
|
};
|
|
987
1070
|
|
|
988
1071
|
export function ApprovalCard({ card, org, cols, armed, draftHint = null, motionE = null, confirm = false, position = null }) {
|
|
989
1072
|
const when = card.askedAt ? String(card.askedAt).replace('T', ' ').slice(0, 16) + ' UTC' : null;
|
|
990
1073
|
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]
|
|
1074
|
+
['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
1075
|
].filter(r => r[1]);
|
|
1076
|
+
if (card.plan) { // the plan itself, so the approver sees what they are approving
|
|
1077
|
+
if (card.plan.lines.length) card.plan.lines.forEach((l, i) => rows.push([i ? '' : 'plan', l]));
|
|
1078
|
+
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']);
|
|
1079
|
+
}
|
|
993
1080
|
if (card.privileged) rows.push(['grants', `PRIVILEGED: ${[card.tool, card.target].filter(Boolean).join(' ')}`]);
|
|
994
1081
|
// APPROVAL-WWWH: what exactly (verbatim), where, who, why, when, read-only or write. The command is a block: never word-wrapped.
|
|
995
1082
|
const f = card.facts || null; const block = [];
|
|
@@ -1009,7 +1096,7 @@ export function ApprovalCard({ card, org, cols, armed, draftHint = null, motionE
|
|
|
1009
1096
|
: 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
1097
|
: 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
1098
|
: '▶ Approve it? y approve · n deny · c chat · Tab later · /approvals all';
|
|
1012
|
-
return h(NeedsYouCard, { cols, title, rows, block, notes: card.undescribable && !confirm ? ['Quox could not describe this action'] : [], footer, arming: armed ? null : arming, motionE: e });
|
|
1099
|
+
return h(NeedsYouCard, { cols, title, rows, block, notes: [...(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
1100
|
}
|
|
1014
1101
|
|
|
1015
1102
|
/** Columns the status line's Q takes: 2 for the Braille glyph, 1 for the Ⓠ fallback (2 where Ambiguous is wide). */
|
|
@@ -1046,8 +1133,10 @@ export function statusRows({ cols, slot, agent, groups = [], parts, hint, qw = s
|
|
|
1046
1133
|
// every context item says what it is (owner: "profile moo .. ask?? what?"); /help status explains each in a line
|
|
1047
1134
|
const word = { profile: 'profile ', org: 'org ', mode: 'mode ' };
|
|
1048
1135
|
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:
|
|
1136
|
+
for (const k of ['profile', 'org', 'server', 'remote', 'mode', 'rules', 'auto'].filter(k => p[k])) tokens.push({ pre: ' · ',
|
|
1137
|
+
segs: k === 'server' ? [['⬡ ', 'icon'], ['server ', 'label'], [p.server.name, 'value'], ...(p.server.role ? [[` (${p.server.role})`, 'label']] : [])]
|
|
1138
|
+
: k === 'remote' ? [['→ ', 'icon'], [String(p.remote), 'value']]
|
|
1139
|
+
: 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
1140
|
const tlen = (tk) => tk.segs.reduce((n, sg) => n + len(sg[0]), 0);
|
|
1052
1141
|
// split a token too wide for a row into row-sized pieces (whole characters by cell width, never an ellipsis)
|
|
1053
1142
|
const fit = (tk) => {
|
|
@@ -1097,20 +1186,21 @@ export function statusRows({ cols, slot, agent, groups = [], parts, hint, qw = s
|
|
|
1097
1186
|
}
|
|
1098
1187
|
|
|
1099
1188
|
/** QUOXON-FW17: custom rows, one truncated line each, just above the status line. */
|
|
1100
|
-
export function RowsBar({ rows, cols }) {
|
|
1189
|
+
export function RowsBar({ rows, cols, limit }) {
|
|
1101
1190
|
if (!rows?.length) return null;
|
|
1102
|
-
|
|
1103
|
-
|
|
1191
|
+
// TUI-QPE: layoutRows fits cells rows by keep (whole items drop, never mid-word), lines up a grid's columns and
|
|
1192
|
+
// leaves out rows below their min_cols; plain rows take the classic rowParts + clipParts path unchanged.
|
|
1193
|
+
// /qpe N keeps the first N lines of a grid; it applies to drawn lines (a row can bring several), so expand first.
|
|
1194
|
+
const laid = layoutRows(limit ? limitGrid(expandLines(rows), limit.grid, limit.n) : rows, cols);
|
|
1195
|
+
if (!laid.length) return null;
|
|
1104
1196
|
return h(Box, { flexDirection: 'column', width: cols },
|
|
1105
|
-
...
|
|
1106
|
-
const parts = clipParts(rowParts(r, { labelW }), Math.max(1, cols - 2));
|
|
1197
|
+
...laid.map(({ st: r, parts }) =>
|
|
1107
1198
|
// ROWS: label, value, tone and trend as their own runs; every role is a palette colour (no grey, owner rule).
|
|
1108
|
-
|
|
1109
|
-
}));
|
|
1199
|
+
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
1200
|
}
|
|
1111
1201
|
|
|
1112
1202
|
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;
|
|
1203
|
+
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
1204
|
const qw = statusGlyphWidth();
|
|
1115
1205
|
const rows = statusRows({ cols, slot, agent, groups, parts: p, hint: right, qw });
|
|
1116
1206
|
// The still Q (qglyph.js): two Braille cells, or Ⓠ under NO_COLOR / no Braille. statusRows reserved its width.
|
|
@@ -1131,7 +1221,13 @@ function hintNodes(hint) {
|
|
|
1131
1221
|
}
|
|
1132
1222
|
|
|
1133
1223
|
/** Live, actionable counts under the status line. Never a transcript message. */
|
|
1134
|
-
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false }) {
|
|
1224
|
+
export function NotifyBar({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false, inbox = '' }) {
|
|
1225
|
+
const base = NotifyBarBase({ approvals, error, cols, signedOut, alerts, hideCount });
|
|
1226
|
+
if (!inbox || signedOut) return base;
|
|
1227
|
+
const line = h(Text, { key: 'inbox', color: c('ask'), bold: true }, ' ' + clip(inbox, cols - 3)); // HITL-INBOX-BAR: 📨 N new · newest title
|
|
1228
|
+
return base ? h(Box, { flexDirection: 'column' }, base, line) : line;
|
|
1229
|
+
}
|
|
1230
|
+
function NotifyBarBase({ approvals, error, cols, signedOut = false, alerts = [], hideCount = false }) {
|
|
1135
1231
|
if (signedOut) return h(Text, { color: c('warn') }, ' ' + clip('not signed in: approvals unavailable until you sign in', cols - 2));
|
|
1136
1232
|
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
1233
|
// Background workflows that keep failing: one deduped line each (max 2), never roster rows.
|
|
@@ -1155,13 +1251,14 @@ export function Roster({ view, peek = [], confirm = null, hint = null, cols, fx
|
|
|
1155
1251
|
// glyph keeps the state colour; the rest of the line steps down in tone with position and (for finished rows) age
|
|
1156
1252
|
...view.lines.map((l, i) => {
|
|
1157
1253
|
const e = fx && l.key != null ? fx(l.key) : null; // a row that just appeared decodes in (new rows land on top)
|
|
1158
|
-
|
|
1254
|
+
const row = h(Text, { key: 'r' + i, bold: l.selected, wrap: 'truncate-end' },
|
|
1159
1255
|
h(Text, { color: c(l.selected ? 'text' : (l.glyphRole || ROSTER_TONE[l.state])) }, ' ' + l.marker + l.glyph),
|
|
1160
1256
|
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
1257
|
l.pulse ? h(Text, { color: c(l.pulseRole === 'dim' && l.selected ? 'text' : l.pulseRole) }, l.pulse) : null);
|
|
1258
|
+
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
1259
|
}),
|
|
1163
1260
|
view.more ? h(Text, { key: 'more', color: c('dim') }, ` +${view.more} more · /roster`) : null,
|
|
1164
|
-
...peek.
|
|
1261
|
+
...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
1262
|
confirm ? h(Text, { key: 'cf', color: c('warn') }, ' ' + clip(confirm, cols - 2)) : null,
|
|
1166
1263
|
hint ? h(Text, { key: 'h', color: c('dim') }, ' ' + clip(hint, cols - 2)) : null);
|
|
1167
1264
|
}
|
package/lib/quoxon/roster.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* nothing (never a placeholder row).
|
|
6
6
|
*/
|
|
7
7
|
import { clip } from './theme.js';
|
|
8
|
+
import { humanReason, wrapText } from './loopCard.js';
|
|
8
9
|
|
|
9
10
|
export const GLYPH = { running: '◐', waiting: '⏸', done: '✔', failed: '✖' };
|
|
10
11
|
/** A row that ended without finishing says HOW: failed is the only error; the rest are neutral (no error colour). */
|
|
11
|
-
export const OUTCOME_GLYPH = { failed: '✖', cancelled: '○', expired: '○', rejected: '⊘' };
|
|
12
|
+
export const OUTCOME_GLYPH = { failed: '✖', halted: '⚠', cancelled: '○', expired: '○', rejected: '⊘' };
|
|
12
13
|
export const FRESH_MS = 10 * 60 * 1000; // finished rows linger this long
|
|
13
14
|
export const USER_DONE_MS = 20 * 1000; // a user-started completion vanishes quickly: rows come and go
|
|
14
15
|
export const PENDING_FRESH_MS = 24 * 3600 * 1000; // an older "pending" is stale, not waiting
|
|
@@ -42,13 +43,17 @@ const FAILED = new Set(['failed', 'error', 'halted', 'cancelled', 'canceled', 't
|
|
|
42
43
|
/** The word for how an ended row ended (state stays 'failed' for the ended-badly group; this says which). */
|
|
43
44
|
export function outcomeOf(status) {
|
|
44
45
|
const st = String(status || '').toLowerCase();
|
|
46
|
+
if (st === 'halted') return 'halted';
|
|
45
47
|
if (st === 'cancelled' || st === 'canceled') return 'cancelled';
|
|
46
48
|
if (st === 'expired') return 'expired';
|
|
47
49
|
if (st === 'rejected') return 'rejected';
|
|
48
50
|
return 'failed';
|
|
49
51
|
}
|
|
50
52
|
/** Word, glyph and whether it is an error, for a row. Non-failed states return their own state. */
|
|
51
|
-
export function endWord(r) { return r.state === 'failed' ? outcomeOf(r.status) : r.state; }
|
|
53
|
+
export function endWord(r) { return r.state === 'failed' ? (r.outcome || outcomeOf(r.status)) : r.state; }
|
|
54
|
+
/** A human-halted loop keeps objective status 'failed'; the server says loop_state 'halted' (older servers: the halt text). */
|
|
55
|
+
const HALT_TEXT = /^Loop (?:halted on demand|stopped by human review)/i;
|
|
56
|
+
const isHaltedLoop = (o) => o.execution_mode === 'loop' && (o.loop_state === 'halted' || String(o.status || '').toLowerCase() === 'halted' || (String(o.status || '').toLowerCase() === 'failed' && HALT_TEXT.test(String(o.error_message || ''))));
|
|
52
57
|
export const glyphOf = (r) => (r.state === 'failed' ? OUTCOME_GLYPH[endWord(r)] : GLYPH[r.state]);
|
|
53
58
|
|
|
54
59
|
/** How long a finished row lingers before it leaves the roster (null while running/waiting). */
|
|
@@ -83,18 +88,22 @@ export function fromObjective(o, now = Date.now()) {
|
|
|
83
88
|
if (!state) return null;
|
|
84
89
|
const loop = o.execution_mode === 'loop';
|
|
85
90
|
let activity = '';
|
|
86
|
-
|
|
91
|
+
const haltedLoop = state === 'failed' && isHaltedLoop(o);
|
|
92
|
+
// a halted loop reads its clean sentence (the server's loop_reason, with the person's name), never the raw record text
|
|
93
|
+
const shownError = !haltedLoop ? o.error_message : (o.loop_reason || ((n) => (n ? `Stopped by ${n[1].trim()}` : 'Halted on demand'))(/^Loop halted on demand by \S+ \(([^)]{1,80})\)/i.exec(String(o.error_message || ''))));
|
|
94
|
+
if (state === 'failed' && shownError) activity = humanReason(shownError); // the sentence whole, minus the raw spawner prefix; the view decides how to show it
|
|
95
|
+
else if (loop && state === 'done') activity = o.iteration_count > 0 ? `${o.iteration_count} turn${o.iteration_count === 1 ? '' : 's'}` : ''; // the row prints "done <age>" itself; a stale "iter 0/5" beside it was the lie
|
|
87
96
|
else if (loop && o.max_iterations) activity = `iter ${o.iteration_count || 0}/${o.max_iterations}`;
|
|
88
97
|
else if (o.task_count) activity = `${o.tasks_completed || 0}/${o.task_count} tasks`;
|
|
89
98
|
else if (state === 'waiting') activity = 'waiting to start';
|
|
90
99
|
const from = startedAt ?? createdAt;
|
|
91
100
|
return {
|
|
92
101
|
key: `objective:${o.id}`, id: o.id, source: 'objectives', background: false, kind: loop ? 'loop' : 'objective', state,
|
|
93
|
-
name: o.title || o.id, activity, status: o.status,
|
|
102
|
+
name: o.title || o.id, activity, status: o.status, ...(haltedLoop ? { outcome: 'halted' } : {}),
|
|
94
103
|
startedAt: from, finishedAt, ageMs: from != null ? (state === 'done' || state === 'failed' ? (finishedAt ?? now) - from : now - from) : null,
|
|
95
104
|
expiryMs: expiryFor(state, USER_DONE_MS),
|
|
96
105
|
progressAt: latest(parseTime(o.updated_at), parseTime(o.last_progress_at), parseTime(o.last_heartbeat_at)),
|
|
97
|
-
detail: { description: o.description, error:
|
|
106
|
+
detail: { description: o.description, error: shownError ? humanReason(shownError) : shownError, tokens: o.tokens_used, toolCalls: o.tool_calls_used }
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
|
|
@@ -235,7 +244,7 @@ export function countsLine(rows) {
|
|
|
235
244
|
const n = (s) => rows.filter(r => r.state === s).length;
|
|
236
245
|
const o = (w) => rows.filter(r => r.state === 'failed' && endWord(r) === w).length;
|
|
237
246
|
return [['running', 'running', n('running'), GLYPH.running], ['waiting', 'waiting', n('waiting'), GLYPH.waiting],
|
|
238
|
-
['failed', 'failed', o('failed'), OUTCOME_GLYPH.failed], ['cancelled', 'cancelled', o('cancelled'), OUTCOME_GLYPH.cancelled],
|
|
247
|
+
['failed', 'failed', o('failed'), OUTCOME_GLYPH.failed], ['halted', 'halted', o('halted'), OUTCOME_GLYPH.halted], ['cancelled', 'cancelled', o('cancelled'), OUTCOME_GLYPH.cancelled],
|
|
239
248
|
['expired', 'expired', o('expired'), OUTCOME_GLYPH.expired], ['rejected', 'rejected', o('rejected'), OUTCOME_GLYPH.rejected], ['done', 'finished', n('done'), GLYPH.done]]
|
|
240
249
|
.filter(([, , k]) => k).map(([, w, k, g]) => `${g} ${k} ${w}`).join(' · ');
|
|
241
250
|
}
|
|
@@ -261,6 +270,9 @@ export function collapseApprovals(rows, now = Date.now()) {
|
|
|
261
270
|
return rows.flatMap(r => (aps.includes(r) ? (placed ? [] : (placed = true, [one])) : [r]));
|
|
262
271
|
}
|
|
263
272
|
|
|
273
|
+
const KIND_ICON = { loop: '∞', objective: '◎' }; // QuoxGlyph registry keys qloop / objective
|
|
274
|
+
export const kindLabel = k => (KIND_ICON[k] ? `${KIND_ICON[k]} ${k}` : k);
|
|
275
|
+
|
|
264
276
|
export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = true, sel = 0, focus = false, now = Date.now(), motion = false }) {
|
|
265
277
|
if (!on || !rows.length) return { mode: 'hidden', lines: [], more: 0 };
|
|
266
278
|
if (!focus) rows = collapseApprovals(rows, now);
|
|
@@ -269,13 +281,14 @@ export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = tru
|
|
|
269
281
|
}
|
|
270
282
|
const shown = rows.slice(0, max);
|
|
271
283
|
const ageW = Math.max(5, ...shown.map(r => [...ageText(r, now)].length));
|
|
272
|
-
let pos = 0;
|
|
273
|
-
const kindW = Math.min(
|
|
284
|
+
let pos = 0, reasonShown = false;
|
|
285
|
+
const kindW = Math.min(11, Math.max(...shown.map(r => [...kindLabel(r.kind)].length)));
|
|
274
286
|
const lines = shown.map((r, i) => {
|
|
275
287
|
const el = ageText(r, now).padStart(ageW);
|
|
276
288
|
const tone = toneName(r, pos, now); if (r.kind !== 'approval') pos++;
|
|
277
289
|
const actW = Math.min(18, Math.max(0, ...shown.map(x => [...(x.activity || '')].length)));
|
|
278
|
-
const
|
|
290
|
+
const longReason = r.state === 'failed' && [...(r.activity || '')].length > actW; // a reason is never cut: the column says "stopped", the sentence follows wrapped
|
|
291
|
+
const act = (longReason ? clip('stopped', actW) : clip(r.activity || '', actW)).padEnd(actW);
|
|
279
292
|
const fixed = 1 + 1 + 1 + kindW + 1 + 1 + actW + 1 + ageW + 2; // pulse column (space + glyph); marker glyph kind name act elapsed
|
|
280
293
|
const nameW = Math.max(8, cols - 2 - fixed - 1);
|
|
281
294
|
const name = clip(r.name, nameW).padEnd(nameW);
|
|
@@ -286,9 +299,10 @@ export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = tru
|
|
|
286
299
|
const text = room >= 12 ? `${head}${clip(r.newestTitle, room)}${tail}` : clip(`${r.count} approvals waiting · oldest ${elapsed(r.oldestMs)}${tail}`, cols - 4); // too narrow for a title
|
|
287
300
|
return { key: r.key, kind: 'approval', state: r.state, tone: toneName(r, pos, now), selected: false, marker: ' ', glyph: GLYPH[r.state], rest: ' ' + text, pulse: '', pulseRole: 'dim', pulseKind: 'none', text: ` ${GLYPH[r.state]} ${text}` };
|
|
288
301
|
}
|
|
289
|
-
const rest = `${r.kind.padEnd(kindW)} ${name} ${act} ${el}`;
|
|
302
|
+
const rest = `${kindLabel(r.kind).padEnd(kindW)} ${name} ${act} ${el}`;
|
|
290
303
|
const p = pulseOf(r, now, { motion });
|
|
291
|
-
|
|
304
|
+
const extra = longReason && !reasonShown ? (reasonShown = true, wrapText(r.activity, Math.max(20, cols - 8))) : null; // spelled out for the newest stopped row only: the bar must not grow with every old failure
|
|
305
|
+
return { key: r.key, kind: r.kind, state: r.state, tone, extra, selected: focus && i === sel, marker, glyph: glyphOf(r), glyphRole: r.state === 'failed' && endWord(r) === 'halted' ? 'warn' : r.state === 'failed' && endWord(r) !== 'failed' ? 'dim' : null, rest: ' ' + rest, pulse: ' ' + p.glyph, pulseRole: p.role, pulseKind: p.kind, text: `${marker}${glyphOf(r)} ${rest} ${p.glyph}` };
|
|
292
306
|
});
|
|
293
307
|
return { mode: 'full', lines, more: Math.max(0, rows.length - shown.length) };
|
|
294
308
|
}
|
|
@@ -302,6 +316,6 @@ export function peekLines(row, now = Date.now()) {
|
|
|
302
316
|
if (d.description) out.push(`goal ${d.description}`);
|
|
303
317
|
if (d.workflowId) out.push(`workflow ${d.workflowId}`);
|
|
304
318
|
if (d.trigger) out.push(`trigger ${d.trigger}`);
|
|
305
|
-
if (d.error && row.state === 'failed') out.push(`error ${typeof d.error === 'string' ? d.error : JSON.stringify(d.error)}`);
|
|
319
|
+
if (d.error && row.state === 'failed') out.push(`error ${typeof d.error === 'string' ? humanReason(d.error) : JSON.stringify(d.error)}`);
|
|
306
320
|
return out;
|
|
307
321
|
}
|