Skip to content

Commit b10b6c9

Browse files
committed
web(look): fixes from an independent review of the look
An agent review that RAN the build (44 findings, each independently verified) found real defects in the first commit. Fixed: Layout (web/workspace) - The switch read the tab model before the extension host had the chat's tab, so closing the chat and clicking "Editor + chat" left it undocked: wait on onDidChangeTabs, and for the chat to be the active editor. - A later choice now stops an earlier arrangement still waiting (the start-up one resumed and put the chat back on the right). - A reload's restored empty group is closed on the way to chat-first; columns, not list order, say which group is on the left. - Start-up no longer reveals the Explorer or moves focus after the first time in a workspace (workspaceState, not globalState). - The layout commands and the folder command are registered before the store is opened, so a browser that refuses IndexedDB still has them. - Opening a folder from the computer sets the editor layout. - No default key binding (Cmd+Alt+L is Find in Selection on macOS). Chat skin - Card actions revealed on hover only where there is hover; a tap landed on rename/delete. Enter/Space resume a card. aria-modal off the docked list; a /sessions-opened overlay no longer reappears as a modal. - Accent text and the focus ring use the link colour (was 2.2-2.7:1). - The footer row is no longer dimmed with opacity (it dimmed its popover). - The rail and the account card take the theme's palette; the sign-in dialog scrolls in a short window; the empty state is not clipped; the chat paints its own ground; starters make sense with no file open. Workbench - Code-OSS's own Chat view is hidden for real: create() returns a disposable, so workbench.commands was undefined and the first attempt never ran. The gate key is set through the module's commands, again after the gate's own write at 5 s. The title-bar toggle is hidden. - editor.padding.top: 24 (the scroll shadow washed out line one). - No page-level color-scheme: stock webviews (Markdown preview) were white under a dark theme. - The build id covers the declarative built-ins' sources. Tests: unit 73 files on macOS and Linux Node 18; e2e-look 26 checks, ten runs in a row; the other end-to-end checks unchanged. The test helper retries F1 (a key sent while the chat takes the focus is lost).
1 parent 17f85ca commit b10b6c9

23 files changed

Lines changed: 768 additions & 134 deletions

‎CLAUDE.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ the desktop app exactly as it was:
107107
Every skin rule is under `body.lc-web`. An extension's `configurationDefaults` outrank the embedder's, so
108108
`levelcode-themes`' pinned default theme is stripped from the staged copy (`withoutConfigurationDefaults`).
109109
`node web/test/e2e-look.mjs --dist dist-web --stub-port <port>` is the check; look at its screenshots (`--shots`).
110+
The embedder's command API is the module's `commands` (`api.commands.executeCommand`): `create()` returns a
111+
disposable, so `workbench.commands` is `undefined` — a helper that used it ran inside a `try` and silently did
112+
nothing until a browser test asked for its effect.
110113
- The session store (`createSecretStorage`) belongs to every tab: no cache, one-key read-modify-write under a
111114
Web Lock. The refresh token is rotated on use; do not reintroduce a start-up snapshot.
112115
- Build: `node scripts/build-web.mjs --account <origin>` → `dist-web/`; serve with `node web/serve.mjs --dist dist-web`.

‎docs/WEB.md‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,29 @@ extension, a stylesheet or a build-time edit of the browser build's own copy of
104104
| First paint | `initialColorTheme` in `web/lib/config.mjs`, picked by `pickInitialTheme` in `web/main.js` | The colours painted for the half second before the theme extension has loaded, so a dark visitor does not see a light flash. |
105105
| Default theme | `withoutConfigurationDefaults` in `web/lib/extensions.mjs` | An extension's `configurationDefaults` **outrank** the embedder's. `levelcode-themes` pins its own default theme, so the staged copy of it has that key removed (its two themes stay selectable). The desktop source is untouched. |
106106
| Panels, activity bar | `workbench.experimental.modernUI`, `workbench.activityBar.location: top`, `workbench.layoutControl.type: toggles` | Code-OSS's own switches. `modernUI` is **experimental**: if a bump renames or drops it the editor is still fine, only plainer (`e2e-look.mjs` still passes its theme and layout checks; its screenshots are the thing to look at). |
107-
| Chrome | `web/chrome.css`, linked by `main.js` **after** the workbench's stylesheet | Hides source control, run and extensions (this edition has none of them), the Outline and Timeline headers and the keyboard-layout status item. The selectors are workbench class names: when a bump stops matching them `e2e-look.mjs` fails on "not offered" rather than the icons quietly coming back. |
108-
| The workbench's own chat | `hideBuiltInChat` in `main.js` | Code-OSS's Copilot chat view appears in the secondary side bar on the web even with `chat.disableAIFeatures`; it is told it is disabled through an internal context key. Look for a "Chat" tab on the right on a bump. |
107+
| Chrome | `web/chrome.css`, linked by `main.js` **after** the workbench's stylesheet | Hides the activity-bar entries for source control, run and extensions (this edition has none of them), the Outline and Timeline headers, the keyboard-layout status item and the title bar's secondary-side-bar toggle. The selectors are workbench class names and aria labels: when a bump stops matching them `e2e-look.mjs` fails rather than the icons quietly coming back. Their **menus, palette entries and shortcuts are the workbench's own** and still lead to empty views (Run and Debug, Extensions, Source Control, an empty Terminal panel). |
108+
| The workbench's own chat | `hideBuiltInChat` in `main.js` | Code-OSS registers its Copilot Chat view on the web even with `chat.disableAIFeatures`, and opened it fills the window with a dead composer ("No default agent contributed"). Its `when` clause is gated by `chatAccountPolicyGateActive`, which nothing else reads; `main.js` sets it (and `chatSetupDisabledInWorkspace`) through the embedder's `commands.executeCommand('_setContext', …)` — the **module's** `commands`: `create()` returns a disposable, not the workbench — and again after five seconds, because the gate's own contribution writes its key again then. `e2e-look.mjs` opens the bar and fails on a "Chat" tab. |
109+
| Editor padding | `editor.padding.top: 24` | `modernUI` fades the top 24 px of every editor into the ground, line one included. |
110+
| No page-level `color-scheme` | `index.html`, `boot.css` | A page that declares `color-scheme: dark light` made every stock webview (the Markdown preview) paint an opaque *white* page under a dark theme. The chat page paints its own ground (`body.lc-web { background: … }`) for the same reason. |
109111
| Welcome page | `web/ai-extension/copy.js` | The desktop opens its walkthrough once, in front of everything. In a tab the chat is the first thing there is; the walkthrough stays in the Command Palette. |
110112
| The chat page | `web/ai-extension/skin.js` + `skin/chat.css` + `skin/chat.js` | Three exact insertions into the **embedded copy** of `media/chat.html`: a style block, `class="lc-web"` on `<body>`, a nonce'd script. Every rule is under `body.lc-web`; the script sends only messages the page already sends. The build **fails** if chat.html stops having the places the skin is cut into. The desktop's `chat.html` is never edited for this (the desktop suites pin it). |
111113

112114
**Layouts.** `levelcode.web.layout` is `chatFirst` (the chat alone, side bars closed, conversations on its
113115
left — the way a chat app is used) or `split` (Explorer, files on the left, the chat docked on the right — the
114-
way an editor is used). The status bar item at the left, **LevelCode: Layout: Switch** and `Cmd/Ctrl+Alt+L`
115-
change the setting; one listener applies it, so Settings does the same. The first start is `chatFirst`. The
116-
chat panel is not restored by a reload, so a `split` layout is applied again at start. Only existing workbench
117-
commands are used, so what the visitor then arranges by hand is an ordinary layout.
116+
way an editor is used). The status bar item at the left and **LevelCode: Layout: Switch** change the setting
117+
(there is no default key binding: `Cmd+Alt+L` is Find in Selection on macOS); one listener applies it, so Settings
118+
does the same. The first start in a workspace is `chatFirst`. The chat panel is not restored by a reload, so a
119+
`split` layout docks the chat again at start — without revealing the Explorer or moving the focus (that is done
120+
once per workspace, `workspaceState`), so an Explorer the visitor closed stays closed. The start-up and a switch wait
121+
for the chat's tab to exist (`onDidChangeTabs`, 2.5 s) rather than reading the tab model at a guessed time. Only
122+
existing workbench commands are used, so what the visitor then arranges by hand is an ordinary layout. Files that are
123+
open when going back to `chatFirst` stay as tabs beside the chat.
118124

119125
**Widths.** The chat page decides by **its own width**, not the window's: from 1000 px it docks the
120126
conversation list as a 264 px rail; below 760 px it compacts; in between the list is the dialog it is on the
121-
desktop. In `split` the chat is a narrower column, so it will usually be in the dialog form. The *workbench* is
127+
desktop. On a touch device (no hover) the rail's card actions are always shown, as in the dialog, and the second
128+
line gives way; with a mouse they appear on hover or focus. Enter and Space on a card resume it (the desktop's
129+
card is a `div` with a click handler only; the skin adds the keys). In `split` the chat is a narrower column, so it will usually be in the dialog form. The *workbench* is
122130
not responsive: below about 760 px the Explorer, tabs and status bar are the desktop's, and a phone is not a
123131
supported size for this edition yet.
124132

@@ -129,6 +137,10 @@ Constraints worth knowing before changing it:
129137
**before** it moves the chat (`applyLayout`, pinned by `workspace.test.js`).
130138
- The rail asks the page for its conversations with `listSessions`; it must never call `openSessions()`,
131139
which seeds the list with sample conversations when nothing has arrived yet.
140+
- Text drawn in the theme's accent uses `textLink.foreground`, not `focusBorder` (which is translucent and a line
141+
colour: 2.2–2.7:1 as text). The footer row is not dimmed with `opacity`: that dims its popovers as well.
142+
- `chat.html`'s own palettes for the conversation list and the account card (`--cc-*`, One Dark) are re-pointed at
143+
the theme by the skin.
132144

133145
## Signing in
134146

@@ -364,6 +376,16 @@ Say these out loud rather than discover them in production:
364376
landmark at wide widths and the dialog it already was below them.
365377
- **`workbench.experimental.modernUI` across a Code-OSS bump.** It is the workbench's own experimental flag; the
366378
pinned checkout was the only one run.
379+
- **The docked layout after a real browser reload.** `e2e-look.mjs` reloads through the debugger; the workbench
380+
flushes the Explorer's visibility to its own storage on shutdown, and whether a closed tab always gets that
381+
flush written was not measured. The layout is applied again at start (the chat docked), the side bar's state is
382+
the workbench's.
383+
- **The Markdown preview's content.** The pane's ground is now the theme's (it was white); in headless Chrome the
384+
preview's body rendered no text on the old and the new build alike, and that was not investigated.
385+
- **Known and left:** the Command Palette's "Chat: …" commands and the menus of features this edition lacks (above);
386+
the empty Terminal panel behind the panel toggle (a terminal is the next stage, below); the chat page's
387+
composer does not move with the soft keyboard on a phone; `levelcode.ai.chat.startLocation: none` is not
388+
consulted by the layout.
367389
- **The operating system's folder picker itself.** It needs a person. Everything after it — the command, the
368390
workbench's `file` provider on a real `FileSystemDirectoryHandle`, the agent's list, read and write — is run
369391
(`web/test/e2e.mjs` section 5), with the origin-private file system standing in for the picker. Permission

‎scripts/build-web.mjs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import path from 'node:path';
2222
import { createHash } from 'node:crypto';
2323
import { fileURLToPath } from 'node:url';
2424
import { webConfig, renderIndex } from '../web/lib/config.mjs';
25-
import { stageDeclarativeExtensions, copyExtension, withoutConfigurationDefaults } from '../web/lib/extensions.mjs';
25+
import { stageDeclarativeExtensions, declarativeExtensionNames, copyExtension, withoutConfigurationDefaults, COPY_SKIP } from '../web/lib/extensions.mjs';
2626
import { policy, toHeadersFile, toNginx } from '../web/lib/headers.mjs';
2727
import { hashTree } from '../web/lib/fingerprint.mjs';
2828

@@ -77,6 +77,21 @@ hashTree(fingerprint, path.join(WEB, 'ai-extension'), { skip: ['assets.generated
7777
hashTree(fingerprint, path.join(WEB, 'workspace'), { skip: ['node_modules', '.DS_Store'] });
7878
hashTree(fingerprint, path.join(WEB, 'theme'), { skip: ['.DS_Store'] });
7979
hashTree(fingerprint, path.join(WEB, 'lib'), { skip: ['.DS_Store'] });
80+
// The declarative built-ins are staged from the checkout's extensions/ (grammars, language configurations, the desktop's
81+
// themes — whose manifest this build rewrites): a changed grammar is a changed build, and so is the fallback copy of
82+
// the themes taken from this repository when the checkout has none.
83+
{
84+
const baked = fs.existsSync(path.join(staticDir, 'extensions')) ? fs.readdirSync(path.join(staticDir, 'extensions')) : [];
85+
const source = path.join(vscodeDir, 'extensions');
86+
if (fs.existsSync(source)) {
87+
for (const name of declarativeExtensionNames(source, [...baked, 'levelcode-ai', 'levelcode-web'])) {
88+
fingerprint.update('\0ext\0' + name);
89+
hashTree(fingerprint, path.join(source, name), { skip: [...COPY_SKIP, '.DS_Store'] });
90+
}
91+
}
92+
const themes = path.join(REPO, 'extensions', 'levelcode-themes');
93+
if (fs.existsSync(themes)) { hashTree(fingerprint, themes, { skip: [...COPY_SKIP, '.DS_Store'] }); }
94+
}
8095
const id = String(args.id || fingerprint.digest('hex').slice(0, 12));
8196
let gitHead = 'unknown';
8297
try { gitHead = execFileSync('git', ['-C', REPO, 'rev-parse', '--short=10', 'HEAD'], { encoding: 'utf8' }).trim(); } catch { /* not a checkout */ }

‎web/ai-extension/copy.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const REPLACEMENTS = [
2424
['extension.js',
2525
"\tif (!context.globalState.get('levelcode.ai.didShowWelcome')) {",
2626
"\tif (false) { // browser edition: the chat is the first thing a visitor sees (web/ai-extension/copy.js)"],
27+
// The conversation list is on the page's own rail in a tab, and the first thing beside an empty chat.
28+
['media/chat.html',
29+
'Every chat you have is saved here — automatically, on your machine.',
30+
'Every chat you have is saved here — automatically, in this browser.'],
2731
['media/chat.html',
2832
'Bring your own API key, stored in your OS keychain.',
2933
'Bring your own API key, stored encrypted in this browser.'],

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

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,18 @@ body.lc-web {
2424
}
2525
body.lc-web ::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
2626

27+
/* The page paints its own ground. Left to the browser, a page that declares both colour schemes gets Chrome's
28+
canvas (#121212 in dark), not the theme's editor colour. */
29+
body.lc-web { background: var(--vscode-editor-background); }
30+
31+
/* --accent is the theme's focus border: translucent, and a line colour, not a text colour (2.2-2.7:1 as text on
32+
the dark ground). Text that was drawn in it, and the focus ring, take the theme's link colour instead
33+
(5.7:1 dark, 7.5:1 light). Fills keep --accent. */
34+
body.lc-web { --lcw-accent-text: var(--vscode-textLink-foreground, var(--accent)); }
35+
body.lc-web .msg .body a, body.lc-web .filechip .fcname, body.lc-web .modeopt.active .moname, body.lc-web #status .stap.autopilot { color: var(--lcw-accent-text); }
36+
2737
/* ---- the empty chat: a hero and the composer in the middle of the page --------------------------- */
28-
body.lc-web:has(#empty) { justify-content: center; }
38+
body.lc-web:has(#empty) { justify-content: safe center; }
2939
body.lc-web:has(#empty) #log { flex: 0 0 auto; overflow: visible; padding-bottom: 0; }
3040
body.lc-web #empty { opacity: 1; padding: 0 0 22px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
3141
body.lc-web #empty .lc-ascii-wrap, body.lc-web #empty .lc-ascii-sub { display: none; }
@@ -47,8 +57,8 @@ body.lc-web #composer {
4757
transition: border-color .15s, box-shadow .15s;
4858
}
4959
body.lc-web #composer:focus-within, body.lc-web #composer.focused {
50-
border-color: color-mix(in srgb, var(--accent) 70%, var(--lcw-line-strong));
51-
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(--accent) 22%, transparent);
60+
border-color: color-mix(in srgb, var(--lcw-accent-text) 80%, var(--lcw-line-strong));
61+
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);
5262
}
5363
body.lc-web textarea#input { font-size: 15px; line-height: 1.5; min-height: 46px; background: transparent; border: 0; }
5464
body.lc-web #toolbar .tbtn { border-radius: 999px; }
@@ -63,23 +73,45 @@ body.lc-web #send:disabled { opacity: .35; }
6373

6474
/* ---- the footer row: quiet, and the brand mark is not repeated ----------------------------------- */
6575
body.lc-web #status #lcLogo { display: none; }
66-
body.lc-web #status { opacity: .78; }
6776

6877
/* ---- the conversation --------------------------------------------------------------------------- */
6978
body.lc-web .msg.user .body {
7079
border: 0; border-radius: 20px; padding: 10px 16px; background: var(--lcw-soft-2); max-width: 82%; width: fit-content;
7180
}
72-
body.lc-web .msgcopy { opacity: .7; border-color: transparent; }
81+
body.lc-web .msgcopy { border-color: transparent; }
7382
body.lc-web pre, body.lc-web .codeblock { border-radius: 12px; }
7483
body.lc-web #reviewBar, body.lc-web #planBar { border-radius: 14px; }
7584

85+
/* ---- the conversation list and the account card carry their own (One Dark) palette in chat.html; point it at the
86+
theme, so the rail and the sign-in are the same family as the page next to them. */
87+
body.lc-web .sesspanel, body.lc-web .acctcard.classic {
88+
--cc-bg: var(--vscode-editor-background);
89+
--cc-surface: color-mix(in srgb, var(--vscode-foreground) 3%, var(--vscode-editor-background));
90+
--cc-text: var(--vscode-foreground);
91+
--cc-text2: color-mix(in srgb, var(--vscode-foreground) 82%, transparent);
92+
--cc-text3: color-mix(in srgb, var(--vscode-foreground) 68%, transparent);
93+
--cc-line: var(--lcw-line-strong);
94+
--cc-accent: var(--vscode-button-background, var(--accent));
95+
}
96+
body.lc-web .acctcard.classic .signout { color: var(--lcw-accent-text); }
97+
body.lc-web .acctcard.classic .abtn.primary { color: var(--vscode-button-foreground, #fff); }
98+
99+
/* A dialog taller than the window scrolls, with its top reachable (the sign-in card is 660px). */
100+
body.lc-web .overlay { overflow-y: auto; align-items: safe center; }
101+
76102
/* ---- narrow: the compact column that sits beside an editor --------------------------------------- */
77103
@media (max-width: 759px) {
78104
body.lc-web { --prose-size: 14.5px; --shell-x: 12px; }
79105
body.lc-web #composer { border-radius: 20px; padding: 10px 12px 8px; }
80106
body.lc-web .lcw-hero { font-size: 20px; }
81107
body.lc-web .lcw-mark { width: 30px; height: 30px; }
82108
body.lc-web .lcw-sub { display: none; }
109+
body.lc-web #status { gap: 6px; padding-inline: 6px; }
110+
}
111+
/* Short windows (a laptop at 200% zoom, a phone in landscape): the greeting gives up its mark and its sentence. */
112+
@media (max-height: 460px) {
113+
body.lc-web .lcw-mark, body.lc-web .lcw-sub { display: none; }
114+
body.lc-web #empty { padding-bottom: 12px; }
83115
}
84116

85117
/* ---- wide: chat-first, the conversation list docked on the left ---------------------------------- */
@@ -104,12 +136,19 @@ body.lc-web #reviewBar, body.lc-web #planBar { border-radius: 14px; }
104136
}
105137
body.lc-web .lcw-new:hover { background: var(--lcw-soft-2); }
106138
body.lc-web #sessOverlay .sesscard { border-radius: 10px; }
107-
/* A narrow card has room for its title and one line, not for six buttons as well: they appear on hover or
108-
focus and take the place of the second line. */
139+
}
140+
/* A narrow card has room for its title and one line, not for six buttons as well. With a mouse they appear on hover
141+
or focus and take the place of the second line. Without one (a tap applies :hover first, then lands on whatever
142+
the layout change put under the finger — a rename, a delete) they are always there and the second line gives
143+
way, as in the dialog. */
144+
@media (min-width: 1000px) and (hover: hover) and (pointer: fine) {
109145
body.lc-web #sessOverlay .sesscard .sessacts { display: none; }
110146
body.lc-web #sessOverlay .sesscard:hover .sessacts, body.lc-web #sessOverlay .sesscard:focus-within .sessacts { display: flex; margin-left: 0; }
111147
body.lc-web #sessOverlay .sesscard:hover .sesssub, body.lc-web #sessOverlay .sesscard:focus-within .sesssub { display: none; }
112148
}
149+
@media (min-width: 1000px) and (hover: none), (min-width: 1000px) and (pointer: coarse) {
150+
body.lc-web #sessOverlay .sesscard .sesssub { display: none; }
151+
}
113152
@media (max-width: 999px) { body.lc-web .lcw-new { display: none; } }
114153

115154
@media (prefers-reduced-motion: reduce) {

0 commit comments

Comments
 (0)