Skip to content

feat(onboarding): make the browser first-run the BrowserOS onboarding - #2438

Draft
Dani Akash (DaniAkash) wants to merge 10 commits into
mainfrom
epic/app-onboard
Draft

feat(onboarding): make the browser first-run the BrowserOS onboarding#2438
Dani Akash (DaniAkash) wants to merge 10 commits into
mainfrom
epic/app-onboard

Conversation

@DaniAkash

Copy link
Copy Markdown
Contributor

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.

Fresh install
  -> native first-run at chrome://browseros-onboarding/
       1. Welcome
       2. Import history, bookmarks, and saved logins from the current browser
       3. Set up your agent
  -> browser opens on the extension's AI and Agents page to finish setup

What landed

PR
#2413 The three-step flow: welcome, import, and the setup handoff. Reuses the existing chrome.send bridge and importer UI, rethemed to the app's palette, with motion and real provider and coding-agent brand marks.
#2414 CI for the new app: its own test suite in the affected-suites matrix, plus the quality gates on this branch.
#2427 The rollout: stop opening the in-app onboarding on install, bake the app-onboard resources into BrowserOS, run the native first-run for BrowserOS with a landing tab, and remove the retired in-app flow.

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 ShouldShow kept 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 latest alias. 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.
  • Patch stack doctor healthy: 365 patches across 34 features.
  • apps/app and apps/app-onboard: typecheck, Biome, and unit tests pass. Fallow clean.
  • The app-onboard resource build runs end to end and produces a target: universal archive matching what the copy step and the archive validator expect.
  • Merges into main cleanly.

Before this can merge

  • Verify on a real build that onboarding shows for BrowserOS, that completion opens exactly one window, and that the agent extension is loaded before the first window so the landing tab resolves.
  • Publish an 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.
  • Refresh this branch from main before merging if it drifts further.

…, 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.
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

✅ Tests passed: 2662/2665

Ran 16 of 16 suites (0 not affected by this change).

Suite Passed Failed Skipped
server-agent 265/265 0 0
server-api 170/170 0 0
server-tools 254/254 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 141/141 0 0
server-root 38/41 0 3
agent 377/377 0 0
claw-app 392/392 0 0
claw-onboard 86/86 0 0
app-onboard 66/66 0 0
build 49/49 0 0
release 65/65 0 0
claw-server-rust 630/630 0 0
claw-server-rust-quality passed 0 0
claw-mcp 109/109 0 0

passed = ran successfully but emits no JUnit counts (a lint/format gate).

View workflow run

Dani Akash (DaniAkash) and others added 5 commits August 27, 2026 14:40
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant