From 1d2c5553467194330c0d258e344f426d2c87b410 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Mon, 19 Oct 2026 23:59:48 -0400 Subject: [PATCH] style(chat): a thinner, quieter working aura, and the focus ring steps back while it turns The first version read as too big and too noisy: a 2px line over a faint ring all the way round, a 6px blurred halo, and an arc whose head was mixed toward white. Now, in VS Code chat's proportions: a 1px line on the box's own border and a 2px band blurred 1.5px under it, an arc of at most a quarter turn in the plain theme colour, and no ring of its own: the box's border is the track. Waiting on you is a dim, still border; reduced motion is a still 1px border with no glow. The speed is unchanged. In real use the message box keeps the focus after Enter, and its accent focus ring would swallow a 1px line. While the light turns the ring steps back, as in VS Code: on the desktop a 40% accent border and no shadow, in the browser skin no 3px halo. It returns when the run ends. Tested: workingAura.test.js pins the thin design (1px line, a glow of at most 2px, a clear track, the arc's length, no white head, the focus step-back on desktop and in the skin); 40 one-edit mutations, each fails it. The editor check now focuses the box mid-run (the first one never did: the key prompt had the focus) and checks the ring steps back and comes back: 11/11 in the dark theme and in LevelCode One Light. The skinned browser page in the web themes, focused and not: 16/16. scripts/test-extensions.sh: 137 files. --- extensions/levelcode-ai/media/chat.html | 37 ++++++++++--------- extensions/levelcode-ai/package.json | 2 +- .../scripts/working-aura-editor-check.js | 12 ++++++ .../levelcode-ai/test/workingAura.test.js | 28 +++++++++++++- web/ai-extension/skin/chat.css | 6 +++ 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index 608c71b..abf9bc4 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -985,11 +985,13 @@ #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: ''; inherits: false; initial-value: 0deg; } @@ -997,33 +999,32 @@ #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; } } diff --git a/extensions/levelcode-ai/package.json b/extensions/levelcode-ai/package.json index b6a72b3..d24f9dd 100644 --- a/extensions/levelcode-ai/package.json +++ b/extensions/levelcode-ai/package.json @@ -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", diff --git a/extensions/levelcode-ai/scripts/working-aura-editor-check.js b/extensions/levelcode-ai/scripts/working-aura-editor-check.js index c26545d..ddfff56 100644 --- a/extensions/levelcode-ai/scripts/working-aura-editor-check.js +++ b/extensions/levelcode-ai/scripts/working-aura-editor-check.js @@ -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 ---- @@ -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'); diff --git a/extensions/levelcode-ai/test/workingAura.test.js b/extensions/levelcode-ai/test/workingAura.test.js index 599dd44..2a6d8aa 100644 --- a/extensions/levelcode-ai/test/workingAura.test.js +++ b/extensions/levelcode-ai/test/workingAura.test.js @@ -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', () => { diff --git a/web/ai-extension/skin/chat.css b/web/ai-extension/skin/chat.css index c12a136..98d5f6a 100644 --- a/web/ai-extension/skin/chat.css +++ b/web/ai-extension/skin/chat.css @@ -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); }