Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions extensions/levelcode-ai/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -3291,13 +3291,15 @@ function sendConfigToWebview() {
// "unconstrained": the way to widen the measure is a large number, not 0.
const proseSize = clampSetting(cfg.get('chat.fontSize', 0), 8, 24);
const proseWidth = clampSetting(cfg.get('chat.proseWidth', 0), 320, 2000);
// The light round the message box while LevelCode works (media/chat.html, "the working aura").
const workingAura = cfg.get('chat.workingAura', true) !== false;
if (providerMode() === 'gateway' && cloudSignedIn) {
const model = gatewayModel();
post({
type: 'config', provider: 'gateway', proseSize, proseWidth, model: gatewayModelLabel(model), modelId: model,
providerLabel: 'LevelCode Cloud', contextLimit: contextLimitFor('openai', capsModel(model)),
gateway: true, plan: cloudPlanName() || 'Free', paid: isPaidCloudPlan(cloudPlanName()),
groupActivity: groupActivity, canSeeImages: supportsVisionForModel('openai', capsModel(model)), maxImages: maxImagesPerMessage()
groupActivity: groupActivity, workingAura, canSeeImages: supportsVisionForModel('openai', capsModel(model)), maxImages: maxImagesPerMessage()
});
return;
}
Expand All @@ -3306,7 +3308,7 @@ function sendConfigToWebview() {
// Carry the model's context window so the footer meter updates the moment the model changes.
// canSeeImages travels with the model so the composer can refuse an attachment BEFORE anything is
// typed and lost, rather than after a send that the provider would reject.
post({ type: 'config', provider: providerId, proseSize, proseWidth, model: activeModel(cfg, providerId), providerLabel: p.label, contextLimit: currentContextLimit(), groupActivity: groupActivity, canSeeImages: supportsVisionForModel(providerId, activeModel(cfg, providerId)), maxImages: maxImagesPerMessage() });
post({ type: 'config', provider: providerId, proseSize, proseWidth, model: activeModel(cfg, providerId), providerLabel: p.label, contextLimit: currentContextLimit(), groupActivity: groupActivity, workingAura, canSeeImages: supportsVisionForModel(providerId, activeModel(cfg, providerId)), maxImages: maxImagesPerMessage() });
}

/**
Expand Down
74 changes: 74 additions & 0 deletions extensions/levelcode-ai/media/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,50 @@
}
#composer.focused { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* ---- the working aura: a light that travels round the message box while LevelCode works ----
Thinking, streaming, running a command, verifying: a short bright arc circles the box, trailing a soft
glow over a faint whole ring. Waiting on you (a command to approve, a question to answer) it stops and
dims to a thin ring with no glow — paused, because nothing happens until you act. Off: nothing at all.
Two rings on the box's own edge, cut to a band by a mask so the box's inside is untouched: ::after is
the sharp line, ::before the blurred glow under it. Both turn on one angle, so the glow never lags.
Colours come from the theme: the progress colour while working (the theme's own "busy"), the focus
colour while waiting. The speed is set per box (syncAura) so a wide panel does not look frantic. */
@property --aura-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
@keyframes aura-turn { to { --aura-angle: 360deg; } }
#composer {
position: relative;
--aura-color: var(--vscode-progressBar-background, var(--accent));
--aura-head: color-mix(in srgb, var(--aura-color) 62%, var(--vscode-foreground, #fff));
}
#composer::before, #composer::after {
content: ''; position: absolute; border-radius: inherit; pointer-events: none; opacity: 0;
transition: opacity .4s ease;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude;
}
#composer::before {
inset: -6px; padding: 6px; filter: blur(5px);
background: conic-gradient(from var(--aura-angle), transparent 0deg 255deg,
color-mix(in srgb, var(--aura-color) 38%, transparent) 318deg, color-mix(in srgb, var(--aura-head) 80%, transparent) 354deg, transparent 360deg);
}
#composer::after {
inset: -1px; padding: 2px;
/* A faint whole ring (the box is busy) with the bright arc travelling over it. */
background: conic-gradient(from var(--aura-angle), color-mix(in srgb, var(--aura-color) 22%, transparent) 0deg 262deg,
color-mix(in srgb, var(--aura-color) 60%, transparent) 320deg, var(--aura-head) 355deg, color-mix(in srgb, var(--aura-color) 22%, transparent) 360deg);
}
body.aura-working #composer::before, body.aura-working #composer::after { opacity: 1; animation: aura-turn var(--aura-turn, 2.4s) linear infinite; }
/* Waiting on you: paused — a thin, dim ring with no glow, weaker than working and than the box's own focus. */
body.aura-waiting #composer::after { opacity: 1; padding: 1px; background: color-mix(in srgb, var(--accent) 45%, transparent); }
@media (prefers-reduced-motion: reduce) {
/* No motion: working is a whole, bright ring with its glow — still distinct from waiting's dim one. */
body.aura-working #composer::before, body.aura-working #composer::after { animation: none; }
body.aura-working #composer::after { background: color-mix(in srgb, var(--aura-color) 75%, transparent); }
body.aura-working #composer::before { background: color-mix(in srgb, var(--aura-color) 35%, transparent); }
}
/* High contrast draws its own borders; a soft glow there is noise. The working bar still says it. */
@media (forced-colors: active) { #composer::before, #composer::after { display: none; } }

#chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.chip {
display: inline-flex; align-items: center; gap: 5px;
Expand Down Expand Up @@ -3444,6 +3488,33 @@
}
// Waiting on you (a command to approve, a question to answer): the step in progress stops spinning.
function agentParked(){ return !!pendingApproval || !!log.querySelector('.questions:not(.answered)'); }
// ── the working aura (the light round the message box; its look is in the CSS) ──
// `var`, not `let`: syncAura is reached from syncSendBtn, which can run before this line does, and a
// decoration must never stop the chat with a temporal-dead-zone error.
var auraEnabled = true; // levelcode.ai.chat.workingAura, from the config message
var auraShown = 'off';
/** What the aura shows: off (idle, or turned off), working (LevelCode is busy), waiting (busy, but on you). */
function auraState(isBusy, parked, enabled){ return (!enabled || !isBusy) ? 'off' : parked ? 'waiting' : 'working'; }
/** Seconds per turn: grows with the box's perimeter, but slower than it, so a wide panel stays calm. */
function auraDuration(w, h){
const p = 2 * (Math.max(0, w || 0) + Math.max(0, h || 0));
return Math.min(3, Math.max(1.6, 0.55 + 0.06 * Math.sqrt(p)));
}
function syncAura(){
try {
const s = auraState(busy(), agentParked(), auraEnabled);
if (s === auraShown) { return; }
// The speed is fixed when the light starts: a duration changed mid-turn would only apply on a restart.
if (s === 'working') { const r = composer.getBoundingClientRect(); composer.style.setProperty('--aura-turn', auraDuration(r.width, r.height).toFixed(2) + 's'); }
auraShown = s;
document.body.classList.toggle('aura-working', s === 'working');
document.body.classList.toggle('aura-waiting', s === 'waiting');
} catch (e) { /* before the page is wired up: the next change draws it */ }
}
// An approval card or a question appearing, or being answered, is a class or a node in the transcript:
// watched here, so no card has to remember to tell the aura.
new MutationObserver(function(){ if (auraShown !== 'off' || busy()) { syncAura(); } })
.observe(log, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
function renderTurnBox(){
if (turnBox.parentNode !== log) { log.appendChild(turnBox); } // the log is rebuilt on reset / resume
const v = turnView(turn, agentParked());
Expand Down Expand Up @@ -3670,6 +3741,7 @@
sendBtn.classList.toggle('stop', stop);
sendBtn.innerHTML = stop ? '&#9632;' : '&#8629;'; // ■ stops the run, ↵ sends (or queues)
sendBtn.title = stop ? 'Stop' : queueing ? 'Queue — it sends when the agent is ready (Enter)' : 'Send (Enter)';
syncAura(); // busy() changes with the button: a message on its way already counts as LevelCode working
}
function updateReviewBar(count, add, del){
if (count > 0){
Expand Down Expand Up @@ -3811,6 +3883,7 @@
if (workbarEl){ workbarEl.hidden = !on; } // the working bar is visible for the ENTIRE run
if (on){ setWork('Working…'); }
else { setTimeout(drainQueue, 60); } // the run is over: a queued message goes next
syncAura();
}
// Shell-style history recall in the composer: ↑ = previous sent message, ↓ = newer (back to your draft).
// Session-only. In multiline text, ↑/↓ recall only when the caret is on the first/last line (arrows still move otherwise).
Expand Down Expand Up @@ -5069,6 +5142,7 @@
shell.style.setProperty('--prose-size', m.proseSize ? m.proseSize + 'px' : '');
shell.style.setProperty('--prose-max', m.proseWidth ? m.proseWidth + 'px' : '');
}
if (typeof m.workingAura === 'boolean'){ auraEnabled = m.workingAura; syncAura(); }
const label = m.provider === 'claude' ? prettyModel(m.model) : m.model;
const mEl = document.getElementById('model');
mEl.innerHTML = esc(label) + ' <span class="caret">▾</span>';
Expand Down
6 changes: 6 additions & 0 deletions extensions/levelcode-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@
"default": true,
"description": "Fold consecutive agent actions (reads, edits, commands) into one collapsible activity card in the chat — the header shows the live step while running and a summary when done. Turn off for the flat, ungrouped timeline."
},
"levelcode.ai.chat.workingAura": {
"type": "boolean",
"scope": "application",
"default": true,
"description": "Show a light that travels round the message box while LevelCode works — thinking, writing, running a command, checking its edits. While it waits on you (a command to approve, a question to answer) the light stops and stays as a still ring. With reduced motion it is a still ring throughout. Turn off for no light at all; the working line above the message box still shows what is happening."
},
"levelcode.cloud.endpoint": {
"type": "string",
"default": "https://levelcode.ai",
Expand Down
Loading
Loading