|
984 | 984 | } |
985 | 985 | #composer.focused { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } |
986 | 986 |
|
| 987 | + /* ---- the working aura: a light that travels round the message box while LevelCode works ---- |
| 988 | + Thinking, streaming, running a command, verifying: a short bright arc circles the box, trailing a soft |
| 989 | + glow over a faint whole ring. Waiting on you (a command to approve, a question to answer) it stops and |
| 990 | + dims to a thin ring with no glow — paused, because nothing happens until you act. Off: nothing at all. |
| 991 | + Two rings on the box's own edge, cut to a band by a mask so the box's inside is untouched: ::after is |
| 992 | + the sharp line, ::before the blurred glow under it. Both turn on one angle, so the glow never lags. |
| 993 | + Colours come from the theme: the progress colour while working (the theme's own "busy"), the focus |
| 994 | + colour while waiting. The speed is set per box (syncAura) so a wide panel does not look frantic. */ |
| 995 | + @property --aura-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; } |
| 996 | + @keyframes aura-turn { to { --aura-angle: 360deg; } } |
| 997 | + #composer { |
| 998 | + position: relative; |
| 999 | + --aura-color: var(--vscode-progressBar-background, var(--accent)); |
| 1000 | + --aura-head: color-mix(in srgb, var(--aura-color) 62%, var(--vscode-foreground, #fff)); |
| 1001 | + } |
| 1002 | + #composer::before, #composer::after { |
| 1003 | + content: ''; position: absolute; border-radius: inherit; pointer-events: none; opacity: 0; |
| 1004 | + transition: opacity .4s ease; |
| 1005 | + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; |
| 1006 | + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; |
| 1007 | + } |
| 1008 | + #composer::before { |
| 1009 | + inset: -6px; padding: 6px; filter: blur(5px); |
| 1010 | + background: conic-gradient(from var(--aura-angle), transparent 0deg 255deg, |
| 1011 | + color-mix(in srgb, var(--aura-color) 38%, transparent) 318deg, color-mix(in srgb, var(--aura-head) 80%, transparent) 354deg, transparent 360deg); |
| 1012 | + } |
| 1013 | + #composer::after { |
| 1014 | + inset: -1px; padding: 2px; |
| 1015 | + /* A faint whole ring (the box is busy) with the bright arc travelling over it. */ |
| 1016 | + background: conic-gradient(from var(--aura-angle), color-mix(in srgb, var(--aura-color) 22%, transparent) 0deg 262deg, |
| 1017 | + color-mix(in srgb, var(--aura-color) 60%, transparent) 320deg, var(--aura-head) 355deg, color-mix(in srgb, var(--aura-color) 22%, transparent) 360deg); |
| 1018 | + } |
| 1019 | + body.aura-working #composer::before, body.aura-working #composer::after { opacity: 1; animation: aura-turn var(--aura-turn, 2.4s) linear infinite; } |
| 1020 | + /* Waiting on you: paused — a thin, dim ring with no glow, weaker than working and than the box's own focus. */ |
| 1021 | + body.aura-waiting #composer::after { opacity: 1; padding: 1px; background: color-mix(in srgb, var(--accent) 45%, transparent); } |
| 1022 | + @media (prefers-reduced-motion: reduce) { |
| 1023 | + /* No motion: working is a whole, bright ring with its glow — still distinct from waiting's dim one. */ |
| 1024 | + body.aura-working #composer::before, body.aura-working #composer::after { animation: none; } |
| 1025 | + body.aura-working #composer::after { background: color-mix(in srgb, var(--aura-color) 75%, transparent); } |
| 1026 | + body.aura-working #composer::before { background: color-mix(in srgb, var(--aura-color) 35%, transparent); } |
| 1027 | + } |
| 1028 | + /* High contrast draws its own borders; a soft glow there is noise. The working bar still says it. */ |
| 1029 | + @media (forced-colors: active) { #composer::before, #composer::after { display: none; } } |
| 1030 | + |
987 | 1031 | #chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; } |
988 | 1032 | .chip { |
989 | 1033 | display: inline-flex; align-items: center; gap: 5px; |
|
3444 | 3488 | } |
3445 | 3489 | // Waiting on you (a command to approve, a question to answer): the step in progress stops spinning. |
3446 | 3490 | function agentParked(){ return !!pendingApproval || !!log.querySelector('.questions:not(.answered)'); } |
| 3491 | + // ── the working aura (the light round the message box; its look is in the CSS) ── |
| 3492 | + // `var`, not `let`: syncAura is reached from syncSendBtn, which can run before this line does, and a |
| 3493 | + // decoration must never stop the chat with a temporal-dead-zone error. |
| 3494 | + var auraEnabled = true; // levelcode.ai.chat.workingAura, from the config message |
| 3495 | + var auraShown = 'off'; |
| 3496 | + /** What the aura shows: off (idle, or turned off), working (LevelCode is busy), waiting (busy, but on you). */ |
| 3497 | + function auraState(isBusy, parked, enabled){ return (!enabled || !isBusy) ? 'off' : parked ? 'waiting' : 'working'; } |
| 3498 | + /** Seconds per turn: grows with the box's perimeter, but slower than it, so a wide panel stays calm. */ |
| 3499 | + function auraDuration(w, h){ |
| 3500 | + const p = 2 * (Math.max(0, w || 0) + Math.max(0, h || 0)); |
| 3501 | + return Math.min(3, Math.max(1.6, 0.55 + 0.06 * Math.sqrt(p))); |
| 3502 | + } |
| 3503 | + function syncAura(){ |
| 3504 | + try { |
| 3505 | + const s = auraState(busy(), agentParked(), auraEnabled); |
| 3506 | + if (s === auraShown) { return; } |
| 3507 | + // The speed is fixed when the light starts: a duration changed mid-turn would only apply on a restart. |
| 3508 | + if (s === 'working') { const r = composer.getBoundingClientRect(); composer.style.setProperty('--aura-turn', auraDuration(r.width, r.height).toFixed(2) + 's'); } |
| 3509 | + auraShown = s; |
| 3510 | + document.body.classList.toggle('aura-working', s === 'working'); |
| 3511 | + document.body.classList.toggle('aura-waiting', s === 'waiting'); |
| 3512 | + } catch (e) { /* before the page is wired up: the next change draws it */ } |
| 3513 | + } |
| 3514 | + // An approval card or a question appearing, or being answered, is a class or a node in the transcript: |
| 3515 | + // watched here, so no card has to remember to tell the aura. |
| 3516 | + new MutationObserver(function(){ if (auraShown !== 'off' || busy()) { syncAura(); } }) |
| 3517 | + .observe(log, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); |
3447 | 3518 | function renderTurnBox(){ |
3448 | 3519 | if (turnBox.parentNode !== log) { log.appendChild(turnBox); } // the log is rebuilt on reset / resume |
3449 | 3520 | const v = turnView(turn, agentParked()); |
|
3670 | 3741 | sendBtn.classList.toggle('stop', stop); |
3671 | 3742 | sendBtn.innerHTML = stop ? '■' : '↵'; // ■ stops the run, ↵ sends (or queues) |
3672 | 3743 | sendBtn.title = stop ? 'Stop' : queueing ? 'Queue — it sends when the agent is ready (Enter)' : 'Send (Enter)'; |
| 3744 | + syncAura(); // busy() changes with the button: a message on its way already counts as LevelCode working |
3673 | 3745 | } |
3674 | 3746 | function updateReviewBar(count, add, del){ |
3675 | 3747 | if (count > 0){ |
|
3811 | 3883 | if (workbarEl){ workbarEl.hidden = !on; } // the working bar is visible for the ENTIRE run |
3812 | 3884 | if (on){ setWork('Working…'); } |
3813 | 3885 | else { setTimeout(drainQueue, 60); } // the run is over: a queued message goes next |
| 3886 | + syncAura(); |
3814 | 3887 | } |
3815 | 3888 | // Shell-style history recall in the composer: ↑ = previous sent message, ↓ = newer (back to your draft). |
3816 | 3889 | // Session-only. In multiline text, ↑/↓ recall only when the caret is on the first/last line (arrows still move otherwise). |
|
5069 | 5142 | shell.style.setProperty('--prose-size', m.proseSize ? m.proseSize + 'px' : ''); |
5070 | 5143 | shell.style.setProperty('--prose-max', m.proseWidth ? m.proseWidth + 'px' : ''); |
5071 | 5144 | } |
| 5145 | + if (typeof m.workingAura === 'boolean'){ auraEnabled = m.workingAura; syncAura(); } |
5072 | 5146 | const label = m.provider === 'claude' ? prettyModel(m.model) : m.model; |
5073 | 5147 | const mEl = document.getElementById('model'); |
5074 | 5148 | mEl.innerHTML = esc(label) + ' <span class="caret">▾</span>'; |
|
0 commit comments