synabun 2.2.0 → 2.2.1
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 +18 -0
- package/README.md +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/i18n/en.json +3 -0
- package/neural-interface/i18n/pt-BR.json +3 -0
- package/neural-interface/lib/assistant-brains/codex.js +6 -1
- package/neural-interface/lib/assistant-dispatch.js +4 -1
- package/neural-interface/lib/mcp-external-servers.js +27 -3
- package/neural-interface/lib/native-loop-providers.js +3 -2
- package/neural-interface/lib/opencode-native-boundary.js +155 -21
- package/neural-interface/lib/peer-identity.js +49 -5
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/assistant/asst-api.js +14 -2
- package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
- package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1 -1
- package/neural-interface/public/shared/assistant/asst-render.js +62 -7
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
- package/neural-interface/public/shared/assistant/asst-scenes.js +225 -79
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
- package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +24 -4
- package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
- package/neural-interface/public/shared/loading-teardown.js +34 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +1 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +10 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +88 -72
- package/neural-interface/public/shared/storage-retry.js +47 -0
- package/neural-interface/public/shared/storage.js +25 -4
- package/neural-interface/public/shared/styles.css +8 -1
- package/neural-interface/public/shared/synabun-mascot.js +27 -8
- package/neural-interface/public/shared/synabun-ticker.js +147 -12
- package/neural-interface/public/shared/ui-claude-panel.js +79 -61
- package/neural-interface/public/shared/ui-loading.js +5 -4
- package/neural-interface/server.js +87 -40
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -400,12 +400,77 @@ export function stageLabel({ scene = 'generic', phase = 'working', title = '', m
|
|
|
400
400
|
// currentColor (the container's muted text); `sc-gold` is the one live part
|
|
401
401
|
// (gold only means live). Every part sits where its still frame wants it:
|
|
402
402
|
// with no animation (reduced motion, waiting, settled) the scene still reads.
|
|
403
|
+
//
|
|
404
|
+
// A scene is an HTML box (`.asst-scene`, sized by its holder) holding one SVG
|
|
405
|
+
// of everything that stands still (`.sc-base`) and, for every part that
|
|
406
|
+
// moves, an HTML layer of its own (`.sc-layer`: a box over the whole scene
|
|
407
|
+
// with an SVG of that part alone in the same viewBox, so the part sits
|
|
408
|
+
// exactly where it did). The CSS moves the layer box, transform and opacity
|
|
409
|
+
// only, never an SVG element: a box's animation runs on the compositor, and a
|
|
410
|
+
// frame the page draws anyway (a rig's) restyles one span and lays nothing
|
|
411
|
+
// out, where a moving SVG part re-lays its whole SVG out on every frame.
|
|
412
|
+
// Lengths are percentages of the box, since a scene is drawn at several sizes
|
|
413
|
+
// (boxPct); a part that turned round its own centre (the SVG's fill-box)
|
|
414
|
+
// turns round the same point here (`transform-origin`). Parts on the same
|
|
415
|
+
// beat share a layer (the image's tiles); a part that stood above a moving
|
|
416
|
+
// one stays above it in a still layer of its own (`.sc-top`: the playhead).
|
|
403
417
|
|
|
404
418
|
/** The code scene's six lines, repeated twice so its stepped scroll loops seamlessly: [indent, keyword, rest]. */
|
|
405
419
|
export const CODE_LINES = Object.freeze([[6, 8, 26], [12, 10, 42], [12, 6, 22], [18, 9, 35], [12, 12, 48], [6, 7, 14]]);
|
|
406
420
|
export const CODE_STEP = 6; // one line, in viewBox units
|
|
421
|
+
/** The code scene's scrolling window and the video scene's film window (full) and the mini film's: [x, y, w, h]. */
|
|
422
|
+
export const CODE_WINDOW = Object.freeze([4, 4, 112, 32]);
|
|
423
|
+
export const FILM_WINDOW = Object.freeze([4, 4, 112, 32]);
|
|
424
|
+
export const FILM_MINI_WINDOW = Object.freeze([2, 2, 36, 16]);
|
|
425
|
+
|
|
426
|
+
/** The viewBox of a full scene and of a mini, in units. */
|
|
427
|
+
export const SCENE_BOX = Object.freeze({ full: Object.freeze({ w: 120, h: 40 }), mini: Object.freeze({ w: 40, h: 20 }) });
|
|
407
428
|
|
|
408
429
|
const r1 = (v) => Math.round(v * 100) / 100;
|
|
430
|
+
const r3 = (v) => Math.round(v * 1000) / 1000;
|
|
431
|
+
|
|
432
|
+
/** A length in viewBox units as a percentage of the box's width or height: boxPct(14, 120) → '11.667%'. */
|
|
433
|
+
export function boxPct(v, of) {
|
|
434
|
+
return `${r3((Number(v) / of) * 100)}%`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The centre of a polyline path's box (M L H V Z, absolute or relative):
|
|
439
|
+
* where an SVG part with transform-box: fill-box turned round (a tick, the
|
|
440
|
+
* zap, a triangle). Curves are not read (no moving part has one).
|
|
441
|
+
*/
|
|
442
|
+
export function pathCentre(d) {
|
|
443
|
+
const tokens = String(d).match(/[MLHVZmlhvz]|-?\d*\.?\d+/g) || [];
|
|
444
|
+
const xs = [];
|
|
445
|
+
const ys = [];
|
|
446
|
+
let x = 0;
|
|
447
|
+
let y = 0;
|
|
448
|
+
let sx = 0;
|
|
449
|
+
let sy = 0;
|
|
450
|
+
let cmd = 'M';
|
|
451
|
+
let i = 0;
|
|
452
|
+
const num = () => Number(tokens[i++]);
|
|
453
|
+
while (i < tokens.length) {
|
|
454
|
+
if (/^[A-Za-z]$/.test(tokens[i])) {
|
|
455
|
+
cmd = tokens[i];
|
|
456
|
+
i += 1;
|
|
457
|
+
if (cmd === 'Z' || cmd === 'z') { x = sx; y = sy; continue; }
|
|
458
|
+
}
|
|
459
|
+
if (cmd === 'M') { x = num(); y = num(); sx = x; sy = y; cmd = 'L'; }
|
|
460
|
+
else if (cmd === 'm') { x += num(); y += num(); sx = x; sy = y; cmd = 'l'; }
|
|
461
|
+
else if (cmd === 'L') { x = num(); y = num(); }
|
|
462
|
+
else if (cmd === 'l') { x += num(); y += num(); }
|
|
463
|
+
else if (cmd === 'H') x = num();
|
|
464
|
+
else if (cmd === 'h') x += num();
|
|
465
|
+
else if (cmd === 'V') y = num();
|
|
466
|
+
else if (cmd === 'v') y += num();
|
|
467
|
+
else { i += 1; continue; }
|
|
468
|
+
xs.push(x);
|
|
469
|
+
ys.push(y);
|
|
470
|
+
}
|
|
471
|
+
if (!xs.length) return [0, 0];
|
|
472
|
+
return [r3((Math.min(...xs) + Math.max(...xs)) / 2), r3((Math.min(...ys) + Math.max(...ys)) / 2)];
|
|
473
|
+
}
|
|
409
474
|
|
|
410
475
|
/** A gear outline: n trapezoid teeth between radius r and R round (cx, cy). */
|
|
411
476
|
export function gearPath(cx, cy, r, R, n) {
|
|
@@ -426,93 +491,149 @@ export function codeCaretX(s) {
|
|
|
426
491
|
return 4 + indent + key + 3 + rest + 2;
|
|
427
492
|
}
|
|
428
493
|
|
|
429
|
-
|
|
494
|
+
const SVG_ATTRS = 'fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" focusable="false"';
|
|
495
|
+
const svgOf = (viewBox, body) => `<svg viewBox="${viewBox}" ${SVG_ATTRS}>${body}</svg>`;
|
|
496
|
+
|
|
497
|
+
/** Everything of a scene that stands still: one SVG under the layers. */
|
|
498
|
+
function base(box, body) {
|
|
499
|
+
return `<svg class="sc-base" viewBox="0 0 ${box.w} ${box.h}" width="${box.w}" height="${box.h}" ${SVG_ATTRS}>${body}</svg>`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* One moving part's layer, over the whole box (or over a window: clipOf).
|
|
504
|
+
* `cls` names its motion (the CSS animates it); `body` is the part's SVG
|
|
505
|
+
* markup in the box's units, or `html` another layer inside it (a caret that
|
|
506
|
+
* blinks inside a caret that steps). `origin` [x, y]: what rotation and scale
|
|
507
|
+
* turn round (the old fill-box centre, or a view-box point); `still` [x, y,
|
|
508
|
+
* more]: the resting transform of a part drawn at 0,0; `i`: its beat.
|
|
509
|
+
*/
|
|
510
|
+
function layerOf(box, cls, body, { origin = null, still = null, i = null, html = null } = {}) {
|
|
511
|
+
const style = [];
|
|
512
|
+
if (i != null) style.push(`--i:${i}`);
|
|
513
|
+
if (origin) style.push(`transform-origin:${boxPct(origin[0], box.w)} ${boxPct(origin[1], box.h)}`);
|
|
514
|
+
if (still) style.push(`transform:translate(${boxPct(still[0], box.w)},${boxPct(still[1], box.h)})${still[2] ? ` ${still[2]}` : ''}`);
|
|
515
|
+
const inner = html != null ? html : svgOf(`0 0 ${box.w} ${box.h}`, body);
|
|
516
|
+
return `<span class="sc-layer${cls ? ` ${cls}` : ''}"${style.length ? ` style="${style.join(';')}"` : ''}>${inner}</span>`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** A still part that stood above a moving one: it stays above it. */
|
|
520
|
+
const topOf = (box, body) => layerOf(box, 'sc-top', body);
|
|
521
|
+
|
|
522
|
+
/** A clipped window of the scene ([x, y, w, h] in units): what scrolls inside it is cut at its edges. */
|
|
523
|
+
function clipOf(box, [x, y, w, h], inner) {
|
|
524
|
+
return `<span class="sc-clip" style="left:${boxPct(x, box.w)};top:${boxPct(y, box.h)};width:${boxPct(w, box.w)};height:${boxPct(h, box.h)}">${inner}</span>`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Parts that share a beat (`--i`) share one layer: [{ i, body }] → one layer per beat, in first-seen order. */
|
|
528
|
+
function beatLayers(box, cls, parts) {
|
|
529
|
+
const beats = new Map();
|
|
530
|
+
for (const { i, body } of parts) beats.set(i, (beats.get(i) || '') + body);
|
|
531
|
+
return [...beats].map(([i, body]) => layerOf(box, cls, body, { i })).join('');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function codeFull(box) {
|
|
430
535
|
let lines = '';
|
|
431
536
|
for (let i = 0; i < CODE_LINES.length * 2; i += 1) {
|
|
432
537
|
const [indent, key, rest] = CODE_LINES[i % CODE_LINES.length];
|
|
433
538
|
const y = 5 + CODE_STEP * i;
|
|
434
539
|
lines += `<path class="sc-mid" d="M${indent} ${y}h${key}"/><path class="sc-dim" d="M${indent + key + 3} ${y}h${rest}"/>`;
|
|
435
540
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
541
|
+
const [, , w, h] = CODE_WINDOW;
|
|
542
|
+
const caret = layerOf(box, 'sc-caret', '<rect class="sc-gold sc-fill" x="0" y="30.5" width="2" height="5" rx="1"/>');
|
|
543
|
+
return base(box, '<rect class="sc-dim" x="1" y="1.5" width="118" height="37" rx="5"/>')
|
|
544
|
+
+ clipOf(box, CODE_WINDOW, layerOf({ w, h }, 'sc-code-scroll', lines))
|
|
545
|
+
+ layerOf(box, 'sc-code-caret', null, { still: [codeCaretX(0), 0], html: caret });
|
|
439
546
|
}
|
|
440
547
|
|
|
441
548
|
const GEAR_BIG = gearPath(66, 20, 9, 12.5, 8);
|
|
442
549
|
const GEAR_SMALL = gearPath(86.5, 12.5, 5.5, 8, 6);
|
|
443
550
|
|
|
444
|
-
function complexFull() {
|
|
445
|
-
return '<path class="sc-dim" d="M14 7h13l5 5v21H14z"/><path class="sc-mid" d="M8 11h13l5 5v21H8zM21 11v5h5M12 23h9M12 28h11"/>'
|
|
446
|
-
+
|
|
447
|
-
+
|
|
448
|
-
|
|
551
|
+
function complexFull(box) {
|
|
552
|
+
return base(box, '<path class="sc-dim" d="M14 7h13l5 5v21H14z"/><path class="sc-mid" d="M8 11h13l5 5v21H8zM21 11v5h5M12 23h9M12 28h11"/><path class="sc-dim" d="M98 30h14M102 25h10"/>')
|
|
553
|
+
+ layerOf(box, 'sc-gear-big', `<path class="sc-mid" d="${GEAR_BIG}"/><circle class="sc-dim" cx="66" cy="20" r="3.2"/>`, { origin: [66, 20] })
|
|
554
|
+
+ layerOf(box, 'sc-gear-small', `<path class="sc-gold" d="${GEAR_SMALL}"/><circle class="sc-gold" cx="86.5" cy="12.5" r="2"/>`, { origin: [86.5, 12.5] });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A review tick, its own layer (it pops round its own centre). */
|
|
558
|
+
function tickLayer(box, d, i) {
|
|
559
|
+
return layerOf(box, 'sc-tick', `<path class="sc-gold" d="${d}"/>`, { origin: pathCentre(d), i });
|
|
449
560
|
}
|
|
450
561
|
|
|
451
|
-
function reviewFull() {
|
|
562
|
+
function reviewFull(box) {
|
|
452
563
|
const rows = [[9, '+', 52], [16, '-', 64], [23, '+', 40], [30, '+', 58]];
|
|
453
|
-
let
|
|
564
|
+
let still = '<rect class="sc-dim" x="1" y="1.5" width="118" height="37" rx="5"/>';
|
|
565
|
+
let ticks = '';
|
|
454
566
|
rows.forEach(([y, sign, len], i) => {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
567
|
+
still += sign === '+' ? `<path class="sc-mid" d="M6 ${y}h4M8 ${y - 2}v4"/>` : `<path class="sc-mid" d="M6 ${y}h4"/>`;
|
|
568
|
+
still += `<path class="sc-dim" d="M15 ${y}h${len}"/>`;
|
|
569
|
+
ticks += tickLayer(box, `M95 ${y}l2.2 2.2 4.4-4.6`, i);
|
|
458
570
|
});
|
|
459
|
-
return
|
|
571
|
+
return base(box, still) + ticks;
|
|
460
572
|
}
|
|
461
573
|
|
|
462
|
-
function researchFull() {
|
|
574
|
+
function researchFull(box) {
|
|
463
575
|
const page = (x) => `<rect class="sc-dim" x="${x}" y="4" width="40" height="32" rx="3"/><path class="sc-dim" d="M${x + 6} 11h26M${x + 6} 17h22M${x + 6} 23h28M${x + 6} 29h16"/>`;
|
|
464
|
-
return page(8) + page(56)
|
|
465
|
-
+ '
|
|
576
|
+
return base(box, page(8) + page(56))
|
|
577
|
+
+ layerOf(box, 'sc-lens', '<circle class="sc-gold" cx="24" cy="17" r="7"/><path class="sc-gold" d="M29.2 22.2l5 5"/>');
|
|
466
578
|
}
|
|
467
579
|
|
|
468
580
|
const POINTER = 'M0 0v10.5l2.8-2.6 2 4.4 2-.9-2-4.3h3.9z';
|
|
469
581
|
|
|
470
|
-
|
|
471
|
-
|
|
582
|
+
/** A click's ring (a layer that starts invisible: .sc-ring) and the pointer that clicks there (drawn at 0,0, placed by its layer). */
|
|
583
|
+
function pointerLayers(box, kind, [cx, cy], r) {
|
|
584
|
+
return layerOf(box, `sc-ring sc-ripple-${kind}`, `<circle class="sc-gold" cx="${cx}" cy="${cy}" r="${r}"/>`, { origin: [cx, cy] })
|
|
585
|
+
+ layerOf(box, `sc-point-${kind}`, `<path class="sc-gold sc-fill" d="${POINTER}"/>`, { still: [cx, cy] });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function browserFull(box) {
|
|
589
|
+
return base(box, '<rect class="sc-dim" x="1" y="1.5" width="118" height="37" rx="5"/><path class="sc-dim" d="M1 9.5h118"/>'
|
|
472
590
|
+ '<circle class="sc-mid sc-fill" cx="6" cy="5.5" r="1"/><circle class="sc-mid sc-fill" cx="10" cy="5.5" r="1"/><circle class="sc-mid sc-fill" cx="14" cy="5.5" r="1"/>'
|
|
473
591
|
+ '<rect class="sc-dim" x="22" y="3.5" width="56" height="4" rx="2"/>'
|
|
474
|
-
+ '<path class="sc-mid" d="M8 16h36"/><path class="sc-dim" d="M8 22h54M8 27h44M8 32h30"/><rect class="sc-mid" x="78" y="25" width="28" height="8" rx="2"/>'
|
|
475
|
-
+ '
|
|
476
|
-
+ `<g class="sc-point-browser" style="transform:translate(92px,29px)"><path class="sc-gold sc-fill" d="${POINTER}"/></g>`;
|
|
592
|
+
+ '<path class="sc-mid" d="M8 16h36"/><path class="sc-dim" d="M8 22h54M8 27h44M8 32h30"/><rect class="sc-mid" x="78" y="25" width="28" height="8" rx="2"/>')
|
|
593
|
+
+ pointerLayers(box, 'browser', [92, 29], 5);
|
|
477
594
|
}
|
|
478
595
|
|
|
479
|
-
function computerFull() {
|
|
480
|
-
return '<rect class="sc-dim" x="10" y="1.5" width="100" height="30" rx="3"/><path class="sc-dim" d="M60 31.5v4M48 37.5h24"/>'
|
|
596
|
+
function computerFull(box) {
|
|
597
|
+
return base(box, '<rect class="sc-dim" x="10" y="1.5" width="100" height="30" rx="3"/><path class="sc-dim" d="M60 31.5v4M48 37.5h24"/>'
|
|
481
598
|
+ '<rect class="sc-mid sc-shade" x="16" y="6" width="44" height="20" rx="2"/><path class="sc-dim" d="M16 10h44M21 15h20M21 20h28"/>'
|
|
482
|
-
+ '<rect class="sc-mid sc-shade" x="52" y="11" width="50" height="17" rx="2"/><path class="sc-dim" d="M52 15h50"/><rect class="sc-mid" x="84" y="20" width="13" height="5" rx="1.5"/>'
|
|
483
|
-
+ '
|
|
484
|
-
+ `<g class="sc-point-computer" style="transform:translate(90px,22.5px)"><path class="sc-gold sc-fill" d="${POINTER}"/></g>`;
|
|
599
|
+
+ '<rect class="sc-mid sc-shade" x="52" y="11" width="50" height="17" rx="2"/><path class="sc-dim" d="M52 15h50"/><rect class="sc-mid" x="84" y="20" width="13" height="5" rx="1.5"/>')
|
|
600
|
+
+ pointerLayers(box, 'computer', [90, 22.5], 4.5);
|
|
485
601
|
}
|
|
486
602
|
|
|
487
|
-
function automationFull() {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
+ '<
|
|
603
|
+
function automationFull(box) {
|
|
604
|
+
// The hub dot sits in the still SVG: hand and dot are the same gold, so which is on top shows nowhere.
|
|
605
|
+
return base(box, '<circle class="sc-mid" cx="18" cy="20" r="13"/><path class="sc-dim" d="M18 9v2M18 29v2M7 20h2M27 20h2"/>'
|
|
606
|
+
+ '<path class="sc-mid" d="M18 20l5-4"/><circle class="sc-gold sc-fill" cx="18" cy="20" r="1.6"/>'
|
|
491
607
|
+ '<path class="sc-dim" d="M52 24h14M76 24h14"/><rect class="sc-mid" x="44" y="20" width="8" height="8" rx="2"/><rect class="sc-mid" x="66" y="20" width="10" height="8" rx="2"/><rect class="sc-mid" x="90" y="20" width="8" height="8" rx="2"/>'
|
|
492
|
-
+ '<path class="sc-dim" d="M94 17c0-9-46-9-46 0M48 17l-2.5-3M48 17l3-2"/>'
|
|
493
|
-
+ '<
|
|
608
|
+
+ '<path class="sc-dim" d="M94 17c0-9-46-9-46 0M48 17l-2.5-3M48 17l3-2"/>')
|
|
609
|
+
+ layerOf(box, 'sc-hand', '<path class="sc-gold" d="M18 20v-9"/>', { origin: [18, 20] })
|
|
610
|
+
+ layerOf(box, 'sc-run-dot', '<circle class="sc-gold sc-fill" cx="48" cy="24" r="2.2"/>');
|
|
494
611
|
}
|
|
495
612
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
+ '<rect class="sc-
|
|
501
|
-
+ '<
|
|
502
|
-
+ '<
|
|
613
|
+
const TRIANGLE = 'M86 28l9-16 9 16z';
|
|
614
|
+
|
|
615
|
+
function designFull(box) {
|
|
616
|
+
return base(box, '<rect class="sc-gold sc-fill sc-swatch" x="4" y="5" width="9" height="9" rx="2"/><rect class="sc-mid sc-fill sc-swatch-b" x="4" y="16" width="9" height="9" rx="2"/><rect class="sc-dim sc-fill sc-swatch-c" x="4" y="27" width="9" height="9" rx="2"/>'
|
|
617
|
+
+ '<rect class="sc-dim" x="20" y="2.5" width="96" height="35" rx="4"/>')
|
|
618
|
+
+ layerOf(box, 'sc-shape', '<circle class="sc-mid" cx="40" cy="20" r="8"/>', { origin: [40, 20], i: 0 })
|
|
619
|
+
+ layerOf(box, 'sc-shape', '<rect class="sc-mid" x="58" y="12" width="16" height="16" rx="3"/>', { origin: [66, 20], i: 1 })
|
|
620
|
+
+ layerOf(box, 'sc-shape', `<path class="sc-mid" d="${TRIANGLE}"/>`, { origin: pathCentre(TRIANGLE), i: 2 })
|
|
621
|
+
+ layerOf(box, 'sc-pen', '<path class="sc-gold" d="M0 0l6-6M6-6l2.5 2.5M0 0l-1.2 3.7L2.5 2.5z"/>', { still: [40, 20] });
|
|
503
622
|
}
|
|
504
623
|
|
|
505
|
-
|
|
506
|
-
|
|
624
|
+
const SPARKLE = 'M104 6l1.6 4.4L110 12l-4.4 1.6L104 18l-1.6-4.4L98 12l4.4-1.6z';
|
|
625
|
+
|
|
626
|
+
function imageFull(box) {
|
|
627
|
+
const tiles = [];
|
|
507
628
|
for (let row = 0; row < 3; row += 1) {
|
|
508
629
|
for (let col = 0; col < 4; col += 1) {
|
|
509
630
|
const gold = (row === 1 && col === 2) || (row === 2 && col === 0);
|
|
510
|
-
tiles
|
|
631
|
+
tiles.push({ i: row + col, body: `<rect class="${gold ? 'sc-gold' : 'sc-mid'} sc-fill" x="${34 + col * 13.5}" y="${6 + row * 10}" width="11.5" height="8" rx="1.5"/>` });
|
|
511
632
|
}
|
|
512
633
|
}
|
|
513
|
-
return
|
|
514
|
-
+
|
|
515
|
-
+ '
|
|
634
|
+
return base(box, '<rect class="sc-dim" x="30" y="2.5" width="61" height="35" rx="3"/><path class="sc-dim" d="M100 26h12M100 31h8"/>')
|
|
635
|
+
+ beatLayers(box, 'sc-tile', tiles)
|
|
636
|
+
+ layerOf(box, 'sc-sparkle', `<path class="sc-gold sc-fill" d="${SPARKLE}"/>`, { origin: pathCentre(SPARKLE) });
|
|
516
637
|
}
|
|
517
638
|
|
|
518
639
|
function filmFrames(width, y, h, frameW, count) {
|
|
@@ -522,47 +643,71 @@ function filmFrames(width, y, h, frameW, count) {
|
|
|
522
643
|
return out;
|
|
523
644
|
}
|
|
524
645
|
|
|
525
|
-
function videoFull() {
|
|
526
|
-
|
|
527
|
-
|
|
646
|
+
function videoFull(box) {
|
|
647
|
+
const [, , w, h] = FILM_WINDOW;
|
|
648
|
+
return base(box, '')
|
|
649
|
+
+ clipOf(box, FILM_WINDOW, layerOf({ w, h }, 'sc-film', filmFrames(160, 2, 28, 24, 7)))
|
|
650
|
+
+ topOf(box, '<path class="sc-gold" d="M58 2v36M55 2h6l-3 3z"/>');
|
|
528
651
|
}
|
|
529
652
|
|
|
530
653
|
/** An open-end wrench, horizontal, its jaw round (0, 0): place it with translate(bolt) rotate(a) translate(3.5 0). */
|
|
531
654
|
const WRENCH = 'M-4.6 -3.86L-1 -2V2L-4.6 3.86A6 6 0 0 0 5.5 2.4H24A2.4 2.4 0 0 0 24 -2.4H5.5A6 6 0 0 0 -4.6 -3.86Z';
|
|
532
655
|
const hexagon = (cx, cy, r) => `M${[0, 1, 2, 3, 4, 5].map(i => `${r1(cx + r * Math.cos(i * Math.PI / 3))} ${r1(cy + r * Math.sin(i * Math.PI / 3))}`).join('L')}Z`;
|
|
656
|
+
const ZAP = 'M92 5l-8 15h7l-3 14 11-18h-7.5z';
|
|
657
|
+
|
|
658
|
+
/** Sparks off the bolt: one layer each (they flash one after another). */
|
|
659
|
+
const sparkLayers = (box, ds) => ds.map((d, i) => layerOf(box, 'sc-spark', `<path class="sc-gold" d="${d}"/>`, { i })).join('');
|
|
533
660
|
|
|
534
|
-
function quickFull() {
|
|
535
|
-
return `<path class="sc-mid" d="${hexagon(38, 24, 3.4)}"/>`
|
|
536
|
-
+
|
|
537
|
-
+ '
|
|
538
|
-
+ '
|
|
661
|
+
function quickFull(box) {
|
|
662
|
+
return base(box, `<path class="sc-mid" d="${hexagon(38, 24, 3.4)}"/>`)
|
|
663
|
+
+ layerOf(box, 'sc-wrench', `<path class="sc-gold" transform="translate(38 24) rotate(-32) translate(3.5 0)" d="${WRENCH}"/>`, { origin: [38, 24] })
|
|
664
|
+
+ sparkLayers(box, ['M30.5 19.5l-3-2', 'M29.5 24.5h-3.5', 'M30.5 29.5l-3 2'])
|
|
665
|
+
+ layerOf(box, 'sc-zap', `<path class="sc-gold" d="${ZAP}"/>`, { origin: pathCentre(ZAP) });
|
|
539
666
|
}
|
|
540
667
|
|
|
541
|
-
function genericFull() {
|
|
542
|
-
return '<path class="sc-dim" d="M14 20h92"/><circle class="sc-mid" cx="14" cy="20" r="3"/><circle class="sc-mid" cx="60" cy="20" r="3"/><circle class="sc-mid" cx="106" cy="20" r="3"/>'
|
|
543
|
-
+ '
|
|
668
|
+
function genericFull(box) {
|
|
669
|
+
return base(box, '<path class="sc-dim" d="M14 20h92"/><circle class="sc-mid" cx="14" cy="20" r="3"/><circle class="sc-mid" cx="60" cy="20" r="3"/><circle class="sc-mid" cx="106" cy="20" r="3"/>')
|
|
670
|
+
+ layerOf(box, 'sc-slide', '<rect class="sc-gold sc-fill" x="14" y="18.5" width="24" height="3" rx="1.5"/>');
|
|
544
671
|
}
|
|
545
672
|
|
|
673
|
+
const MINI_TRIANGLE = 'M29 15l4.5-8 4.5 8z';
|
|
674
|
+
|
|
546
675
|
// Minis (40×20, the tray at 32×16): one gesture each.
|
|
547
676
|
const MINI = {
|
|
548
|
-
code:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
677
|
+
code: box => base(box, '<path class="sc-mid" d="M5 5h20M9 10h24M5 15h14"/>')
|
|
678
|
+
+ layerOf(box, 'sc-caret', '<rect class="sc-gold sc-fill" x="21.5" y="12.5" width="1.8" height="5" rx=".9"/>'),
|
|
679
|
+
complex: box => base(box, '')
|
|
680
|
+
+ layerOf(box, 'sc-gear-big', `<path class="sc-mid" d="${gearPath(14, 11, 4.5, 6.6, 6)}"/>`, { origin: [14, 11] })
|
|
681
|
+
+ layerOf(box, 'sc-gear-small', `<path class="sc-gold" d="${gearPath(26.5, 7.5, 3, 4.6, 6)}"/>`, { origin: [26.5, 7.5] }),
|
|
682
|
+
review: box => base(box, '<path class="sc-dim" d="M4 5h20M4 10h24M4 15h18"/>')
|
|
683
|
+
+ tickLayer(box, 'M30 5l1.6 1.6 3.2-3.4', 0) + tickLayer(box, 'M32 10l1.6 1.6 3.2-3.4', 1),
|
|
684
|
+
research: box => base(box, '<rect class="sc-dim" x="3" y="2.5" width="26" height="15" rx="2"/><path class="sc-dim" d="M7 7h16M7 11h12M7 15h14"/>')
|
|
685
|
+
+ layerOf(box, 'sc-lens-mini', '<circle class="sc-gold" cx="12" cy="9" r="3.8"/><path class="sc-gold" d="M14.8 11.8l2.8 2.8"/>'),
|
|
686
|
+
browser: box => base(box, '<rect class="sc-dim" x="2" y="2" width="36" height="16" rx="2.5"/><path class="sc-dim" d="M2 6h36"/><rect class="sc-mid" x="24" y="11" width="10" height="4" rx="1.2"/>')
|
|
687
|
+
+ layerOf(box, 'sc-point-mini', `<path class="sc-gold sc-fill" d="${POINTER}"/>`, { still: [29, 13, 'scale(.6)'] }),
|
|
688
|
+
computer: box => base(box, '<rect class="sc-dim" x="5" y="1.5" width="30" height="14" rx="2"/><path class="sc-dim" d="M20 15.5v2M15 18.5h10"/><rect class="sc-mid" x="8" y="4.5" width="13" height="8" rx="1"/>')
|
|
689
|
+
+ layerOf(box, 'sc-point-mini', `<path class="sc-gold sc-fill" d="${POINTER}"/>`, { still: [29, 13, 'scale(.6)'] }),
|
|
690
|
+
automation: box => base(box, '<circle class="sc-mid" cx="11" cy="10" r="7.5"/><path class="sc-dim" d="M11 10l3-2.5"/><path class="sc-mid" d="M33.5 10a5 5 0 1 1-1.5-3.6M32 3.6v2.8h-2.8"/>')
|
|
691
|
+
+ layerOf(box, 'sc-hand', '<path class="sc-gold" d="M11 10V4.5"/>', { origin: [11, 10] }),
|
|
692
|
+
design: box => base(box, '<rect class="sc-mid sc-fill" x="3" y="3" width="6" height="6" rx="1.5"/><rect class="sc-dim sc-fill" x="3" y="11" width="6" height="6" rx="1.5"/>')
|
|
693
|
+
+ layerOf(box, 'sc-shape', '<circle class="sc-gold" cx="21" cy="10" r="5"/>', { origin: [21, 10], i: 0 })
|
|
694
|
+
+ layerOf(box, 'sc-shape', `<path class="sc-mid" d="${MINI_TRIANGLE}"/>`, { origin: pathCentre(MINI_TRIANGLE), i: 1 }),
|
|
695
|
+
image_gen: (box) => {
|
|
696
|
+
const tiles = [];
|
|
697
|
+
for (let row = 0; row < 2; row += 1) for (let col = 0; col < 3; col += 1) tiles.push({ i: row + col, body: `<rect class="${row === 1 && col === 2 ? 'sc-gold' : 'sc-mid'} sc-fill" x="${11 + col * 6.4}" y="${5 + row * 5.6}" width="5" height="4.2" rx="1"/>` });
|
|
698
|
+
return base(box, '<rect class="sc-dim" x="9" y="2.5" width="22" height="15" rx="2"/>') + beatLayers(box, 'sc-tile', tiles);
|
|
699
|
+
},
|
|
700
|
+
video_gen: (box) => {
|
|
701
|
+
const [, , w, h] = FILM_MINI_WINDOW;
|
|
702
|
+
return base(box, '')
|
|
703
|
+
+ clipOf(box, FILM_MINI_WINDOW, layerOf({ w, h }, 'sc-film-mini', filmFrames(60, 1, 14, 12, 5)))
|
|
704
|
+
+ topOf(box, '<path class="sc-gold" d="M20 1v18"/>');
|
|
561
705
|
},
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
+ '
|
|
565
|
-
generic:
|
|
706
|
+
quick: box => base(box, `<path class="sc-mid" d="${hexagon(11, 11, 2.6)}"/>`)
|
|
707
|
+
+ layerOf(box, 'sc-wrench', `<path class="sc-gold" stroke-width="2.4" transform="translate(11 11) rotate(-32) scale(.62) translate(3.5 0)" d="${WRENCH}"/>`, { origin: [11, 11] })
|
|
708
|
+
+ sparkLayers(box, ['M5.5 7.5l-2-1.3', 'M5 11.5H2.6']),
|
|
709
|
+
generic: box => base(box, '')
|
|
710
|
+
+ [11, 20, 29].map((cx, i) => layerOf(box, 'sc-dot', `<circle class="sc-gold sc-fill" cx="${cx}" cy="10" r="2.4"/>`, { i })).join(''),
|
|
566
711
|
};
|
|
567
712
|
|
|
568
713
|
const FULL = {
|
|
@@ -571,13 +716,14 @@ const FULL = {
|
|
|
571
716
|
};
|
|
572
717
|
|
|
573
718
|
/**
|
|
574
|
-
* A scene's
|
|
719
|
+
* A scene's markup (decorative: aria-hidden): an HTML box with its still SVG
|
|
720
|
+
* and one layer per moving part (see above). `mini` draws the tray's 40×20
|
|
575
721
|
* version. `live` starts it moving (the stage and tray set data-live
|
|
576
722
|
* themselves later; this is only the first state).
|
|
577
723
|
*/
|
|
578
724
|
export function sceneSvg(scene, { mini = false, live = false, className = '' } = {}) {
|
|
579
725
|
const id = Object.hasOwn(FULL, scene) ? scene : 'generic';
|
|
580
|
-
const
|
|
581
|
-
const body = (mini ? MINI[id] : FULL[id])();
|
|
582
|
-
return `<
|
|
726
|
+
const box = mini ? SCENE_BOX.mini : SCENE_BOX.full;
|
|
727
|
+
const body = (mini ? MINI[id] : FULL[id])(box);
|
|
728
|
+
return `<span class="asst-scene${mini ? ' is-mini' : ''}${className ? ` ${className}` : ''}" data-scene="${id}"${live ? ' data-live=""' : ''} aria-hidden="true">${body}</span>`;
|
|
583
729
|
}
|
|
@@ -459,15 +459,15 @@ export function injectAssistantSidepanelStyles() {
|
|
|
459
459
|
|
|
460
460
|
body.ui-interacting .asp-placeholder .asp-mark,
|
|
461
461
|
body.ui-interacting .asp-placeholder-track::after,
|
|
462
|
-
body.ui-interacting .asp-status .syna-cameo-
|
|
463
|
-
body.ui-interacting .asp-session-pill .syna-cameo-
|
|
462
|
+
body.ui-interacting .asp-status .syna-cameo-eyes,
|
|
463
|
+
body.ui-interacting .asp-session-pill .syna-cameo-eyes { animation-play-state: paused !important; }
|
|
464
464
|
|
|
465
465
|
@media (prefers-reduced-motion: reduce) {
|
|
466
466
|
.assistant-panel { transition: opacity 0.15s ease; }
|
|
467
467
|
.asp-placeholder .asp-mark,
|
|
468
468
|
.asp-placeholder-track::after,
|
|
469
|
-
.asp-status .syna-cameo-
|
|
470
|
-
.asp-session-pill .syna-cameo-
|
|
469
|
+
.asp-status .syna-cameo-eyes,
|
|
470
|
+
.asp-session-pill .syna-cameo-eyes,
|
|
471
471
|
.assistant-panel .asst-send::before { animation: none !important; }
|
|
472
472
|
.asp-placeholder-track::after { left: 31%; }
|
|
473
473
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Assistant — computed styles read once
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// A value the stylesheet decides and the renderer needs on a hot path (the
|
|
5
|
+
// rack's --rk-cut, where a long subject is cut; its bubble line's
|
|
6
|
+
// white-space), read once and kept until the renderer says it may have
|
|
7
|
+
// changed. getComputedStyle() on a frame's path forces a style recalculation
|
|
8
|
+
// of everything the frame dirtied (a rig's SVG, a scene); these values change
|
|
9
|
+
// only when a container query flips, which happens only when the stage's width
|
|
10
|
+
// changes, and the renderer's ResizeObserver sees every such change and calls
|
|
11
|
+
// invalidate().
|
|
12
|
+
//
|
|
13
|
+
// createStyleCache({ read }?) → { get(el, prop), invalidate(el?), reads() }
|
|
14
|
+
//
|
|
15
|
+
// `read(el, prop)` defaults to getComputedStyle(el).getPropertyValue(prop)
|
|
16
|
+
// (trimmed); a read that throws counts as ''. DOM-free: tested in Node.
|
|
17
|
+
|
|
18
|
+
const defaultRead = (el, prop) => globalThis.getComputedStyle(el).getPropertyValue(prop);
|
|
19
|
+
|
|
20
|
+
export function createStyleCache({ read = defaultRead } = {}) {
|
|
21
|
+
let values = new WeakMap(); // element → Map(prop → value)
|
|
22
|
+
let count = 0;
|
|
23
|
+
return {
|
|
24
|
+
/** The element's computed `prop` (a custom property or a longhand), read once until invalidated. */
|
|
25
|
+
get(el, prop) {
|
|
26
|
+
if (!el) return '';
|
|
27
|
+
let props = values.get(el);
|
|
28
|
+
if (!props) { props = new Map(); values.set(el, props); }
|
|
29
|
+
if (props.has(prop)) return props.get(prop);
|
|
30
|
+
count += 1;
|
|
31
|
+
let value = '';
|
|
32
|
+
try { value = String(read(el, prop) ?? '').trim(); } catch { value = ''; }
|
|
33
|
+
props.set(prop, value);
|
|
34
|
+
return value;
|
|
35
|
+
},
|
|
36
|
+
/** Forget one element's values, or every value (no argument). */
|
|
37
|
+
invalidate(el) {
|
|
38
|
+
if (el) values.delete(el);
|
|
39
|
+
else values = new WeakMap();
|
|
40
|
+
},
|
|
41
|
+
/** How many real reads were made (tests, the idle-CPU suite). */
|
|
42
|
+
reads: () => count,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -252,9 +252,13 @@ export function injectAssistantStyles() {
|
|
|
252
252
|
.asst-count.attention { color: var(--asst-warn); box-shadow: inset 0 0 0 1px var(--asst-pill-warn-ring); }
|
|
253
253
|
/* The running cameo: the mascot's two pill eyes, glancing ±1.2px every 1.4s.
|
|
254
254
|
One form everywhere (the header status, the tray pill, run pills):
|
|
255
|
-
mascotCameoSvg() from synabun-mascot.js, a 10×7 SVG.
|
|
255
|
+
mascotCameoSvg() from synabun-mascot.js, a 10×7 SVG in two spans. The
|
|
256
|
+
glance moves the HTML box around the eyes (.syna-cameo-eyes), along the
|
|
257
|
+
pair's −12° lean (1.2 of the SVG's 10×7 units: 11.738% and 3.564% of the
|
|
258
|
+
box), never a part of the SVG: a box's transform runs on the compositor,
|
|
259
|
+
and a frame the page draws anyway restyles one span and lays nothing out. */
|
|
256
260
|
.syna-cameo { display: inline-block; flex-shrink: 0; overflow: visible; color: var(--asst-brand); }
|
|
257
|
-
.syna-cameo-
|
|
261
|
+
.syna-cameo-eyes { display: block; animation: asst-cameo 2.8s var(--asst-ease-emphasized) infinite; }
|
|
258
262
|
|
|
259
263
|
/* ── Toolbar: flush under the terminal tabs ─────────────────────────
|
|
260
264
|
Edge to edge like the tab bar above it (same hairline), no card and no
|
|
@@ -1088,6 +1092,8 @@ export function injectAssistantStyles() {
|
|
|
1088
1092
|
border-radius: 50%;
|
|
1089
1093
|
animation: asst-spin .8s linear infinite;
|
|
1090
1094
|
}
|
|
1095
|
+
/* A spinner nobody can see never runs (a hidden holder still restyles a running loop every frame). */
|
|
1096
|
+
[hidden] .asst-spinner, .asst-spinner[hidden], [inert] .asst-spinner { animation: none; }
|
|
1091
1097
|
.asst-computer-activity { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 7px 8px 7px 12px; border-bottom: 1px solid var(--asst-line); }
|
|
1092
1098
|
.asst-live-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: 50%; background: var(--asst-live); animation: asst-live 1.4s var(--asst-ease-standard) infinite; }
|
|
1093
1099
|
.asst-computer-activity[data-state="paused_user"] .asst-live-dot { background: var(--asst-warn); animation: none; }
|
|
@@ -2063,10 +2069,24 @@ export function injectAssistantStyles() {
|
|
|
2063
2069
|
whole Assistant holds still: the component, its menus and modals, the
|
|
2064
2070
|
sidepanel frame and its header (.asp-*), and the tray pills. */
|
|
2065
2071
|
@keyframes asst-live { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.8); } }
|
|
2066
|
-
@keyframes asst-cameo { 0%, 42% { transform:
|
|
2072
|
+
@keyframes asst-cameo { 0%, 42% { transform: translate(-11.738%, 3.564%); } 50%, 92% { transform: translate(11.738%, -3.564%); } 100% { transform: translate(-11.738%, 3.564%); } }
|
|
2067
2073
|
@keyframes asst-spin { to { transform: rotate(360deg); } }
|
|
2068
2074
|
|
|
2069
|
-
|
|
2075
|
+
/* A moving box gets its layer up front (only where motion is allowed; nothing is promoted under reduced motion). */
|
|
2076
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2077
|
+
.syna-cameo-eyes { will-change: transform; }
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
body.ui-interacting :is(.asst-live-dot, .asst-spinner, .asst-computer-dot, .syna-cameo-eyes) { animation-play-state: paused !important; }
|
|
2081
|
+
|
|
2082
|
+
/* Off screen, nothing loops. A holder the renderer saw leave the viewport (asst-motion-gate.js marks it
|
|
2083
|
+
data-offscreen: a run card, a rack or a route line scrolled out of its transcript) and a closed side
|
|
2084
|
+
panel (it stays in the page, moved out and faded: .assistant-panel without .open) pause every loop
|
|
2085
|
+
under them; each picks up where it stopped when it is back. Every frame the page draws restyles each
|
|
2086
|
+
running animation, composited or not, so a loop nobody sees is never free. */
|
|
2087
|
+
[data-offscreen], [data-offscreen]::before, [data-offscreen]::after,
|
|
2088
|
+
[data-offscreen] *, [data-offscreen] *::before, [data-offscreen] *::after,
|
|
2089
|
+
.assistant-panel:not(.open) *, .assistant-panel:not(.open) *::before, .assistant-panel:not(.open) *::after { animation-play-state: paused !important; }
|
|
2070
2090
|
body.ui-interacting .asst-route-line[data-live] .asst-route-icon,
|
|
2071
2091
|
body.ui-interacting .asst-route-line[data-live] .asst-route-sweep::after { animation-play-state: paused !important; }
|
|
2072
2092
|
|
|
@@ -16,6 +16,16 @@ import {
|
|
|
16
16
|
const ICON_CHECK = '<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5 6.5-7"/></svg>';
|
|
17
17
|
const ICON_X = '<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>';
|
|
18
18
|
|
|
19
|
+
// A waiting card's spinning border (.cp-spin-ring in ui-claude-panel.js): a box the compositor turns
|
|
20
|
+
// under the ring's mask while the card is .active-perm. The card's first child.
|
|
21
|
+
function spinRing() {
|
|
22
|
+
const ring = document.createElement('span');
|
|
23
|
+
ring.className = 'cp-spin-ring';
|
|
24
|
+
ring.setAttribute('aria-hidden', 'true');
|
|
25
|
+
ring.appendChild(document.createElement('i'));
|
|
26
|
+
return ring;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
// Modes, rule suggestions and the prompt's context are decided in
|
|
20
30
|
// cp-permission-model.js (DOM-free, tested); this file renders them.
|
|
21
31
|
export const PERMISSION_MODES = ALL_PERMISSION_MODES;
|
|
@@ -100,6 +110,7 @@ export function renderPermissionCard(tab, requestId, req, hooks) {
|
|
|
100
110
|
const card = document.createElement('div');
|
|
101
111
|
card.className = 'perm-card active-perm cp-perm-card';
|
|
102
112
|
card.dataset.requestId = requestId;
|
|
113
|
+
card.appendChild(spinRing());
|
|
103
114
|
if (ctx.toolUseId) card.dataset.toolUseId = ctx.toolUseId;
|
|
104
115
|
|
|
105
116
|
const hdr = document.createElement('div');
|
|
@@ -283,6 +294,7 @@ export function renderElicitationCard(tab, requestId, req, hooks) {
|
|
|
283
294
|
const card = document.createElement('div');
|
|
284
295
|
card.className = 'perm-card active-perm cp-perm-card cp-elicit-card';
|
|
285
296
|
card.dataset.requestId = requestId;
|
|
297
|
+
card.appendChild(spinRing());
|
|
286
298
|
if (req.elicitation_id) card.dataset.elicitationId = req.elicitation_id;
|
|
287
299
|
|
|
288
300
|
const hdr = document.createElement('div');
|