feat(onboarding): make the browser first-run the BrowserOS onboarding - #2438
Draft
Dani Akash (DaniAkash) wants to merge 10 commits into
Draft
feat(onboarding): make the browser first-run the BrowserOS onboarding#2438Dani Akash (DaniAkash) wants to merge 10 commits into
Dani Akash (DaniAkash) wants to merge 10 commits into
Conversation
…, app theme, base-ui) Scaffold the standalone BrowserOS onboarding app, following the claw-onboard build architecture (Vite SPA with a single-file chromium-mode WebUI build and resource verifier) while wearing the app's theme tokens and base-ui components. - Vite + React 19 + Tailwind v4 (CSS-first), app theme tokens, system font stack - base-ui shadcn components generated via the shadcn CLI - dev and chromium (WebUI resource) build modes with the resource-contract verifier - biome and fallow wired to match the rest of the monorepo Base branch for the onboarding project; flow UI and native resource wiring to follow.
* feat(app-onboard): implement the 3-step onboarding flow Build the standalone onboarding flow on top of the app-onboard scaffold: - Welcome, Import, and a new "Set up your agent" handoff step. - Import reuses the chrome.send bridge and importer UI shared with the native onboarding handler, rethemed to the app palette. - The setup step shows the breadth of supported services with real provider and coding-agent brand marks, then reports completion so the native first-run can open the app's AI and Agents screen. - Motion via motion/react: a keyed directional slide per step, reduced-motion aware. The value rail and step dots carry the app theme. - A compatibility token layer maps the ported components onto the app's orange/neutral palette without per-class edits. Also tracks the onboarding icons explicitly; a broad global gitignore pattern was excluding the icon directory, so the scaffold's icons had never landed. * fix(app-onboard): make the form resolver a 1-arg function DeepScan flagged TOO_MANY_ARGS: the resolver was annotated as react-hook-form's Resolver (a 3-parameter call signature), so the schemas test could call it with 3 args even though the implementation uses only `values`. Type it via `satisfies Resolver<...>` instead, so its call arity is genuinely 1 while still conforming to the react-hook-form contract, and call it with a single argument in the test.
* ci(app-onboard): run the app-onboard checks on PRs The Tests workflow discovers suites from ci/affected-suites.ts, and app-onboard was not registered, so a PR touching it reported "0 suites affected" and never ran its tests. Add an app-onboard suite (mirrors claw-onboard) plus its package mapping so `bun run test` runs when the app is affected, and add epic/app-onboard to code-quality's branch list so Biome, Typecheck (Turbo --affected), and Fallow run on the epic PRs too. Editing ci/ is a harness change, so this run exercises the full matrix and self-verifies the discovery. * ci: drop the stale feat/claw-server-bootstrap trigger from code-quality
…#2427) * feat(app): stop opening the in-app onboarding tab on install Fresh installs opened app.html#/onboarding from the background worker. That flow is being replaced by the browser's own first-run experience, and leaving this in place would open both on a fresh profile. Only the auto-open is removed here; the routes still resolve if navigated to directly, so this stays reversible while the new flow lands. Side-panel setup on install is untouched. * feat(build): bake the app-onboard resources into BrowserOS The onboarding SPA is selected at build time and both products baked the neo onboarding. Give the BrowserOS product its own onboarding component so each product ships the flow it actually uses. - Add the app-onboard prod asset build (script, descriptor, tests) and the root build scripts, mirroring the neo onboarding pipeline. - Register app-onboard as a release component with its own resource family, R2 allocation probe, and release workflow on the app-onboard/v* tag. - Declare app-onboard as the BrowserOS product's onboarding component and gate the onboarding download and copy operations by product, so each binary receives only its own SPA. The grit pak name and id stay shared; only the source bytes differ. - Resolve the onboarding archive name and build command from the product's component instead of hardcoding the neo one. - Reserve both onboarding versions for nightlies so each product pins the version of the component it bakes. * feat(browser): run the onboarding first-run for BrowserOS too The onboarding WebUI, its importer handler, and the first-run controller are already compiled into both product binaries; only a product check in ShouldShow kept BrowserOS from ever reaching them. Now that BrowserOS bakes its own onboarding SPA, drop that check so the first-run flow runs for both products. On completion, BrowserOS appends the agent extension's AI settings page to the first-run tabs, so the browser opens where the user finishes connecting a provider or coding agent. A chrome:// document cannot navigate there itself, and BrowserOS neo keeps routing itself, so the tab is added only for BrowserOS. The earlier NeutralizeUpstreamFirstRun call is left in place: it only sets kFirstRunFinished, which ShouldShow never reads, so it cannot pre-empt the onboarding branch, and it still stands down the upstream first-run on later launches once onboarding is complete. * refactor(app): remove the retired in-app onboarding flow The browser now owns first-run, so the in-app welcome, profile, connect-apps, sign-in, and demo screens no longer have an entry point. Remove the route group and the screens behind it, along with the profile sync that only fed that flow and the analytics events only it fired. The features showcase moves out of the onboarding directory and keeps its own route, since it is reached from settings and is not part of the retired flow. Its click event keeps its current name so the existing analytics series stays continuous. Storage is trimmed to just the two keys the flow owned. The import and sign-in hint keys, the post-signin redirect path, and the first-run confetti flag all stay: they belong to the new tab and chat surfaces, which continue to read them. * fix(build): pin the app-onboard resource download to its exact version The latest-to-version rewrite is keyed by R2 prefix, and the new app-onboard family had no entry, so a published BrowserOS build kept the mutable latest selector and would embed whichever onboarding bytes that alias happened to point at instead of the version it reserved. Add the family so the key resolves like every other pinned resource, and cover it in the resolver tests, including the case where no override is supplied.
Contributor
✅ Tests passed: 2662/2665Ran 16 of 16 suites (0 not affected by this change).
|
* feat(mcp): upgrade both MCP servers to the 2026-07-28 revision (#2290) * feat(server): migrate the MCP server to the v2 SDK with dual-era 2026-07-28 support (#2272) * feat(server): migrate the MCP server to the v2 SDK with dual-era 2026-07-28 support Replace @hono/mcp and the server-side @modelcontextprotocol/sdk 1.x with @modelcontextprotocol/server and @modelcontextprotocol/hono 2.0, serving both the legacy initialize handshake and the modern 2026-07-28 protocol per request via a stateless WebStandardStreamableHTTPServerTransport. v2 derives tools/list JSON schemas only from Zod v4, so the local browser and filesystem tool schemas move to the zod/v4 subpath of the installed zod 3.25; the Klavis connector schemas are bridged through fromJsonSchema until the Strata client migration. Drop the logging capability and the SetLevel handler, both removed in 2026-07-28. The Strata MCP client stays on the 1.x SDK for a follow-up change. Refs #2168 * fix(ci): call the real tabs tool in the MCP integration test The test invoked a non-existent tool name and passed only because the 1.x server returned a lenient error result for unknown tools. The v2 server correctly rejects unknown tools (its sibling test asserts the throw), so call the registered tabs tool with a valid action instead. * refactor(server): migrate the Strata MCP client to the v2 client SDK (#2274) Move the Klavis Strata MCP client and the remaining Klavis type imports from the 1.x @modelcontextprotocol/sdk to @modelcontextprotocol/client 2.0, so apps/server no longer depends on the legacy SDK at runtime. The SDK stays as a devDependency for the integration and end-to-end tests that simulate a legacy client against the v2 server. No behavior change; the connector schema bridge is unchanged and can be cleaned up in a follow-up. Refs #2168 * chore(claw-server): upgrade rmcp to 3.x with a legacy protocol version pin (#2275) * chore(claw-server): upgrade rmcp to 3.x with a legacy protocol version pin Bump rmcp from 2.1 to 3.x. rmcp 3.0 changes the ServerHandler call_tool return type to the MRTR-aware CallToolResponse, so wrap the existing CallToolResult with .into() in the production and reference handlers and the serving test. rmcp 3.x advertises the modern 2026-07-28 protocol by default, but the claw-server's sessionless state model for that revision is not in place yet, so override supported_protocol_versions() on the production handler to pin to legacy versions (2025-11-25 and earlier) and keep this upgrade behavior-neutral. The pin is lifted together with the handle-based session model. All crates compile, cargo test passes, clippy and fmt clean. Refs #2168 * test(claw-server): prove the legacy protocol pin rejects the modern revision Add a serving-level test that initializes against a legacy-pinned handler with protocolVersion 2026-07-28 and asserts the server negotiates down to a legacy revision rather than agreeing to the modern one. This exercises the same supported_protocol_versions() override the claw-server uses and confirms rmcp consults it during initialize negotiation. * feat(claw-server): serve the modern MCP revision with a session handle (#2278) * feat(claw-server): serve the modern MCP revision with a session handle Advertise the 2026-07-28 revision alongside the legacy revisions. rmcp serves 2026-07-28 clients statelessly, so the per-agent tab-group session, previously keyed on the mcp-session-id header that revision removes, is now carried as a server-minted handle passed as an ordinary `session` tool argument: minted as a UUID when absent, returned in structuredContent, and looked up on later calls so the agent keeps its tab group. Legacy clients and stdio keep the existing session model unchanged, and idle sweeping reaps handle sessions independent of transport close. * test(claw-server): expect the injected session arg in the name_session golden schema * fix(claw-server): only reuse live server-minted session handles Resolve a modern session by reusing a handle only when it maps to a live session; any absent or unrecognized handle now mints a fresh server-generated handle instead of being minted under the caller-supplied value. A caller can no longer choose or seed a session id, so it cannot land on another agent's ownership by presenting a chosen id, and because every mint uses a fresh unique id, two concurrent calls can no longer both mint under the same id and orphan one session. Continuity flows through the returned handle. * feat(server): give /mcp clients a server-minted session identity handle (#2289) * feat(server): give /mcp clients a server-minted session identity handle Expose an optional session string argument on each /mcp browser tool. The server mints a UUID when the caller omits it and returns it in the result's structuredContent so an agent can thread it back on later calls to identify its own session; a supplied handle is echoed. The handle is stripped before the tool runs, so tool logic (including the one strict-schema tool) and the internal agent's shared tool schemas stay unchanged. It is gated behind an opt-in registration flag so only the /mcp surface carries it, and it is attributed in the per-tool execution metric. Identity only: no isolation, no per-session state, no change to browser behavior. * fix(server): attribute the /mcp session handle on the per-call log The session handle was added to the aggregated `tool_executed` metric event, whose rollup keeps only tool name, source, and success, so the handle was discarded. Move the attribution to the per-call log context instead, which is the right home for a per-session value and avoids the unbounded cardinality of a UUID in an aggregated metric. The returned handle in structuredContent is unchanged. * fix(server): serve the 2026-07-28 revision on /mcp and harden the endpoint (#2298) * fix(server): deliver the /mcp tool abort signal from the request context The browser tool wrapper read the abort signal from a top-level `extra.signal`, which does not exist on the v2 SDK's tool handler context, so it was always undefined and cancellation never reached the tool over /mcp. Read it from `extra.mcpReq.signal` where the SDK actually places it. The internal AI-SDK agent path threads its own signal and is unaffected. Adds a test that a pre-aborted signal delivered via mcpReq.signal makes a waiting tool abort. * feat(server): reject browser-originated /mcp requests via Sec-Fetch-Site The /mcp endpoint is intentionally reachable across the LAN, so a loopback bind is not an option. Browsers always send a Sec-Fetch-Site header while native MCP clients and the internal ACP client never do, so rejecting requests that carry it filters browser-originated attacks (DNS rebinding, CSRF) without restricting the bind address. Scoped to /mcp so browser-facing routes are unaffected, and mirrors the claw-server's request hygiene. * fix(server): serve the modern 2026-07-28 revision on the TS /mcp endpoint The TS agent server advertised only legacy protocol versions and rejected 2026-07-28: the McpServer was built without supportedProtocolVersions (so it never registered server/discover), and the route dispatched through the bare streamable-HTTP transport, which does not run the modern request machinery. Advertise the modern revision alongside the legacy list on the McpServer, and split the route: legacy (2025-era) requests keep the existing hand-wired transport with enableJsonResponse so the internal ACP client still gets single-JSON responses, while modern requests go through createMcpHandler, which serves server/discover and the stateless 2026-07-28 dispatch. Both eras are exercised end to end by new tests: legacy initialize negotiates 2025-11-25 as JSON, modern server/discover advertises 2026-07-28, and a modern tool call succeeds. * feat(mcp-manager): add read-only tool catalogue endpoint for settings UI (#2410) The settings Available Tools list previously ran a full MCP handshake against /mcp from the browser. With the browser-request guard on /mcp, that path is no longer reachable from the extension. Add a GET /mcp-manager/tools endpoint that returns the same tool set (browser tools plus Klavis connector tools) as name and description pairs, and point the settings section at it through a react-query hook mirroring the existing agents hook. The MCP hardening is left unchanged. * fix(klavis): pass connector tool JSON schemas through unchanged (#2412) * fix(klavis): pass connector tool JSON schemas through unchanged Strata connector tools (Linear and others) advertised an empty input schema, so every parameterized call was rejected with both "required" and "additional properties" errors and the agent could never pass arguments. Their remote JSON schema was routed through zod-from-json-schema and then zod-to-json-schema, which under zod v3 silently drops every property. Pass the remote JSON schema straight through instead: fromJsonSchema for the MCP server path and the ai jsonSchema() helper for the AI SDK tool set. The hand-built connector_mcp_servers schema is unchanged. Removes the now-unused per-session schema map. * chore(server): drop unused zod-from-json-schema dependency * fix(claw-app): drop the audit timeline risk labels and recolor the rows (#2416) The audit timeline tagged act/evaluate/run/download rows as "High risk", which is inaccurate, and highlighted them in warning-amber. Remove the label and switch the row emphasis to the theme accent (blue); errors keep their red highlight. The same tool set still auto-expands on load, now named for what it is (notable actions) rather than risk. * chore(release): update extension alpha feeds to 0.2.17.0 Automated release snapshot update. * chore: bump browserclaw extension version to 0.2.17.0 (#2421) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(claw-server): install the neo skill under browseros-neo and migrate legacy dirs (#2428) * fix(claw-server): install the neo skill under browseros-neo and migrate legacy dirs The managed skill's SKILL.md declares name: browseros-neo but was installed into a directory named browserclaw, so agents that require name == parent directory reject it (e.g. Claude Code: name "browseros-neo" does not match parent directory "browserclaw"). Install the skill under browseros-neo so the directory matches the frontmatter, and add a one-time, idempotent migration that removes the legacy browserclaw directories (only ones whose ownership marker proves BrowserOS installed them) across every harness and purges their manifest records, so the reconcile replants them under the new name. User-authored browserclaw directories are left untouched. The migration runs on every managed-skill reconcile (startup and connect), so existing users heal without any action. Marker internals (managed_by) stay stable for recognition. * fix(harness): keep legacy skill records for directories that remain Purge a legacy manifest record only once its directory is actually gone. If removal or marker inspection fails the directory stays on disk, so keeping its record leaves the manifest consistent and lets the next reconcile retry, instead of orphaning it. * fix(chat): surface the full error in a copyable block (#2434) * fix(chat): surface the full scrubbed upstream error in a copyable block When a turn fails, the classifier only forwarded the short constructed message into the error envelope's details and capped it at 500 characters, so the gateway's real reason (OpenRouter-style gateways hide it in the response body / metadata.raw) never reached the user. Errors the AI SDK could not classify carried no details at all. Forward the full upstream response body (or structured data.raw), pretty-printed as JSON, into details on every path, redacted and bounded at a larger cap so the whole body survives to be copied. The card now renders that detail always expanded under a Full error heading with a copy control that copies the entire error even when the block clips it on screen, while the classified title and message stay on top. The block is omitted when the detail only repeats the message, so a clean one-line error is not shown twice. * fix(chat): broaden secret scrubbing for the full upstream error body Forwarding the whole response body means more credential shapes can appear than the four key patterns caught. Redact value-shaped secrets on the raw text so it works whether or not the body parses as an object: JWTs, PEM private keys, Google/Slack/GitLab/GitHub tokens, URL userinfo credentials (host kept), and sensitive JSON string values matched by key name. Skewed toward over-redaction since a missed token is copied into a bug report. * feat(agents): custom ACP agent support (#2433) * feat(server): add custom ACP agent data model + migration Widen the agent type enum to include 'custom', add a nullable custom_config JSON column to acp_agents (named migration 0007) plus the bootstrap DDL and migration-history mirror, and round-trip a validated CustomAcpAgentConfig through the agent definition and store (with a new update method). * feat(server): launch custom ACP agents from a stored command Add a quote-aware command splitter and a launcher branch that runs a custom agent's full command line as given (no bundled-bun wrapping). Make the agent policy data-driven for custom agents: a per-agent registry id, stored env, system-prompt append, and full-access modes. Skip the full-access mode step when none are configured, and read the reasoning-effort key from config. * feat(server): route custom agent chats through the ACP path Include 'custom' in the conversation target-type enum and dispatch custom chat requests to processAcpMessage alongside claude and codex. * feat(server): validate custom agent config and add an update endpoint Extend POST /agents to accept and cross-validate customConfig (required for custom agents, rejected for built-ins), and add PUT /agents/:agentId so a custom agent's command can be edited. Refresh running sessions on update so the next turn re-spawns with the new command. * feat(server): probe a custom agent by explicit command Accept an optional command/env/cwd on POST /acpx/probe (command required for type 'custom') so the settings UI can validate a not-yet-saved agent and enumerate its models before the user commits. * test(server): cover custom ACP agent config, launch, routes, and probe Add a command-splitter suite and custom-agent cases across the store (config round-trip + update through the real migration), agent routes (create cross-validation + PUT), launcher (run-as-given argv, env injection, no bundled-bun wrapping), policy (per-agent registry id, stored env/prompt/modes, empty full-access), and probe (command required, forwarded). * feat(app): custom agent data layer (config type, update + probe hooks) Add customConfig to the app AcpAgent type, extend the create payload, add useUpdateAcpAgent (PUT), and a useProbeCustomAgent mutation that probes a not-yet-saved custom command for the Test-connection flow. * feat(app): custom ACP agent dialog with Test connection + popular agents Add the add/edit custom-agent dialog (name, full command line, working dir, Test-connection probe, advanced env/modes/prompt/icon), a nested Popular ACP agents helper backed by a verified registry (opencode, hermes, openclaw, pi; antigravity as docs-only), and the dashed 'Custom ACP agent' template tile. * feat(app): surface custom agents in settings and chat Wire the custom-agent dialog into the AI settings pane (template tile + edit from the agents list), render a custom agent's stored icon and command in its card, and give custom agents a distinct icon/label fallback in the adapter helpers and the chat provider selector. * test(app): cover custom agent form helpers and card rendering Add unit tests for the env/CSV parsing and config builder, and card cases for the custom-agent command, icon, and edit button. * test(server): use a neutral path in the command-splitter fixture * fix(ci): expect the custom-agent migration in the bootstrap schema test * fix(server): keep Windows backslash paths intact when splitting commands Treat backslashes as literal in the custom-agent command splitter so Windows paths like C:\Users\me\agent.exe survive; use quotes for arguments with spaces. Previously every backslash was consumed as an escape, launching a nonexistent executable path on Windows. * feat(app): real brand logos and code-block commands in popular agents Render svgl brand marks for opencode (currentColor), OpenClaw, and Antigravity in neutral tiles, with monograms for pi and Hermes (no svgl logo). Show each suggested command as a proper code chip so it reads as code, not description. * fix(server): resolve custom agents against the login-shell PATH A GUI-launched server inherits a minimal PATH that omits shell-profile additions (homebrew, nvm/fnm, ~/.local/bin), so a custom agent's binary could be unfindable even when it works in a terminal. Ask the user's login shell for its PATH once (cached, timeout-guarded) and prepend it for custom command spawns. Windows inherits the full registry PATH already, so it is unaffected. * feat(app): real brand logos across built-in and custom agents Consolidate agent brand marks (Claude, Codex, opencode, OpenClaw, Antigravity, pi) into a shared module reusing the project's svg components, and use them in the provider templates, agent cards, and the Popular-agents picker. Picking a popular agent now carries its logo onto the saved agent. Removed the manual Icon text field. Hermes keeps a monogram until its logo asset is added. * feat(app): agent brand logos in the chat selector, Hermes icon, opencode color Render brand marks in the newtab + sidepanel provider selector by threading a brand key through the chat targets. Add the Hermes icon (object-contain, from the provided asset) and give opencode its own dark/white ink instead of the tile accent color. * fix(app): brand logos in the selected-agent chip, input avatar, and header The newtab search-bar pill and input avatar and the sidepanel header rendered a generic Bot for the selected agent. Resolve the agent's brand mark from its brand key in all three, falling back to Bot when none. * fix(app): make the sidepanel agent selector look clickable The header agent selector read as a static title once it showed a brand logo. Add a border, an active (foreground) name, and a rotating chevron caret so it reads as a dropdown, matching the newtab pill. * fix(mcp): reject unknown arguments on every browser tool (#2432) * fix(mcp): reject unknown arguments on every browser tool Only tabs, windows, and history rejected unknown fields. The other 14 tools dropped them during deserialization and ran with defaults, so a misspelled argument produced a successful but quietly wrong result: screenshot {page: 1, fullpage: true} -> viewport shot, no error grep {page, pattern, maxResults: 5} -> default 50 matches, no error This is the failure mode behind #2161, and the only reason that one surfaced is that tabs happens to be strict, so the retired hidden input raised "unknown field" instead of being ignored. Add deny_unknown_fields to the 14 permissive Rust arg structs and .strict() to the matching zod schemas, which also closes a parity gap where tabs and windows were strict in Rust but permissive in TypeScript. The schema pipeline already handles this: normalize_schema_value rewrites additionalProperties: false into {"not": {}}, so the generated schemas stay free of boolean nodes. Fixes #2431 * fix(mcp): reject unknown fields on nested tool arguments too Top-level strictness did not propagate into nested argument objects, so a misspelled nested key was still silently dropped and defaulted. screenshot size {width, heigth} accepted 'heigth', defaulted height to 768, and returned a successful but wrong capture. Add deny_unknown_fields / .strict() to the nested input objects (screenshot size, act fill fields) in both the Rust and TypeScript implementations, and cover a nested unknown key in the regression test. * test(mcp): walk nested schemas in the Rust strictness check The Rust regression test only inspected the top-level additionalProperties, so the nested structs fixed in the previous commit were guarded on the TypeScript side but not the Rust side. Walk the whole generated schema instead, so any object node that still accepts unknown properties fails and the message names the offending path: screenshot accepts unknown arguments at $.properties.size Verified by removing deny_unknown_fields from ScreenshotSize alone. --------- Co-authored-by: DaniAkash <DaniAkash@users.noreply.github.com> * perf(rust): drop dependency debug info from the dev profile (#2440) A clean dev build of the workspace produces a 3.0G target directory, and every git worktree pays it in full. A strip probe on the largest dependency rlib put debug info at 81% of the artifact: 224M drops to 42M. Dependencies are not stepped into during normal work, so their DWARF is pure disk cost. Turning it off for `*` leaves workspace crates untouched, and line-tables-only on those keeps panics and backtraces resolving to file and line while dropping variable and type DWARF. Measured on a clean build, same commit, same machine: target/ 3.0G -> 1.8G (-40%) target/debug/deps 2.1G -> 1.1G build time 57.33s -> 51.00s The build is faster because there is less debug info to write. Object files carrying DWARF drop from 9964 (1349M) to 1461 (121M). Smaller artifacts also shrink the Swatinem/rust-cache entry in the test workflow, which eases pressure on the per-repo Actions cache limit. `cargo clippy --workspace --all-targets -- -D warnings` passes clean. * fix(analytics): surface unrecognized MCP clients instead of dropping them (#2439) * fix(analytics): surface unrecognized MCP clients instead of dropping them Every client whose name was not in the ~19-entry allowlist was collapsed to a single opaque "unrecognized-client" bucket, discarding the real name before it reached analytics. That hid the largest slice of real users behind one label. Split the unrecognized path into privacy-safe buckets: a blank name becomes unrecognized-empty; a name carrying structural PII markers (email, path, URL, host:port) or an opaque over-long blob becomes unrecognized-redacted; and a safe-shaped name surfaces its own slug so real long-tail clients (roo-code, librechat, 5ire, ...) become visible and can be allowlisted. The PII gate runs on the original raw, not the slug, so stripped fragments cannot leak. * refactor(analytics): record unlisted client names directly, drop the marker gate An unlisted client now records its own slug; only a blank name is reported as unrecognized-empty. Removes the PII-marker and length gates: clientInfo.name is a client-chosen implementation identifier, so recording it verbatim is the intended behaviour. * ci: resolve the bun version from package.json in every workflow (#2441) * ci: resolve the bun version from package.json in every workflow Nine setup-bun steps had no version pinned, so they installed whatever Bun was newest at run time: the whole of Code Quality (biome, typecheck, claw-api-codegen, claw-api-contract, fallow), both Tests jobs, Audit, and the Turbo cache warmer. Those are the workflows that gate merges, so a Bun release could change install layout, resolution, or test behaviour with no commit and no warning. It also let Tests pass on a Bun the release workflows never use. The remaining six hardcoded "1.3.6" in YAML, a third place to keep in sync with what the repo already declares. All fifteen now read the version from the existing source of truth: packageManager: "bun@1.3.6" setup-bun's bun-version-file accepts package.json and prefers packageManager, falling back to engines.bun. Both are present and agree, so behaviour for the six release workflows is unchanged. Bumping Bun for the entire repo is now a one-line change. Every workflow was parsed to confirm valid YAML and that each setup-bun step resolves from the file with no leftover literal: 15 steps, 0 problems. * test(release): assert the release build pins bun via the version file The release extensions workflow test asserted the literal `bun-version: "1.3.6"` in the build job. The guard's intent is that the release build pins Bun rather than floating, and that still holds; the mechanism is now `bun-version-file` reading packageManager from package.json. Point the assertion at the new form so the guard keeps its meaning instead of pinning a string that no longer appears. * fix(ci): keep the macos builder on a literal bun pin The macOS build job runs on the self-hosted builder against the persistent tree at BROWSEROS_REPO_PATH and has no actions/checkout, so GITHUB_WORKSPACE never holds the source. setup-bun resolves bun-version-file from GITHUB_WORKSPACE, finds nothing, and falls back to latest, which would have left source-mode macOS releases on a floating toolchain. Restore the literal pin for this one job and record why it cannot read the version file. A job-level audit confirms it is the only one of the fifteen setup-bun steps without a checkout ahead of it. * feat(analytics): let a session declare its task category (#2443) * feat(analytics): let a session declare its task category name_session gains an optional category argument from a fixed enum. The free-form name stays local and drives the tab group title exactly as before; only the enum category is emitted, on a new agent_session_task_declared event (task_category + client_name), so we can aggregate what kind of work sessions do without any free-form text leaving the machine. An unrecognized category is coerced to "other" so the declaration still counts; a non-string is dropped. The prompt and skill now nudge the agent to pass a category. * fix(analytics): declare task category at most once per session; fix catalog boundary test Guard the task-declared emit with a per-session once-flag so a later name_session rename does not emit a second agent_session_task_declared, which would overcount the category mix and the declaration rate. Also register the new wire name in the analytics single-source boundary test and its catalog count. * fix(ci): update name_session route test for the category argument * refactor(server): replace custom compaction with AI SDK guided pruning (#2445) * refactor(server): replace custom compaction with AI SDK guided pruning Collapse the four-stage compaction pipeline (prune, tool-output reduction, LLM summarization, sliding-window fallback) into the shape the AI SDK compaction guide recommends: one trigger threshold, one prune pass, one deterministic prune-only fallback. The sliding-window fallback is replaced by pruneMessages hard mode at all four of its former call sites, which cannot orphan a tool call from its result. LLM summarization and the split-turn dual-summary path are removed. Token estimation stays image-aware rather than adopting the guide's JSON.stringify heuristic, which would read a single base64 screenshot as roughly 250K tokens. Fixes three latent issues along the way: the chars-per-token constant was 3 in the estimator and 4 in the inflation guard, the fallback targeted a looser budget than the trigger so a compaction could succeed and re-fire immediately, and a zero or negative client-supplied context window produced a threshold every request exceeded. * fix(server): stop compaction returning a transcript over the budget Pruning has no lever against plain user and assistant text, so once the floor pass has cleared every tool exchange a prose-heavy transcript came back untouched and went to the provider over the model's limit. Verified at a 200K window: 212,003 tokens in, 212,003 tokens out against a 180,000 threshold. Adds a message-drop floor that keeps the first message and the longest recent suffix that fits. It runs only after every tool call has already been pruned, so unlike a general sliding window it cannot separate a tool call from its result: there are no pairs left to break. A single message larger than the window still cannot be shrunk, which is now surfaced on the log line rather than left as a bare provider error. Also tightens the context window guard from > 0 to >= 1. A fractional value below one passed the check and then floored to zero, producing a zero-token threshold that every step exceeded. The test that should have caught the first bug asserted only that the message count did not grow, which passes when nothing changes at all. It now asserts the transcript lands under the threshold, at three window sizes. * perf(rust): share cargo intermediates across checkouts (#2446) * perf(rust): share cargo intermediates across checkouts Every checkout compiles its own copy of the dependency graph. Anyone keeping more than one clone or worktree open pays that in full each time, around 1.6G apiece. build-dir moves only the intermediate artifacts out of the checkout, and it supports path templating, so {cargo-cache-home} resolves to CARGO_HOME and one shared location covers every checkout on a machine. Nothing absolute or machine specific is committed. target-dir was the obvious alternative and does not work here: it has no templating, cargo expands neither ~ nor $HOME, so a committed value could only be relative to the checkout. That would limit sharing to sibling directories, and because it also moves the final artifacts it would break the three places the BrowserClaw release locates a built binary. Final artifacts still land in <checkout>/target, so nothing that resolves a build output by path changes. Measured across two checkouts of the same branch: cold build 52.36s target 227M shared 1.6G second checkout 16.14s target 227M shared 2.1G A release build against a warm shared directory still produces target/release/browseros-claw-server-rs. rust-cache saves only workspace target dirs plus the registry and git caches, and never reads a build dir setting, so the shared directory is named to it explicitly. Without that, CI would recompile the dependency graph on every run. * ci(rust): warm the rust cache on main and drop it fortnightly Three related gaps around the shared cargo build directory. The Rust cache was never warm for a new pull request. Tests run only on pull_request, so rust-cache saved under a PR branch's scope, and branches cannot read each other's caches. This is the same problem the Turbo warm run already solves, and Rust was simply never covered. It matters more now that the intermediates live in a cache-directories entry: without a warm run, every PR recompiles the dependency graph. Warming alone would not have worked. rust-cache builds its key from GITHUB_JOB unless shared-key is set, and the existing keys show it: v0-rust-test-Linux-x64-<hash>-<hash> A warm job under any other name would have written a cache nothing else could read. Both steps now pin the same shared-key, workspaces, cache-directories and toolchain, since the toolchain hashes into the key too. The new warm job mirrors what the Rust suites compile, test binaries and clippy's separate artifacts, and deliberately omits -D warnings because it exists to populate a cache rather than to gate on lints. Finally, rust-cache prunes only workspace target dirs and never extra cache-directories, so the shared build directory is cached wholesale and grows without bound. It is already the larger part of the problem: v0-rust 25 entries 6.97 GB all caches 262 entries 10.35 GB against a 10 GB allowance Being over the allowance means LRU eviction is already discarding other caches. Dropping the Rust entries on the 1st and 15th keeps that bounded, matched on the prefix so nothing else is touched, and the warm workflow is dispatched straight after so no branch waits for the next merge. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Athul Nambiar <108534940+athul-22@users.noreply.github.com>
The previous sync landed as a squash, so its content arrived but its ancestry did not. Git still computed the merge base at the pre-sync commit, which made the epic PR render every change from main as part of the epic's own diff, 261 files instead of the epic's actual work. This merge carries both parents so the merge base moves to main's tip. The resulting tree is byte-identical to the current branch, so no file changes.
Adds OpenClaw and Hermes to the coding agent row and a caption making the breadth explicit, since the row can only ever show a handful of the agents that actually work. Retitles the group to 'a coding agent harness you already use'. Harness is the accurate word: what gets connected is the tool that drives a model, not the model. Both marks are inline single-colour SVGs. The app renders Hermes from a PNG, which this app cannot do: the Chromium target emits only app.js, app.css and the icon/ directory listed in its BUILD.gn, so a Vite-processed image either throws during the build or trips the data: URL guard. Hermes' mark is a detailed illustration with no single-colour form, so its chip falls back to the same monogram the app's agent picker uses.
Swaps the icon set for the one the app ships. This app was scaffolded from the neo onboarding and inherited its blue mark, so the rail was showing the wrong product. Retitles the rail to state what BrowserOS is rather than who it is for. Adds a tooltip to every provider and agent chip. The row is a wall of brand marks and several are only recognisable to people who already use them, so the name now surfaces on hover instead of only in the accessible name. Hermes ships as an icon/ resource rather than a monogram. Its mark is a full-bleed illustration, so it is referenced by runtime path and registered in the onboarding resource target; importing it would let Vite rewrite it into an asset the Chromium build rejects.
#2450) * refactor(app): rebuild the AI settings page around one default control The page had two ways to set the default provider: a select at the top and a radio list at the bottom, both writing the same state, roughly a screen apart. The select is gone. The list is the only writer, and the default row states its own status. Providers and coding agents were two components rendering the same row into the same column, already sharing one radio group name, so the split only ever showed as a visual seam. They are now one list with a kind badge. The add-a-provider grid mixed USE and ADD badges for what is one verb, and injected the agent tiles inside the provider loop at index 0, which is why the custom-agent tile sat between GitHub Copilot and Qwen Code. Entries are now grouped by how you connect (agent, subscription, api key, local), every entry has the same Add button, and a search filters across all four groups. Row actions move into an overflow menu behind fixed-width slots. Without the slots, promoting a row adds a badge and drops a button, which resized the row and shifted every other row's badges sideways. Behaviour is unchanged: the same icons, the same dialogs, the same handlers, the same radio group, and the same OAuth flows for the three sign-in templates. Descriptions and action lists moved into pure helpers so they can be tested without opening a menu. * fix(app): stop the provider row slots squeezing the name on a narrow pane The fixed trailing slots that keep the row from shifting when the default changes reserve 220px, which is more than a narrow pane can spare: at 360px that left the target name with nothing. Below the sm breakpoint the kind badge and the set-default button now drop out and the name takes the width back. Nothing is lost, since the description already names the adapter and the row itself is the control that sets the default, so the button was only ever a hover affordance for pointer users. * feat(app): show what a custom ACP agent gets you "Custom ACP agent" named the mechanism, not the outcome, and sat in a plus- sign tile the same size as the two fixed adapters beside it. Nothing on it suggested that this is the entry point for every other agent. The tile now takes the full row and carries the marks of the agents the picker actually offers, with copy naming them. Search matches on those names too. Looking for opencode or Hermes previously returned nothing, because the only entry that connects them is labelled Custom.
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.
Epic branch for the BrowserOS onboarding rework. Draft until the items under "Before this can merge" are done.
BrowserOS onboarding moves out of the extension and into the browser's own first-run experience, and shrinks to the two things a new user actually has to do: bring their browser over, then connect an agent.
What landed
chrome.sendbridge and importer UI, rethemed to the app's palette, with motion and real provider and coding-agent brand marks.Also in the branch: the app scaffold, and a merge of main.
Notable details
The native machinery was already there. The onboarding WebUI, the Chromium importer handler, and the first-run controller are compiled into both product binaries. Only a product check in
ShouldShowkept BrowserOS from reaching them, so enabling it was a small native change rather than a new subsystem.Each product now bakes its own onboarding SPA. The grit pak name and id stay shared; only the source bytes differ, selected by product at build time. app-onboard gets its own build, release component, resource family, and release workflow on the
app-onboard/v*tag.Version pinning was tightened along the way. Nightlies previously read one onboarding version and applied it to both products, and the resource resolver had no entry for the new component, so its download would have followed the mutable
latestalias. Both are fixed and covered by tests.The retired flow is gone, but its neighbours are not. The features showcase keeps its route and its analytics event name. The import and sign-in hints, the post-signin redirect, and the first-run confetti flag all stay, since the new tab and chat surfaces read them.
Validation so far
bos_build: 1137 tests, no regressions against the pre-change baseline.apps/appandapps/app-onboard: typecheck, Biome, and unit tests pass. Fallow clean.target: universalarchive matching what the copy step and the archive validator expect.Before this can merge
app-onboard/v*release, so a BrowserOS build resolving onboarding from R2 has an object to fetch. The release-time path that builds from source is already product-aware and needs no publish.