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
37 changes: 19 additions & 18 deletions extensions/levelcode-ai/media/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -985,45 +985,46 @@
#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.
Thinking, streaming, running a command, verifying: a short arc of light slides along the box's own
border, a hairline with a faint soft edge. Waiting on you (a command to approve, a question to answer)
it stops and becomes a dim, still border, because nothing happens until you act. Off: nothing at all.
Two rings on the box's border, cut to a band by a mask so the box's inside is untouched: ::after is the
1px line, ::before a 2px blurred band under it. Both turn on one angle, so the soft edge never lags.
No ring of its own behind the arc: the box's border is the track, and while the light turns the focus
ring steps back (below), as in VS Code's chat, or a hairline in the accent colour is lost on it.
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;
content: ''; position: absolute; inset: -1px; 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);
padding: 2px; filter: blur(1.5px);
background: conic-gradient(from var(--aura-angle), transparent 0deg 285deg,
color-mix(in srgb, var(--aura-color) 45%, transparent) 335deg, 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);
padding: 1px;
background: conic-gradient(from var(--aura-angle), transparent 0deg 275deg,
color-mix(in srgb, var(--aura-color) 55%, transparent) 318deg, var(--aura-color) 338deg, 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); }
body.aura-working #composer.focused { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); box-shadow: none; }
/* Waiting on you: paused — a dim, still border with no glow, weaker than working and than the box's own focus. */
body.aura-waiting #composer::after { opacity: 1; 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. */
/* No motion: working is a whole, still border in the progress colour — brighter than 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); }
body.aura-working #composer::before { opacity: 0; }
}
/* 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; } }
Expand Down
2 changes: 1 addition & 1 deletion extensions/levelcode-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@
"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."
"description": "Show a thin line of light that travels round the message box's border 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 the border stays dimly lit. With reduced motion the border is lit without moving. Turn off for no light at all; the working line above the message box still shows what is happening."
},
"levelcode.cloud.endpoint": {
"type": "string",
Expand Down
12 changes: 12 additions & 0 deletions extensions/levelcode-ai/scripts/working-aura-editor-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ async function main() {
const a = w.getComputedStyle(c, '::after'), b = w.getComputedStyle(c, '::before');
return { cls: ['aura-working', 'aura-waiting'].filter((x) => d.body.classList.contains(x)), lineOpacity: +a.opacity, glowOpacity: +b.opacity,
anim: a.animationName, angle: a.getPropertyValue('--aura-angle').trim(), turn: c.style.getPropertyValue('--aura-turn'), bg: a.backgroundImage.slice(0, 40) }; })()`);
// The box itself: whether it has the focus, and the focus ring it draws.
const box = () => evalIn(chat, `(() => { const d = ${D}, w = ${W}, c = d.getElementById('composer'), s = w.getComputedStyle(c);
return { focused: c.classList.contains('focused'), shadow: s.boxShadow, border: s.borderTopColor }; })()`);
await front();

// ---- working ----
Expand All @@ -210,11 +213,20 @@ async function main() {
await shot('aura-1-working');
await sleep(800);
await shot('aura-1-working-later'); // the same run 0.8 s on: the light has moved round the box
// In real use the message box keeps the focus after Enter (here the key prompt took it). Its accent
// focus ring would swallow a 1px line, so while the light turns the ring steps back.
await evalIn(chat, `(() => { ${D}.getElementById('input').focus(); return true; })()`);
await sleep(300);
const wf = await box();
check('…and with the box focused, its focus ring steps back so the line shows', wf.focused && wf.shadow === 'none', JSON.stringify(wf));
await shot('aura-1-working-focused');
release();
await until('the run to end', () => evalIn(chat, `(() => /Done: thought/.test(${D}.getElementById('log').innerText))()`), 30000);
await sleep(700);
const idle = await ring();
check('idle: when the run is over the ring is gone', idle.cls.length === 0 && idle.lineOpacity === 0 && idle.glowOpacity === 0, JSON.stringify(idle));
const idleBox = await box();
check('…and a focused box has its full focus ring back', idleBox.focused && idleBox.shadow !== 'none', JSON.stringify(idleBox));

// ---- waiting ----
await send('run something');
Expand Down
28 changes: 27 additions & 1 deletion extensions/levelcode-ai/test/workingAura.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,35 @@ test('CSS: colours come from the theme — progress while working, focus while w
assert.match(rule(' #composer'), /--aura-color: var\(--vscode-progressBar-background, var\(--accent\)\)/);
assert.match(rule('body.aura-waiting #composer::after'), /var\(--accent\)/);
assert.ok(!/body\.aura-waiting #composer::before/.test(auraCss), 'waiting has a glow: it should read as paused, not busy');
const colours = auraCss.replace(/linear-gradient\(#000 0 0\)/g, '').replace(/var\(--vscode-foreground, #fff\)/g, '');
const colours = auraCss.replace(/linear-gradient\(#000 0 0\)/g, '');
assert.ok(!/#[0-9a-f]{3,8}\b|rgba?\(/i.test(colours), 'a fixed colour in the aura');
});
test('CSS: thin and quiet — a 1px line on the border, a 2px soft edge, a short arc, no ring or white head of its own', () => {
assert.match(rule('#composer::before, #composer::after'), /inset: -1px;/, 'both rings sit on the box\'s 1px border');
assert.match(rule('#composer::after'), /padding: 1px;/, 'the line is a hairline');
const glow = rule('#composer::before');
assert.ok(Number((glow.match(/padding: ([\d.]+)px/) || [])[1]) <= 2, 'the soft edge is at most 2px wide');
assert.ok(Number((glow.match(/filter: blur\(([\d.]+)px\)/) || [])[1]) <= 2, 'and blurred at most 2px');
for (const sel of ['#composer::before', '#composer::after']) {
const dark = rule(sel).match(/conic-gradient\(from var\(--aura-angle\), transparent 0deg (\d+)deg/);
assert.ok(dark, sel + ': the gradient is clear round the box, not a ring of its own');
assert.ok(360 - Number(dark[1]) <= 90, sel + ': the lit arc is at most a quarter turn');
}
assert.ok(!/--vscode-foreground/.test(auraCss), 'the arc is the theme colour, not mixed toward white');
const reduced = css.slice(css.indexOf('@media (prefers-reduced-motion: reduce) {\n /* No motion'));
assert.match(reduced.slice(0, 600), /body\.aura-working #composer::before \{ opacity: 0; \}/, 'reduced motion: a still border, no glow');
});
test('CSS: while the light turns the focus ring steps back — a hairline on an accent border is lost (desktop and browser)', () => {
const desk = rule('body.aura-working #composer.focused');
assert.match(desk, /box-shadow: none/);
assert.match(desk, /border-color: color-mix\(in srgb, var\(--accent\) 40%, var\(--border\)\)/);
const skin = fs.readFileSync(path.join(ROOT, '..', '..', 'web', 'ai-extension', 'skin', 'chat.css'), 'utf8');
const i = skin.indexOf('body.lc-web.aura-working #composer:focus-within, body.lc-web.aura-working #composer.focused {');
assert.ok(i >= 0, 'the browser skin\'s own focus halo (3px) is not stepped back while working');
const block = skin.slice(i, skin.indexOf('}', i));
assert.ok(!/0 0 0 3px/.test(block), 'the 3px halo is still drawn while working');
assert.ok(skin.indexOf('body.lc-web #composer:focus-within, body.lc-web #composer.focused {') < i, 'it must come after the halo it steps back');
});

// ---- the wiring -------------------------------------------------------------------------------------
test('WIRING: every change of "busy" reaches the aura — the run starting and ending, a message on its way', () => {
Expand Down
6 changes: 6 additions & 0 deletions web/ai-extension/skin/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ body.lc-web #composer:focus-within, body.lc-web #composer.focused {
border-color: color-mix(in srgb, var(--lcw-accent-text) 80%, var(--lcw-line-strong));
box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 10px 32px rgba(0, 0, 0, .18), 0 0 0 3px color-mix(in srgb, var(--lcw-accent-text) 26%, transparent);
}
/* While the working light turns (chat.html, "the working aura") the focus halo steps back, as on the desktop:
the light is a hairline on the border, and a 3px accent halo round it would swallow it. */
body.lc-web.aura-working #composer:focus-within, body.lc-web.aura-working #composer.focused {
border-color: color-mix(in srgb, var(--lcw-accent-text) 35%, var(--lcw-line-strong));
box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 10px 32px rgba(0, 0, 0, .14);
}
body.lc-web textarea#input { font-size: 15px; line-height: 1.5; min-height: 46px; background: transparent; border: 0; }
body.lc-web #toolbar .tbtn { border-radius: 999px; }
body.lc-web #toolbar .pill { border-radius: 999px; background: transparent; border-color: transparent; padding: 3px 10px; color: var(--lcw-dim); }
Expand Down
Loading