Repository navigation
feat(web): LevelCode in your browser — the same workbench and AI extension as a static site, signed in with a LevelCode account - #105
Merged
Conversation
… can do The agent read and wrote the workspace with synchronous fs and assumed it could start programs. LevelCode is about to run in a browser tab, where the project is whatever the editor's file system provider says it is and there is no shell. host.js is the one place that difference lives. On the desktop every function does what the call site did before, behind await; capabilities (shell, mcpStdio, ripgrep) are all true. Where a capability is missing the tool is withheld rather than offered and left to fail, the system prompt says what is missing, and content search falls back to a bounded scan. resolveWorkspacePath stays (the suites pin it); resolveWorkspacePathAsync is the same resolution for hosts that can only ask asynchronously. A workspace rooted at '/' now contains its files. All 63 suites pass on macOS.
…ch workspace and the browser build of levelcode-ai A static Code-OSS web build plus three things of ours: - web/main.js: the embedder entry a static host needs (Code-OSS's own workbench.ts is written for its server). Same URL-callback and workspace-URL contracts; secrets sealed with a non-extractable AES-GCM key in IndexedDB; opens the scratch workspace when no folder is given. - web/workspace: the scratch workspace, a FileSystemProvider over IndexedDB shared by every tab. - web/ai-extension: levelcode-ai bundled for the web worker extension host by esbuild with small shims (path, os, crypto incl. a synchronous SHA-256, child_process that refuses, and an in-memory fs made durable in IndexedDB). The desktop extension stays plain JS. web/test/e2e.mjs drives a real headless Chrome: boot, sign in through the real PKCE flow and callback page against a stand-in backend that applies the same redirect and CORS rules, ask the agent to create a file, reload.
…heck covers the agent's file tools Quick Open, the Search view and workspace.findFiles (the agent's list_files) wait forever for a provider registered for the folder's scheme. The scratch workspace now registers a file and a text search provider (proposed APIs, declared by this built-in extension). e2e: list, search, read and edit through the real tool loop, Quick Open, and a wait for the run to finish between prompts (the send button is the stop button while a run is active).
…nd unit suites for the stand-ins - web/ai-extension/manifest.js: the desktop manifest minus the commands, settings and welcome copy that need a shell, a disk or a local process; a key the browser lets through (Ctrl+Shift+I is developer tools on Windows and Linux). - unit suites in the gate (scripts/test-extensions.sh now also runs web/test/unit): path and crypto are compared with Node over many inputs (the comparison found three differences in the first path shim), fs for the behaviours and error codes the extension relies on, host.js for both modes, the manifest for dangling references. - the e2e also runs with webviews and the extension host on origins of their own, where the extension's requests come from a per-session v--<hash> origin.
…t's HTTP policy Builds Code-OSS's web client (or takes a prebuilt one), bundles levelcode-ai for the web worker extension host, stages the scratch workspace and the declarative built-ins, and lays it all out under a build-addressed prefix (/_/<id>) so it caches for a year. index.html is rendered with its configuration baked in. _headers and an nginx fragment carry the same CSP and cache policy; web/serve.mjs --dist serves a release with them, and the end-to-end check runs under that policy. Findings that shaped it, each checked in a real browser: - the checkout's web packaging emits only the ~20 extensions with a browser entry, so every declarative one (grammars, themes, icons) is staged here and loaded as an additional built-in; - a marketplace is on by default and would install third-party code into the page's origin: the release sets extensionsGallery to null; - each webview needs its own subdomain, so a self-hosted webview origin has to be a wildcard; the default is Code-OSS's CDN at this build's own commit (pinned insider commit is a service worker version behind); - the Copilot status item is created before the web-only hide rule runs: hidden by CSS; - the scratch workspace is /scratch, so the window and Explorer are not called "/".
…d check - web/ai-extension/copy.js: the three desktop sentences about the OS keychain and 'your machine' are rewritten for a tab at build time, and the build fails if one is no longer in the source, so the browser never ships a stale claim about where a secret lives. - web/test/e2e-byok.mjs: set a key through the real command, ask the agent, and see the request go from the tab straight to the provider with that key and no LevelCode account call. - the stand-in backend gains an OpenAI-compatible provider endpoint with the CORS real ones send.
…gainst a release Undo of a file the agent created removes it, and so does everything else still pending from the session; Keep clears the bar. The account card that sign-in leaves open is closed the way a user would, before the chat is clicked.
A pop-up opened from a chain that begins in a webview is the first thing a strict browser (Safari, a phone) refuses. The editor now takes its own tab to the account site's sign-in and the sign-in returns to the same tab: - host.openAuth: in the browser the extension asks the page to navigate (levelcode.web.openAuthUrl, an embedder command) with the address the editor's own opener would have used; without the command it opens as before. The desktop is untouched. - web/main.js registers the command. It only goes to the account site's /ai/ pages: any extension in the page can call it, and it must not be an open redirect. - callback.html returns the tab to the editor when this tab left it (a sessionStorage mark), and otherwise says to close the tab and offers a link in. - At start-up the editor delivers a result the sign-in left in localStorage (a callback.html result written in the last two minutes: the one-time code lives for one), and ?signin=1 — the account site's "Open in browser" — starts the sign-in once and removes itself from the address. Tests: unit (host.openAuth; main.js's leaveForSignIn, drain and start-up, mutation-checked) and an end-to-end in headless Chrome (web/test/e2e-signin-tab.mjs): no second window, the verifier survives the reload, no repeat, no sign-in for a signed-in editor, a result waiting for a gone editor is delivered and an old one is dropped.
…st on its own origin
…ted it The refresh token is rotated on use. The browser secret store loaded everything once and rewrote the whole thing on every change, so a second tab held the token the first had already rotated away: its renewal was refused, ended the session the first tab had just renewed, and wrote its start-up snapshot back over the first tab's tokens. Now nothing is cached. A read is a read of what is stored; a change reads, changes ONE key and writes, under a lock shared by the tabs (Web Locks; without it a tab still applies its own changes one at a time). A browser that refuses the write keeps the session in memory for that tab rather than completing a sign-in that is not there. Tests: unit (two tabs over one store, the rotated token, concurrent writers, unreadable store, no key, refused write — each mutation-checked) and a second tab in the end-to-end.
…ecurity model, and what was not run
…re tests mean the same on every Node
…cker Run for the first time against a real browser session and the real login page, the command did nothing useful from the default scratch workspace: the workbench chooses between its own file browser and the browser's picker by the scheme of the default location, and in the scratch workspace that scheme is ours — so it showed the scratch workspace's own folders. The command now asks the dialog from the `file` scheme, which the web workbench serves with the File System Access handles the user picks, and opens what came back in this tab. Tests: a unit test of the command against a stand-in for vscode (mutation-checked), and a section of the end-to-end check that opens a folder through a real FileSystemDirectoryHandle (the origin-private file system stands in for the OS picker) and has the agent list, read and write there.
A release puts everything under /_/<id>/ and tells browsers it never changes. The id was a hash of the client, the page and the extension's top-level .js files, so a changed scratch-workspace extension, shim, media file or skill kept its id — and would have been served stale, from cache, for a year. The hash now covers the extension's whole tree, its browser build, the scratch workspace and the page's configuration code.
The extension already supports an API host that is not the account origin (levelcode.cloud.apiUrl); the build can now bake it in, which is what a local account site with the backend behind a tunnel needs. docs/WEB.md says how to run the real stack locally and what to look for, and what was and was not run.
…ays what the build needs
The browser edition now opens on the chat the way a chat app does, and one click gives the editor layout (Explorer, files, chat docked on the right): - web/theme/levelcode-web-theme: "LevelCode Web Dark" / "LevelCode Web Light", following the system; initialColorTheme paints the same colours before the theme has loaded. levelcode-themes' pinned default is stripped from the staged copy (an extension's configurationDefaults outrank the embedder's). - web/ai-extension/skin*: a centred greeting with starters, a pill composer in a 768px reading column and the conversation list docked as a rail from 1000px, cut into the browser build's COPY of media/chat.html at build time and scoped under body.lc-web. The desktop page and its suites are untouched. - web/chrome.css, config defaults: floating panels, activity bar as a row, no source control / run / extensions, no Outline / Timeline, no minimap; the workbench's own chat view is told it is disabled; no Welcome walkthrough. - web/workspace: levelcode.web.layout (chatFirst | split), a status-bar switch, three commands, Cmd/Ctrl+Alt+L. A docked layout is docked again after a reload. Tests: skin, theme, layout and copy unit suites (mutation-checked), main.js's new helpers, and web/test/e2e-look.mjs (21 checks, headless Chrome).
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).
This was referenced Oct 10, 2026
ndemianc
marked this pull request as ready for review
October 14, 2026 17:23
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved authentication, persistence, workspace, and hosting defects prevent safe merging.
12 open findings
Reject unauthenticated callback parameters and require PKCE codes · New Serve index.html explicitly for the nginx root location · New Scope workspace file caps before filtering and limiting · New Use host-aware filesystem validation for diagram links · New Resync chat after asynchronous review rehydration · New Include all staged assets in the immutable build ID · New Preserve and restore the initiating workspace URL · New Allow inline completions in the default scratch workspace · New Persist PKCE verifiers across memory-fallback navigation · New Prevent concurrent IndexedDB key initialization races · New Remove destination descendants during overwrite replacements · New Register recovery command when IndexedDB initialization fails · New
What changed in this PR
Adds a static-site browser edition of LevelCode, reusing the Code-OSS workbench and AI extension with browser-compatible storage, authentication, and workspace access.
Changes:
- Adds browser startup, account sign-in, scratch workspaces, and local-folder access.
- Adapts AI workspace operations and withholds unsupported shell tools.
- Adds release packaging, hosting policies, documentation, and browser tests.
| File | Description |
|---|---|
| web/workspace/package.json | Declares the scratch-workspace extension. |
| web/workspace/extension.js | Implements persistent scratch files, search, and folder commands. |
| web/test/unit/workspace.test.js | Tests local-folder selection. |
| web/test/unit/shims.test.js | Tests Node compatibility shims. |
| web/test/unit/manifest.test.js | Tests browser manifest adaptations. |
| web/test/unit/host.test.js | Tests desktop and browser host behavior. |
| web/test/unit/fingerprint.test.js | Tests asset fingerprinting. |
| web/test/unit/copy.test.js | Tests browser-specific text replacements. |
| web/test/stub-backend.mjs | Supplies a test authentication and AI backend. |
| web/test/e2e-signin-tab.mjs | Tests same-tab sign-in and session reuse. |
| web/test/e2e-byok.mjs | Tests direct provider-key requests. |
| web/test/cdp.mjs | Adds a headless Chrome driver. |
| web/serve.mjs | Serves development and release assets. |
| web/manifest.webmanifest | Defines browser app metadata. |
| web/main.js | Initializes the workbench, callbacks, and secret storage. |
| web/lib/headers.mjs | Generates hosting and security headers. |
| web/lib/fingerprint.mjs | Hashes asset trees. |
| web/lib/extensions.mjs | Stages declarative built-in extensions. |
| web/lib/config.mjs | Generates shared page configuration. |
| web/index.html | Adds the editor entry page. |
| web/icon.svg | Adds the browser icon. |
| web/callback.js | Delivers sign-in results to the editor. |
| web/callback.html | Adds the sign-in return page. |
| web/callback.css | Styles the callback page. |
| web/boot.css | Styles startup and hides unused Copilot UI. |
| web/ai-extension/shims/process.js | Supplies browser process compatibility. |
| web/ai-extension/shims/path.js | Implements POSIX path utilities. |
| web/ai-extension/shims/os.js | Supplies browser host metadata. |
| web/ai-extension/shims/fs.js | Persists private extension files in IndexedDB. |
| web/ai-extension/shims/crypto.js | Supplies hashing and randomness. |
| web/ai-extension/shims/child_process.js | Rejects unsupported process execution. |
| web/ai-extension/manifest.js | Removes desktop-only contributions. |
| web/ai-extension/inject.js | Injects compatible Node globals. |
| web/ai-extension/entry.js | Prepares browser storage before activation. |
| web/ai-extension/copy.js | Adapts browser messaging and sign-in opening. |
| web/ai-extension/build.mjs | Bundles the browser AI extension. |
| scripts/test-extensions.sh | Includes browser unit suites in the gate. |
| scripts/build-web.mjs | Assembles the static release. |
| README.md | Introduces the browser edition. |
| extensions/levelcode-ai/test/diagramHost.test.js | Adds host-aware diagram test fixtures. |
| extensions/levelcode-ai/reviewSession.js | Adapts edit review to asynchronous workspace access. |
| extensions/levelcode-ai/providers/anthropic.js | Enables browser-specific API headers. |
| extensions/levelcode-ai/host.js | Centralizes host capabilities and workspace access. |
| extensions/levelcode-ai/extension.js | Adapts retrieval, links, and attachments. |
| extensions/levelcode-ai/agent.js | Adds browser file operations and capability filtering. |
| CLAUDE.md | Documents browser architecture and conventions. |
| .gitignore | Excludes generated browser artifacts. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…in; search, diagram links, completions and storage work in a tab Sixteen findings: four from the code-quality check, twelve from Copilot. All held up against the code; each substantive one is fixed with a test that fails on the code as reviewed. Signing in - callback.js forwarded every parameter of the link — `token=` and `refresh=` included — to the extension, whose old token-in-the-link branch stored them, and the page's start-up delivers what callback.html left even when no sign-in was pending. A crafted link to callback.html, followed by opening the editor, signed the user in to the link-maker's account. callback.html now passes on a sign-in to levelcode.levelcode-ai/auth/callback and only its one-time code; and in a browser (new capability `host.caps.unaskedSignIn`, off there) the extension takes a code only while a sign-in of its own is pending — never a token in a link. The desktop is unchanged. - A sign-in started from a folder or workspace opened from the computer came back to `/`, the scratch workspace. The editor notes the address it leaves (minus `signin=1`); callback.html returns there after checking it is the editor page of this site. - With no persistent secret store (no IndexedDB or WebCrypto, or a refused write), the PKCE verifier lived in memory and was gone when the tab came back from the sign-in. The tab's secrets now ride through that one navigation in sessionStorage and are taken back, and removed, at the next start. - Two tabs opened at once on a fresh profile could each create and store a different sealing key; each then read the other's sealed session as garbage and cleared it. The key is stored in one readwrite transaction, only if none is there, and every tab uses the key that is. The agent in a tab - The content scans that stand in for ripgrep (agent.js scanSearch, extension.js contentHits) capped the file list across the whole workspace before filtering to the folder searched: in a two-folder workspace the other folder could use up the cap. Both now search under the folder (RelativePattern) and cap that. - Diagram links were checked with fs.statSync/realpathSync, which in the browser build reach a shim with no workspace in it, so every link was dropped. Drawing now checks against the files the extension has indexed; a click checks the file system itself (host.withStats, the async-loader pattern of host.withReads). - Pending Keep/Undo reviews are read back asynchronously in a browser; a chat that asked for its cards first was sent none and never got them. They are sent once they are read. - Inline completion refused every document that is not `file` or `untitled` — all of the scratch workspace. It now asks host.isUserDocument, which in a browser also accepts the workspace folders' own schemes. The scratch workspace - A rename or copy over an existing folder deleted only the folder's own entry: its files stayed under the new name, visible to search. The whole destination subtree goes in the same transaction; a move over a folder that contains the source is refused. - With IndexedDB unavailable, activation returned before registering "Open a folder from your computer" — the one way out the warning names. It is registered first. Hosting - The generated nginx block answered 404 at `/`: `try_files $uri` looks for a file, and an exact location keeps the request from the fallback. The root rule names /index.html. - The build id hashed a list of inputs and missed the rest — the declarative built-ins (themes, grammars), the web client's CSS, workers and nls — so a changed file was served stale under the same immutable URL. The build is now staged first and named after a hash of everything staged. Checked by building: two builds give the same id; a change to the client's CSS alone gives a new one (the reviewed script gave the same id). Code quality: an unused SYSTEM_TOKENS_EST (unused on develop too), an unused import and two unused helpers in the browser tests. Tests: new suites callback, signin, headers, search-scope and review-rehydrate; main, host and workspace extend theirs (fake-idb.js is a small IndexedDB stand-in for them). The unit gate passes, 75 files, on Node 24 and Node 18. In Chrome against a build with these changes: e2e 28/28, e2e-signin-tab 20/20 (its left-behind-code case now expects the code to be delivered and NOT exchanged), e2e-byok 5/5. Not run: a real nginx (Docker was unavailable), Safari, Firefox.
… does The merge of feat/web-edition replaced the build's list of inputs with a hash of the staging folder. The test of that list now stages the extensions and hashes the result the same way, and COPY_SKIP — exported only for the list — goes.
… read in both themes
Both themes' syntax palettes (One Dark / One Light) had colours below WCAG AA's 4.5:1 on their own
editor background. Each one is moved in HSL lightness only (hue and saturation kept, within 2 degrees
after rounding) to the first value that reaches 4.5:1. theme.test.js now holds every tokenColors and
semanticTokenColors foreground of both themes to 4.5:1 on the theme's editor.background (an alpha
colour composited first); it fails on the previous themes, listing each colour.
Dark (editor #1b1c22), review comment 4274117784:
- comments, markup quotes, doc comments / doctype #5c6370 2.81 -> #7b8493 4.50
Light (editor #ffffff), review comment 4274117904:
- comments, markup quotes #a0a1a7 2.58 -> #75767e 4.52
- strings, inherited class, inline/fenced code #50a14f 3.21 -> #428541 4.50
- regexp, escapes, support types #0184bc 4.18 -> #017eb4 4.52
- variables, properties, tags, headings, JSON keys #e45649 3.67 -> #df3425 4.51
- functions, decorators, links #4078f2 4.05 -> #326ef1 4.53
- classes, types, CSS tags #c18401 3.20 -> #9f6d01 4.50
- constants, attribute names, bold #986801 4.87 -> #765101 7.12
(passed already; darkened with the class colour, which shares its hue and would otherwise be
indistinguishable from it: CIELAB delta-E 2.9, against 16.2 before and 16.9 now)
Token backgrounds: the editor does not paint them ("Token background colors are currently not
supported", colorThemeSchema.ts), so the light theme's `invalid` scope (white on a #e45649 background)
was white text on the white editor, 1.00:1. `invalid` is now the theme's red text in both themes
(light #df3425 4.51, dark #e06c75 5.32) with no background, and the test fails on any token rule that
sets one.
…of the chat, fills the window in both layouts, and works where a workspace sets it Review comment 4274117959 (the moves act on whatever is active): whenChatActive resolved on its timeout as well, so the move commands ran whether or not the chat was the active editor and could move the file beside it; and in split the second levelcode.ai.focus, after README.md was opened (and became the editor on show), was followed by the move with no wait at all. Each command that acts on the active editor (the two moves, and the join below) now goes through one guard: the tab model is read immediately before it, after waiting up to 1.5 s for the reveal to land; one that cannot be confirmed is skipped, as is every later one in that arrangement, and so is one overtaken by a newer layout request while it waited. whenChatActive resolves true/false. The wait for the chat to come to the front before the window is read at all stays: when the layout is changed by saving the setting from a settings editor opened in front of the chat, that editor's group is in the tab model until the chat is focused, and read at once the window looks like two groups already, so nothing is docked (found in a browser run of this change before it was committed; pinned by a unit case). If that wait times out, nothing is moved. Tests: chatFirst with the chat never active does not move; split waits after the README until the chat is active again and does not move if it never is (also on the reload path); a split waiting to move is dropped when chatFirst is chosen meanwhile. Review comment 4274118020 (chatFirst left file groups visible): only empty groups were closed, so a group with files (a split file editor, files put on the right) kept part of the window. Such groups are now joined into the chat's group with workbench.action.joinAllGroups, which merges into the active group and moves the editors in inactive, so no document is closed and the chat stays on show. The remaining group is then unlocked: a group the chat was docked in is auto-locked, and a lone locked group sends the next file into a new split. The usual split -> chatFirst switch (the chat alone on the right) is unchanged. Test: three arrangements with files outside the chat's group are joined and unlocked; no join when the chat is not active. Review comment 4274118061 (split in a folder without README.md): with the chat as the only tab nothing kept the first group, so the move closed it and left one group, auto-locked. An empty untitled document is opened in column one first (preserveFocus, not a preview) when there is no README and no other tab; if even that cannot be opened the chat is not moved. The existing test that pinned the old sequence for this case is changed to the new one. Review comment 4274118118 (the switch wrote only the user setting): with levelcode.web.layout in .vscode/settings.json the workspace value wins, so the status item and the commands did nothing. writeLayout() inspects the setting and updates the workspace value when there is one, the user's otherwise. A folder-level value is not a third target: the setting is window-scoped, which VS Code does not read from a multi-root folder (FOLDER_SCOPES), and in a single-folder window the folder's settings are the workspace's. Tests drive activate() and the registered commands. The vscode stand-in now always has activeTabGroup: a file shown by showTextDocument is on show until the chat's reveal lands (a test can hold the reveal back), otherwise the chat is; plus openTextDocument, inspect() and a workspace layer of settings. On the previous extension.js the 8 new or changed cases fail and the other 32 pass.
web: a chat-first look, light/dark themes and a layout switch for the browser edition
Two conflicts, both the same kind: develop (#103) and this branch each added to the same lines. - scripts/test-extensions.sh: develop gates modules/*/test/*.test.js, this branch web/test/unit/*.test.js. The gate now discovers all three trees, and its header and its "no suites matched" error name all three. - CLAUDE.md: the scripts/ line lists extension-signature.mjs and build-web.mjs; both new sections are kept, "Extensions are verified" and then "LevelCode in a tab"; the commit line names modules/ and web/, and dist-web/ as never committed. One break git could not see: extensionSignature.test.js pinned the gate's loop line exactly, so with the web glob added it failed and stopped the gate. It now checks what it is there for — modules/*/test/*.test.js is one of the loop's patterns — and still refuses a loop without it. release.yml's comment on the gate names the web suites too. The gate passes on the merged tree on macOS with Node 24: 79 test files (63 extension, 1 module, 15 web), 1,500 cases. Not run on Linux here; the pull request's CI is that run.
…ring with undefined `head` is always a string, so `head !== undefined` was always true and the condition was only ever "depth 0 or 1". Said plainly now. The check behaves as before: run alone against samples, it still fails on an unscoped rule at the top level and inside @media, and passes the real stylesheet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What
LevelCode in your browser. The same workbench and the same
levelcode-aiextension as a static site: open the page, sign in with a LevelCode account (or bring a provider key), and chat with the agent — nothing to install, no server of ours that runs code.run_command; auto-verify commandsThe agent is only offered the tools a tab can honour — the shell tools are withheld from the model rather than offered and failing, and the system prompt is built from the same lines so it never promises one.
Companion PRs: backend
systemu-net/thin.ly#447(a callback the editor's page may be sent a code at, CORS for exactly its origins,GET /web_editor) and the account site's entry pointssystemu-net/onetime#98(/web, "Open in browser", docs). All three are off until the backend is configured.How it fits
docs/WEB.mdis the reference (architecture, sign-in sequence, build, the three origins, headers, security model, what was not run). In short:scripts/build-web.mjs→ one static directory: Code-OSS's web client (vscode-web-min-ci), the browser build oflevelcode-ai, the scratch-workspace extension, the declarative built-ins,index.htmlwith its configuration baked in,_headersand an nginx fragment. Everything but the two HTML pages is under a build-addressed/_/<id>/and cached for a year.web/— the page (main.js: the embedder), the sign-in callback, the scratch workspace (levelcode-scratch:over IndexedDB, with file and text search), the browser build of the extension (esbuild + shims), the dev server, the tests.fs/path/os/crypto/child_process(private state only), andextensions/levelcode-ai/host.jsfor the workspace (async reads through the editor's file system, capability flags). On the desktop everyhostfunction does what the call site did before, behindawait; nothing outsidehost.jsasks "am I in a browser?".?signin=1/ "Sign in" →<account>/ai/loginwith the existing PKCE flow →/callback.html→ back to the editor, which delivers the result to the extension. The PKCE verifier lives in SecretStorage, so it survives the reload.Where to look
extensions/levelcode-ai/{agent,extension,reviewSession}.js,providers/anthropic.js, newhost.js— 399 insertions, 67 deletions. The changes are workspace reads and writes becomingawait host.…, a capability filter on the tool list, andhost.openAuth. Anthropic gets its browser-access header only when__LEVELCODE_BROWSER_HOST__is set.web/main.js(leaveForSignIn— the one command the page offers an extension, only to<account>/ai/…; the secret store;drain),web/callback.js,web/lib/headers.mjs(the CSP), anddocs/WEB.md#security-model.scripts/test-extensions.shnow also runsweb/test/unit/*.test.js, sotest.ymlgates them on every PR.Verified (commit
c6d838b)scripts/test-extensions.sh)_headers, against a stand-in account siteFileSystemDirectoryHandle, listed, read and written by the agent)?signin=1on a signed-in editor does nothing; a second tab uses the same session; a stranded result is delivered, an old one dropped)--product)main.test.jsandworkspace.test.jsgoes red when its code is put backThe real stack, run by hand-driven script in a real headless Chrome: Rails from
thin.ly#447in test mode (email never sent, Stripe answered locally, a throwaway Postgres database and Redis db), the real/ai/loginpage fromonetime#98(its dev server), and a release of this editor built with--account http://localhost:5199 --api-url https://…:?signin=1→ the real login page ("Connect the editor to your account…") → email code typed → back in the editor, clean address, "Signed in to LevelCode.", realexchange200, realprofileandmodels200;POST /ai/authorize_editor, then the exchange);gpt-oss-120b, under 0.1 credits) — the request crossed the CORS preflight from the editor's origin;hello.txtin the scratch workspace with the real model, both gateway calls 200;main.pyand writingcopy.pybeside it.What running it for real found, all fixed in this PR or the companions:
filescheme (5393933).9c86c94).POST /ai/authorize_editorwith the SPA shell, so "Open in browser" could not work undernpm run dev(onetime#98); and the login page treated the editor's callback page as a custom-scheme app launch, with a fallback that says the editor "may not be installed on this device" (onetime#98).The editor's own end-to-end checks use a stand-in backend (
web/test/stub-backend.mjs) that applies the redirect and CORS rules the real one must; the real rules arethin.ly#447's specs (785 examples, CI green). The real-stack script needs the other repositories' internals and is not part of this repo;docs/WEB.mdhas the steps to do it by hand.A cold first load fetches about 20 MB uncompressed (5 MB gzip) over 147 files from the editor's origin, plus the webview host page; the release directory is about 195 MB.
Review round (
633d0a8, 2026-10-14)All 16 review threads are answered in place and resolved. The changes in behaviour:
token=included. A crafted link, followed by opening the editor, could sign the user in to someone else's account. The page now passes on only a LevelCode sign-in's one-time code. In a tab, the extension exchanges a code only while a sign-in it started is pending. The desktop is unchanged./.Verified: the unit gate passes, 75 files on Node 24 and on Node 18. Headless Chrome against a release: e2e 28/28, same-tab sign-in 20/20, bring your own key 5/5. Not run: a real nginx (Docker was unavailable), Safari, Firefox.
These fixes are merged up into
feat/web-look(#106) andfeat/web-shell(#107).Not run
./scripts/run-dev.sh): the suites that runagent.jsandreviewSession.jsagainst stand-ins pass, but I did not drive the real editor. Please do before merging..github/workflowsjob for the web build and its end-to-end checks: not added (a Linux bootstrap and Chrome in CI is a separate piece, and I would not ship one I had not run).Decisions that are yours
SameSite=Lax, a site boundary; seedocs/WEB.md#deploying). Which registrable domain, and who holds it.*.view.example.com+ certificate).?signin=1.