Repository navigation
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
| // Rough token estimates (chars/4) for the static prompt segments, so the context popover can break | ||
| // down "what's filling the window" — system + tools are sent on every request, the rest is messages. | ||
| const SYSTEM_TOKENS_EST = Math.round(SYSTEM_BASE.length / 4); | ||
| const SYSTEM_TOKENS_EST = Math.round(SYSTEM_BASE_HOST.length / 4); |
| // The provider settings (custom, OpenAI-compatible, at the stand-in's address) are configuration defaults of | ||
| // the development server; a release bakes its own defaults, so this runs against the development layout. | ||
| import { spawn } from 'node:child_process'; | ||
| import fs from 'node:fs'; |
| const r = e.getBoundingClientRect(); | ||
| return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; | ||
| })()`); | ||
| const clickText = async (re, label) => { |
| await page.click(r.x, r.y); | ||
| await sleep(600); | ||
| }; | ||
| const chatSession = () => { |
…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
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.
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.