Skip to content

Commit 39b22c3

Browse files
authored
Merge pull request #117 from levelcodeai/feat/working-aura-thin
style(chat): a thinner, quieter working aura, and the focus ring steps back while it turns
2 parents 271137e + 1d2c555 commit 39b22c3

5 files changed

Lines changed: 65 additions & 20 deletions

File tree

‎extensions/levelcode-ai/media/chat.html‎

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -985,45 +985,46 @@
985985
#composer.focused { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
986986

987987
/* ---- 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.
988+
Thinking, streaming, running a command, verifying: a short arc of light slides along the box's own
989+
border, a hairline with a faint soft edge. Waiting on you (a command to approve, a question to answer)
990+
it stops and becomes a dim, still border, because nothing happens until you act. Off: nothing at all.
991+
Two rings on the box's border, cut to a band by a mask so the box's inside is untouched: ::after is the
992+
1px line, ::before a 2px blurred band under it. Both turn on one angle, so the soft edge never lags.
993+
No ring of its own behind the arc: the box's border is the track, and while the light turns the focus
994+
ring steps back (below), as in VS Code's chat, or a hairline in the accent colour is lost on it.
993995
Colours come from the theme: the progress colour while working (the theme's own "busy"), the focus
994996
colour while waiting. The speed is set per box (syncAura) so a wide panel does not look frantic. */
995997
@property --aura-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
996998
@keyframes aura-turn { to { --aura-angle: 360deg; } }
997999
#composer {
9981000
position: relative;
9991001
--aura-color: var(--vscode-progressBar-background, var(--accent));
1000-
--aura-head: color-mix(in srgb, var(--aura-color) 62%, var(--vscode-foreground, #fff));
10011002
}
10021003
#composer::before, #composer::after {
1003-
content: ''; position: absolute; border-radius: inherit; pointer-events: none; opacity: 0;
1004+
content: ''; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none; opacity: 0;
10041005
transition: opacity .4s ease;
10051006
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor;
10061007
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude;
10071008
}
10081009
#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);
1010+
padding: 2px; filter: blur(1.5px);
1011+
background: conic-gradient(from var(--aura-angle), transparent 0deg 285deg,
1012+
color-mix(in srgb, var(--aura-color) 45%, transparent) 335deg, transparent 360deg);
10121013
}
10131014
#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);
1015+
padding: 1px;
1016+
background: conic-gradient(from var(--aura-angle), transparent 0deg 275deg,
1017+
color-mix(in srgb, var(--aura-color) 55%, transparent) 318deg, var(--aura-color) 338deg, transparent 360deg);
10181018
}
10191019
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); }
1020+
body.aura-working #composer.focused { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); box-shadow: none; }
1021+
/* Waiting on you: paused — a dim, still border with no glow, weaker than working and than the box's own focus. */
1022+
body.aura-waiting #composer::after { opacity: 1; background: color-mix(in srgb, var(--accent) 45%, transparent); }
10221023
@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+
/* No motion: working is a whole, still border in the progress colour — brighter than waiting's dim one. */
10241025
body.aura-working #composer::before, body.aura-working #composer::after { animation: none; }
10251026
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+
body.aura-working #composer::before { opacity: 0; }
10271028
}
10281029
/* High contrast draws its own borders; a soft glow there is noise. The working bar still says it. */
10291030
@media (forced-colors: active) { #composer::before, #composer::after { display: none; } }

‎extensions/levelcode-ai/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@
456456
"type": "boolean",
457457
"scope": "application",
458458
"default": true,
459-
"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."
459+
"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."
460460
},
461461
"levelcode.cloud.endpoint": {
462462
"type": "string",

‎extensions/levelcode-ai/scripts/working-aura-editor-check.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ async function main() {
184184
const a = w.getComputedStyle(c, '::after'), b = w.getComputedStyle(c, '::before');
185185
return { cls: ['aura-working', 'aura-waiting'].filter((x) => d.body.classList.contains(x)), lineOpacity: +a.opacity, glowOpacity: +b.opacity,
186186
anim: a.animationName, angle: a.getPropertyValue('--aura-angle').trim(), turn: c.style.getPropertyValue('--aura-turn'), bg: a.backgroundImage.slice(0, 40) }; })()`);
187+
// The box itself: whether it has the focus, and the focus ring it draws.
188+
const box = () => evalIn(chat, `(() => { const d = ${D}, w = ${W}, c = d.getElementById('composer'), s = w.getComputedStyle(c);
189+
return { focused: c.classList.contains('focused'), shadow: s.boxShadow, border: s.borderTopColor }; })()`);
187190
await front();
188191

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

219231
// ---- waiting ----
220232
await send('run something');

‎extensions/levelcode-ai/test/workingAura.test.js‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,35 @@ test('CSS: colours come from the theme — progress while working, focus while w
154154
assert.match(rule(' #composer'), /--aura-color: var\(--vscode-progressBar-background, var\(--accent\)\)/);
155155
assert.match(rule('body.aura-waiting #composer::after'), /var\(--accent\)/);
156156
assert.ok(!/body\.aura-waiting #composer::before/.test(auraCss), 'waiting has a glow: it should read as paused, not busy');
157-
const colours = auraCss.replace(/linear-gradient\(#000 0 0\)/g, '').replace(/var\(--vscode-foreground, #fff\)/g, '');
157+
const colours = auraCss.replace(/linear-gradient\(#000 0 0\)/g, '');
158158
assert.ok(!/#[0-9a-f]{3,8}\b|rgba?\(/i.test(colours), 'a fixed colour in the aura');
159159
});
160+
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', () => {
161+
assert.match(rule('#composer::before, #composer::after'), /inset: -1px;/, 'both rings sit on the box\'s 1px border');
162+
assert.match(rule('#composer::after'), /padding: 1px;/, 'the line is a hairline');
163+
const glow = rule('#composer::before');
164+
assert.ok(Number((glow.match(/padding: ([\d.]+)px/) || [])[1]) <= 2, 'the soft edge is at most 2px wide');
165+
assert.ok(Number((glow.match(/filter: blur\(([\d.]+)px\)/) || [])[1]) <= 2, 'and blurred at most 2px');
166+
for (const sel of ['#composer::before', '#composer::after']) {
167+
const dark = rule(sel).match(/conic-gradient\(from var\(--aura-angle\), transparent 0deg (\d+)deg/);
168+
assert.ok(dark, sel + ': the gradient is clear round the box, not a ring of its own');
169+
assert.ok(360 - Number(dark[1]) <= 90, sel + ': the lit arc is at most a quarter turn');
170+
}
171+
assert.ok(!/--vscode-foreground/.test(auraCss), 'the arc is the theme colour, not mixed toward white');
172+
const reduced = css.slice(css.indexOf('@media (prefers-reduced-motion: reduce) {\n /* No motion'));
173+
assert.match(reduced.slice(0, 600), /body\.aura-working #composer::before \{ opacity: 0; \}/, 'reduced motion: a still border, no glow');
174+
});
175+
test('CSS: while the light turns the focus ring steps back — a hairline on an accent border is lost (desktop and browser)', () => {
176+
const desk = rule('body.aura-working #composer.focused');
177+
assert.match(desk, /box-shadow: none/);
178+
assert.match(desk, /border-color: color-mix\(in srgb, var\(--accent\) 40%, var\(--border\)\)/);
179+
const skin = fs.readFileSync(path.join(ROOT, '..', '..', 'web', 'ai-extension', 'skin', 'chat.css'), 'utf8');
180+
const i = skin.indexOf('body.lc-web.aura-working #composer:focus-within, body.lc-web.aura-working #composer.focused {');
181+
assert.ok(i >= 0, 'the browser skin\'s own focus halo (3px) is not stepped back while working');
182+
const block = skin.slice(i, skin.indexOf('}', i));
183+
assert.ok(!/0 0 0 3px/.test(block), 'the 3px halo is still drawn while working');
184+
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');
185+
});
160186

161187
// ---- the wiring -------------------------------------------------------------------------------------
162188
test('WIRING: every change of "busy" reaches the aura — the run starting and ending, a message on its way', () => {

‎web/ai-extension/skin/chat.css‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ body.lc-web #composer:focus-within, body.lc-web #composer.focused {
6060
border-color: color-mix(in srgb, var(--lcw-accent-text) 80%, var(--lcw-line-strong));
6161
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);
6262
}
63+
/* While the working light turns (chat.html, "the working aura") the focus halo steps back, as on the desktop:
64+
the light is a hairline on the border, and a 3px accent halo round it would swallow it. */
65+
body.lc-web.aura-working #composer:focus-within, body.lc-web.aura-working #composer.focused {
66+
border-color: color-mix(in srgb, var(--lcw-accent-text) 35%, var(--lcw-line-strong));
67+
box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 10px 32px rgba(0, 0, 0, .14);
68+
}
6369
body.lc-web textarea#input { font-size: 15px; line-height: 1.5; min-height: 46px; background: transparent; border: 0; }
6470
body.lc-web #toolbar .tbtn { border-radius: 999px; }
6571
body.lc-web #toolbar .pill { border-radius: 999px; background: transparent; border-color: transparent; padding: 3px 10px; color: var(--lcw-dim); }

0 commit comments

Comments
 (0)