Skip to content

Commit 86864f3

Browse files
authored
Merge pull request #106 from levelcodeai/feat/web-look
web: a chat-first look, light/dark themes and a layout switch for the browser edition
2 parents 633d0a8 + 3925003 commit 86864f3

29 files changed

Lines changed: 3252 additions & 53 deletions

‎CLAUDE.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ extensions/levelcode-updater/ notify-only update checker (polls the update fe
4242
patches/levelcode-core.patch our core source edits, applied on bootstrap
4343
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, editor-identity.mjs, build-macos.sh, make-dmg.sh, make-icon.sh, build-web.mjs; atom (CLI launcher) + install-level.sh
4444
tools/ dependency-free reference servers: sync-server (/v1 Settings-Sync), update-server (/api/update feed)
45-
web/ LevelCode in a browser tab: page entry + callback, scratch-workspace extension, browser build of levelcode-ai, dev server, tests (docs/WEB.md)
45+
web/ LevelCode in a browser tab: page entry + callback, scratch-workspace extension, layout, themes + chat skin, browser build of levelcode-ai, dev server, tests (docs/WEB.md)
4646
vscode/ GITIGNORED upstream Code-OSS checkout (generated)
4747
```
4848

@@ -101,6 +101,15 @@ the desktop app exactly as it was:
101101
- `web/main.js` is a plain ES module with a **top-level `await`**: a `const` declared lower in the file is not
102102
initialised until that await finishes (this cleared a saved session once). Declare constants above the
103103
`try`, and use function declarations for helpers it calls.
104+
- **The look is web-only and lives in `web/`** (theme package, `chrome.css`, the chat skin, the layout commands;
105+
`docs/WEB.md` "The look and the layout"). The chat skin is cut into the browser build's **copy** of
106+
`media/chat.html` by `web/ai-extension/skin.js`; never edit the desktop file for it (its suites pin the page).
107+
Every skin rule is under `body.lc-web`. An extension's `configurationDefaults` outrank the embedder's, so
108+
`levelcode-themes`' pinned default theme is stripped from the staged copy (`withoutConfigurationDefaults`).
109+
`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.
104113
- The session store (`createSecretStorage`) belongs to every tab: no cache, one-key read-modify-write under a
105114
Web Lock. The refresh token is rotated on use; do not reintroduce a start-up snapshot.
106115
- Build: `node scripts/build-web.mjs --account <origin>` → `dist-web/`; serve with `node web/serve.mjs --dist dist-web`.

‎docs/WEB.md‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ run. For the product's own page and copy see the account site (`/web`, `/docs/br
1010

1111
- [What it is, and what it is not](#what-it-is-and-what-it-is-not)
1212
- [How it fits together](#how-it-fits-together)
13+
- [The look and the layout](#the-look-and-the-layout)
1314
- [Signing in](#signing-in)
1415
- [Build and run](#build-and-run)
1516
- [Deploying](#deploying) — origins, headers, backend settings
@@ -64,6 +65,8 @@ the same lines so it never promises one (`host.caps`, see below).
6465
| `web/ai-extension/` | The browser build of `levelcode-ai`: `build.mjs` (esbuild, `platform: browser`), `shims/` (`fs`, `path`, `os`, `crypto`, `child_process`, `process`), `manifest.js` (the desktop manifest minus what a tab cannot do), `copy.js` (the few strings that are true on a Mac and false in a tab). |
6566
| `extensions/levelcode-ai/host.js` | The one place the desktop/browser difference lives in the extension (see below). |
6667
| `web/workspace/` | The `levelcode-web` extension: the **scratch workspace** (`levelcode-scratch:` file system in IndexedDB, with file and text search), and the commands to open a local folder or the scratch. |
68+
| `web/theme/levelcode-web-theme/`, `web/chrome.css` | The two colour themes (dark and light) and the stylesheet that tidies the workbench's chrome. See [the look](#the-look-and-the-layout). |
69+
| `web/ai-extension/skin.js`, `skin/chat.css`, `skin/chat.js` | The chat's browser skin, cut into the browser build's copy of `media/chat.html` at build time. |
6770
| `web/serve.mjs` | Dev / end-to-end server. Serves a build (`--dist`) with its own `_headers`, or the pieces (`--static`, `--extensions`). |
6871
| `web/test/` | Unit tests (`unit/`) and the end-to-end checks (`e2e*.mjs`) with a hand-written CDP driver and a stand-in backend. |
6972

@@ -88,6 +91,68 @@ The two rules that keep the desktop app exactly as it was:
8891
- **Nothing outside `host.js` asks "am I in a browser?".** A call site asks for a capability
8992
(`host.caps.shell`) or calls a function that already knows.
9093

94+
## The look and the layout
95+
96+
The first thing a visitor sees is the chat, not an IDE: a centred greeting with four starters and one rounded
97+
composer, with the list of conversations docked on its left when there is room, and the Explorer and the files one
98+
click away in the status bar. It is made **without forking the workbench** — every piece is a setting, a built-in
99+
extension, a stylesheet or a build-time edit of the browser build's own copy of a file.
100+
101+
| Piece | Where | What it does, and what it depends on |
102+
| --- | --- | --- |
103+
| Colour themes | `web/theme/levelcode-web-theme/` (staged as a built-in extension by `build-web.mjs`) | "LevelCode Web Dark" / "LevelCode Web Light": neutral chrome with the editor one step lighter, hairline borders, one violet accent. They carry the `agentsPanel.*` keys the floating cards use. Follows the system (`window.autoDetectColorScheme` + the two preferred-theme settings). The syntax colours are One Dark's and One Light's with every one under 4.5:1 on the editor moved in lightness only, hue kept (the light constants darkened with them, so numbers stay apart from class names); `theme.test.js` holds every syntax colour to 4.5:1. The editor paints no token *backgrounds*, so none is set (`invalid` is red text). |
104+
| 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. |
105+
| 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. |
106+
| 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 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. |
111+
| 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. |
112+
| 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). |
113+
114+
**Layouts.** `levelcode.web.layout` is `chatFirst` (the chat alone, side bars closed, conversations on its
115+
left — the way a chat app is used) or `split` (Explorer, files on the left, the chat docked on the right — the
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) where it takes effect — the workspace's
118+
value when `.vscode/settings.json` has one, since that outranks the user's, the user's otherwise; one listener
119+
applies it, so Settings does the same. The first start in a workspace is `chatFirst`. The chat panel is not restored by a reload, so a
120+
`split` layout docks the chat again at start — without revealing the Explorer or moving the focus (that is done
121+
once per workspace, `workspaceState`), so an Explorer the visitor closed stays closed. The start-up and a switch wait
122+
for the chat's tab to exist (`onDidChangeTabs`, 2.5 s) rather than reading the tab model at a guessed time. Only
123+
existing workbench commands are used, so what the visitor then arranges by hand is an ordinary layout. Files that are
124+
open when going back to `chatFirst` stay open as tabs behind the chat: a group that still has files (a split file
125+
editor, say) is joined into the chat's (`workbench.action.joinAllGroups`, which moves them in inactive), and that one
126+
group is unlocked, because a group the chat was docked in is locked and would send the next file into a new split.
127+
128+
**Widths.** The chat page decides by **its own width**, not the window's: from 1000 px it docks the
129+
conversation list as a 264 px rail; below 760 px it compacts; in between the list is the dialog it is on the
130+
desktop. On a touch device (no hover) the rail's card actions are always shown, as in the dialog, and the second
131+
line gives way; with a mouse they appear on hover or focus. Enter and Space on a card resume it (the desktop's
132+
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
133+
not responsive: below about 760 px the Explorer, tabs and status bar are the desktop's, and a phone is not a
134+
supported size for this edition yet.
135+
136+
Constraints worth knowing before changing it:
137+
138+
- The chat webview's CSP has no `font-src`: the skin uses the system font stack only.
139+
- A group that is emptied by moving the chat out of it is closed, so `split` opens a file in the first group
140+
**before** it moves the chat — the README, or an empty untitled document when there is none and the chat is the
141+
group's only tab (`applyLayout`, pinned by `workspace.test.js`).
142+
- The move and join commands act on the **active editor**, and `levelcode.ai.focus` returns before the reveal has
143+
reached the tab model. The window is read only once the chat is in front (until then an editor that was in front
144+
of it, such as a settings editor in a group of its own, still counts as a group). Each move or join then runs only
145+
when the tab model says the chat is the active editor, read immediately
146+
before it (after waiting up to 1.5 s for that); otherwise it is skipped, as is every later one in that arrangement,
147+
and so is one that a newer layout request has overtaken. A skipped move leaves the window as it was; a guessed one
148+
would move the file beside the chat.
149+
- The rail asks the page for its conversations with `listSessions`; it must never call `openSessions()`,
150+
which seeds the list with sample conversations when nothing has arrived yet.
151+
- Text drawn in the theme's accent uses `textLink.foreground`, not `focusBorder` (which is translucent and a line
152+
colour: 2.2–2.7:1 as text). The footer row is not dimmed with `opacity`: that dims its popovers as well.
153+
- `chat.html`'s own palettes for the conversation list and the account card (`--cc-*`, One Dark) are re-pointed at
154+
the theme by the skin.
155+
91156
## Signing in
92157

93158
The extension's existing PKCE flow, with the browser's pieces in place of the OS's:
@@ -270,8 +335,9 @@ transaction, so both end with the same key.
270335

271336
| Check | Command | What it proves |
272337
| --- | --- | --- |
273-
| Unit gate | `./scripts/test-extensions.sh` | Includes `web/test/unit/*.test.js`: the shims against Node, `host.js` on both sides, `main.js`'s functions (address check, delivery, the shared secret store, `?signin=1`), the manifest, the copy rewrites. Runs in CI on every PR (`test.yml`). Also run on Linux / Node 18 in a container with the network refused. |
338+
| Unit gate | `./scripts/test-extensions.sh` | Includes `web/test/unit/*.test.js`: the shims against Node, `host.js` on both sides, `main.js`'s functions (address check, delivery, the shared secret store, `?signin=1`), the manifest, the copy rewrites, the chat skin (what it inserts and that every rule is scoped), the themes (required colours, contrast of the text pairs, every syntax colour at 4.5:1 on the editor, the page's defaults naming themes that exist) and the layout commands. Runs in CI on every PR (`test.yml`). Also run on Linux / Node 18 in a container with the network refused. |
274339
| End to end | `node web/test/e2e.mjs --dist dist-web --stub-port <port the build used>` | Real headless Chrome: boot from static files, the extension activates in the worker, sign in through the real callback page, agent tool calls (write / list / search / read / edit) against the scratch workspace, Quick Open, Keep and Undo, a reload that keeps the session and files. |
340+
| The look | `node web/test/e2e-look.mjs --dist dist-web --stub-port <port>` | The skinned chat (greeting, starters, composer in the reading column, the rail at 1440 px and the dialog at 900 px), light and dark following the system with this build's own colours (also in the chat's webview), no Welcome page, no second chat, source control / run / extensions not offered, the layout switch both ways, and the docked layout docked again after a reload. Release builds only. |
275341
| Same-tab sign-in | `node web/test/e2e-signin-tab.mjs --dist dist-web --stub-port <port>` | No second window; verifier survives the reload; no repeat; no sign-in for a signed-in editor; a second tab uses the same session; a stranded result is delivered, an old one dropped. |
276342
| Bring your own key | `node web/test/e2e-byok.mjs --static … --extensions …` | The request goes from the tab to the provider with the user's key and nothing of LevelCode's. |
277343

@@ -328,6 +394,22 @@ the other repository's internals and is not part of this repo's checks.
328394
Say these out loud rather than discover them in production:
329395

330396
- **A real browser other than headless Chrome.** Safari and Firefox are untested; so are phones.
397+
- **The look in Safari, Firefox and on a phone.** `e2e-look.mjs` and the screenshots are headless Chrome at 1440,
398+
1100, 900 and 420 px wide. The chat page is responsive down to 420 px; the workbench around it is not (see
399+
[widths](#the-look-and-the-layout)). No screen-reader pass was done on the skin; the rail is a labelled
400+
landmark at wide widths and the dialog it already was below them.
401+
- **`workbench.experimental.modernUI` across a Code-OSS bump.** It is the workbench's own experimental flag; the
402+
pinned checkout was the only one run.
403+
- **The docked layout after a real browser reload.** `e2e-look.mjs` reloads through the debugger; the workbench
404+
flushes the Explorer's visibility to its own storage on shutdown, and whether a closed tab always gets that
405+
flush written was not measured. The layout is applied again at start (the chat docked), the side bar's state is
406+
the workbench's.
407+
- **The Markdown preview's content.** The pane's ground is now the theme's (it was white); in headless Chrome the
408+
preview's body rendered no text on the old and the new build alike, and that was not investigated.
409+
- **Known and left:** the Command Palette's "Chat: …" commands and the menus of features this edition lacks (above);
410+
the empty Terminal panel behind the panel toggle (a terminal is the next stage, below); the chat page's
411+
composer does not move with the soft keyboard on a phone; `levelcode.ai.chat.startLocation: none` is not
412+
consulted by the layout.
331413
- **The operating system's folder picker itself.** It needs a person. Everything after it — the command, the
332414
workbench's `file` provider on a real `FileSystemDirectoryHandle`, the agent's list, read and write — is run
333415
(`web/test/e2e.mjs` section 5), with the origin-private file system standing in for the picker. Permission

0 commit comments

Comments
 (0)