Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ word list: Discord's channel composer is found by its channelTextArea
container and a channel or contact is named by the longest text of its
selected rail link that the composer's label also contains, LinkedIn's
conversation list by its class, a formatting toolbar by its role, WhatsApp's
contact drawer closes through its header's one named button, and a phone
contact drawer closes through the named button that leads its header (the one before its title when the header also holds Edit), and a phone
number in a composer's label is taken by its digits.

Chrome's one trusted retry click on a generic element whose synthetic click
Expand Down
8 changes: 4 additions & 4 deletions docs/lean-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ For verification, lean mode follows the Claude in Chrome model ([claude-chrome-c

Settings → Advanced → **Lean mode** (storage key `leanMode`):

- **Auto** (default): on for strong cloud models, off for local and small models (rules below).
- **Auto** (default): on for strong cloud models, including those WebBrain Companion or a local CLIProxyAPI relays; off for local and small models (rules below).
- **On**: always, for every provider and tier.
- **Off**: never.

Expand All @@ -19,16 +19,16 @@ It applies to Act mode only. Ask, Dev and the standalone chat keep their behavio
The resolved mode is recorded:

- in the trace's runtime config, as `lean_mode_setting` (`auto`, `on` or `off`) and `lean_mode` (`true` or `false`);
- in a `lean_mode` trace note at the start of the run, with the reason (for example `strong_cloud_model`, `local_provider`, `prompt_tier`, `model_not_listed`) and the model family;
- in a `lean_mode` trace note at the start of the run, with the reason (for example `strong_cloud_model`, `strong_relay_model`, `local_provider`, `prompt_tier`, `model_not_listed`) and the model family;
- in the run's end details (`onRunFinished`'s `leanMode`), which the scheduler also keeps for scheduled runs.

## Auto rules

Auto turns lean mode on only when all of these hold (`src/*/src/agent/lean-mode.js`):

1. **The run is in Act mode.**
2. **The provider is a cloud or router provider.** The category WebBrain gives the provider decides, so Ollama Cloud is a cloud provider. A local provider never gets lean mode under Auto, whatever model it serves, and a provider of unknown category does not either. A provider config that names no category is local when it is llama.cpp, Ollama, LM Studio or WebGPU.
3. **The provider's prompt tier is Full.** Auto keeps WebBrain's checks for a provider the user set to Compact or Mid.
2. **The provider is a cloud or router provider, or a local relay to the cloud.** The category WebBrain gives the provider decides, so Ollama Cloud is a cloud provider. A local provider never gets lean mode under Auto, whatever model it serves (a local model can carry a strong model's name), and a provider of unknown category does not either. A provider config that names no category is local when it is llama.cpp, Ollama, LM Studio or WebGPU. The exception is the two local relays, WebBrain Companion (`webbrain_companion`) and the Local OpenAI-compatible Proxy (`local_openai_proxy`): their address is on this computer, but they forward to the cloud subscriptions the user signed in with (both are CLIProxyAPI), so Auto judges them by the model they serve, as it does a router, and the trace note says `strong_relay_model`. The one model the Companion runs itself, Bonsai, is not on the strong list.
3. **The provider's prompt tier is Full.** Auto keeps WebBrain's checks for a provider the user set to Compact or Mid. A relay's tier is not read: like every local provider it defaults to Mid, and the settings page saves that default with the provider, so it says nothing about the model.
4. **The model id is on the strong-model list:**
- Claude Opus and Sonnet, generation 4 and later, and Claude Fable and Mythos (`claude-sonnet-4-5`, `claude-opus-4-1-…`, `claude-fable-5-1`, `anthropic/claude-sonnet-4.5`, Bedrock ids);
- GPT-5 and later, including gpt-6 variants, except nano models;
Expand Down
9 changes: 6 additions & 3 deletions docs/native-chat-observation.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,12 @@ the right chat. The probe reports that kind of request
already pinned to a verified chat identity, never moves its anchor. Once a check
is armed, a later selection change spends it.

The drawer's close control is its header's one named plain button, whatever
language names it ("Close", "Kapat"); a header with two controls offers none,
and the control must keep the name it had when the guarded click armed it.
The drawer's close control is the named plain button that leads its header,
whatever language names it ("Close", "Kapat"): the header's one button, or,
where the header also holds actions after its title (a saved contact's drawer
reads Close, "Contact info", Edit), the one button before that title. A header
without one title, or with more than one button before it, offers none, and the
control must keep the name it had when the guarded click armed it.

Contact-drawer media loaders and the Close icon can update unrelated visual
content without changing identity. The original subtitle and phone nodes,
Expand Down
11 changes: 7 additions & 4 deletions src/chrome/src/agent/adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -16397,10 +16397,13 @@ const ADAPTERS = [
fullPageCapture: { infiniteScroll: isRedditInfiniteScrollUrl },
notes: `
- Prefer old.reddit.com — its DOM is simpler and easier to automate. If you land on www.reddit.com or new.reddit.com, navigate to the old.reddit.com equivalent first (swap the subdomain, keep the path).
- old.reddit.com and new.reddit.com have completely different DOMs. Check the URL before assuming selectors.
- Comments are nested deeply; "load more comments" links require clicking to expand.
- Voting requires being logged in; the up/down arrows are siblings of each post/comment.
- Post composer: title, body (markdown editor), flair selector. "Submit" button is at the bottom.`,
- On old.reddit.com /r/<subreddit>/submit, choose the "text" tab for a text/self post before filling the separate title and Markdown body textareas. Modern Reddit has a different composer; check the URL first.
- Before opening old Reddit's flair picker, scroll the "choose a flair" row into the upper half of the viewport so the popup and its bottom button fit. After scrolling, observe the new positions before clicking.
- Old Reddit post flair is a two-step popup, not a native select: click "select", choose the appropriate flair, then explicitly click the popup's "apply" button. Clicking an option only previews it; clicking outside dismisses the popup without applying it.
- Choosing a flair inserts a preview and "(remove flair)" row ABOVE "apply", moving that button DOWN. Re-read the picker for a fresh AX reference in element mode, or call take_screenshot in vision mode, then single-click the actual current "apply" button. Do not reuse its pre-selection coordinates, press Enter on the option, or double-click the option to apply it.
- A flair chip can appear on the main form even before it is applied. After explicitly clicking "apply", verify that the popup closes and the intended flair remains. If submit reports "Your post must contain post flair.", preserve the title/body, reopen the picker and explicitly apply the intended flair before retrying submit.
- Submit only after applying any required flair. Verify the resulting /r/<subreddit>/comments/<id>/ permalink and posted title/body in the intended community before marking that item published or moving to the next subreddit; observe the result before retrying a submit.
- Comments are nested deeply; "load more comments" links require clicking to expand.`,
},
{
name: 'youtube',
Expand Down
39 changes: 27 additions & 12 deletions src/chrome/src/agent/lean-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,21 @@ export function strongModelFamily(modelId) {
}

// Providers that run on the user's machine or in the browser. Auto never
// turns lean mode on for them, whatever model they serve. The type and id
// lists only classify a provider whose config names no category.
// turns lean mode on for them, whatever model they serve: a local model may
// carry a strong model's name ("...-claude-sonnet-4-distill"). The type and
// id lists only classify a provider whose config names no category.
const LOCAL_PROVIDER_TYPES = Object.freeze(['llamacpp', 'webgpu']);
const LOCAL_PROVIDER_ID = /^(?:llamacpp|ollama|lmstudio|webgpu)(?:$|[_-])/;

// Local relays: apps on this computer that forward to the cloud subscriptions
// the user signed in with. WebBrain Companion embeds CLIProxyAPI, and the
// Local OpenAI-compatible Proxy is CLIProxyAPI on its own. Their category is
// local (their address is), but the model that answers is the cloud model
// they serve, so Auto judges them by that model, as it does a router. The one
// model the Companion runs itself (Bonsai) is not on the strong list.
const LOCAL_RELAY_PROVIDER_NAMES = Object.freeze(['webbrain-companion', 'local-openai-proxy']);
const LOCAL_RELAY_PROVIDER_ID = /^(?:webbrain_companion|local_openai_proxy)(?:$|[_-])/;

// The first value that is a non-blank string, read lazily: a provider's
// getters can throw, and a blank configured value (model: '') means "not
// set", so the provider's own default applies.
Expand All @@ -84,27 +94,32 @@ export function leanProviderInfo(provider) {
const category = firstText(() => config.category, () => provider?.category).trim().toLowerCase();
const type = firstText(() => config.type, () => provider?.type).trim().toLowerCase();
const providerId = firstText(() => config._providerId, () => provider?.providerId).trim().toLowerCase();
const providerName = firstText(() => config.providerName, () => provider?.providerName).trim().toLowerCase();
// A blank configured model is unset: the provider's model getter returns
// the model it actually calls (AnthropicProvider defaults to its own).
const model = firstText(() => config.model, () => provider?.model).trim();
const promptTier = firstText(() => provider?.promptTier, () => config.promptTier).trim().toLowerCase();
const relay = LOCAL_RELAY_PROVIDER_NAMES.includes(providerName) || LOCAL_RELAY_PROVIDER_ID.test(providerId);
// An explicit category decides, as ProviderManager.categoryFor does: the
// built-in Ollama Cloud is 'cloud' although its id starts with "ollama-".
// The type and id lists only classify a config written without one.
const remote = category === 'cloud' || category === 'router';
const local = category === 'local'
|| (!remote && (LOCAL_PROVIDER_TYPES.includes(type) || LOCAL_PROVIDER_ID.test(providerId)));
const cloud = !local && remote;
return { category, type, providerId, model, promptTier, local, cloud };
const local = !relay && (category === 'local'
|| (!remote && (LOCAL_PROVIDER_TYPES.includes(type) || LOCAL_PROVIDER_ID.test(providerId))));
const cloud = !local && (remote || relay);
return { category, type, providerId, providerName, model, promptTier, relay, local, cloud };
}

/**
* Whether lean mode applies to a run. Lean mode is an Act-mode setting; Ask
* and Dev keep their own behavior. 'on' and 'off' are the user's choice.
* 'auto' needs a cloud provider (not llama.cpp, Ollama, LM Studio, WebGPU or
* any other local provider) serving a listed strong model, and the
* provider's prompt tier vetoes it: a provider the user set to the Compact or
* Mid tier is never lean under Auto.
* 'auto' needs a cloud provider, or a local relay to the cloud (WebBrain
* Companion, the Local OpenAI-compatible Proxy), serving a listed strong
* model; never llama.cpp, Ollama, LM Studio, WebGPU or any other local
* provider. The provider's prompt tier vetoes it: a provider the user set to
* the Compact or Mid tier is never lean under Auto. A relay's tier is not
* read: like every local provider it defaults to Mid, and the settings page
* saves that default with the provider, so it says nothing about the model.
* Returns { active, setting, reason, family? }.
*/
export function resolveLeanMode(setting, { mode = 'act', provider = null } = {}) {
Expand All @@ -115,12 +130,12 @@ export function resolveLeanMode(setting, { mode = 'act', provider = null } = {})
const info = leanProviderInfo(provider);
if (info.local) return { active: false, setting: value, reason: 'local_provider' };
if (!info.cloud) return { active: false, setting: value, reason: 'unknown_provider' };
if (info.promptTier === 'compact' || info.promptTier === 'mid') {
if (!info.relay && (info.promptTier === 'compact' || info.promptTier === 'mid')) {
return { active: false, setting: value, reason: 'prompt_tier' };
}
const family = strongModelFamily(info.model);
if (!family) return { active: false, setting: value, reason: 'model_not_listed' };
return { active: true, setting: value, reason: 'strong_cloud_model', family };
return { active: true, setting: value, reason: info.relay ? 'strong_relay_model' : 'strong_cloud_model', family };
}

// Quality guards that keep running in lean mode, with the reason. Each one
Expand Down
25 changes: 19 additions & 6 deletions src/chrome/src/content/chat-observation.js
Original file line number Diff line number Diff line change
Expand Up @@ -627,16 +627,29 @@
|| /^\+[\d ()-]{7,40}$/.test(compact(node.innerText || node.textContent, 80)));
});
};
// The contact drawer's close control, in any language: the one named plain
// button in the drawer's own header (WhatsApp names it in the page's
// language). A drawer that was removed is judged as it was when it closed.
// The contact drawer's close control, in any language (WhatsApp names it in
// the page's language): the named plain button that leads the drawer's own
// header. That is its one button, or, where the header also holds actions
// after its title, the one button before that title. A saved contact's
// drawer reads Close, "Contact info", Edit: taking only a header with one
// button left that Close blocked, the very step the guard names, so a
// saved contact could never be confirmed. A header without one title, or
// with more than one button before it, still offers none. A drawer that
// was removed is judged as it was when it closed.
const whatsappDrawerClose = drawer => {
if (!drawer) return null;
const shown = node => !drawer.isConnected || whatsappRendered(node);
const header = query(drawer, 'header').find(shown);
const buttons = header ? query(header, 'button').filter(button => shown(button)
&& button.closest('[data-testid="chat-info-drawer"]') === drawer && button.closest('header') === header) : [];
const button = buttons.length === 1 ? buttons[0] : null;
const own = node => shown(node) && node.closest('[data-testid="chat-info-drawer"]') === drawer
&& node.closest('header') === header;
const buttons = header ? query(header, 'button').filter(own) : [];
let button = buttons.length === 1 ? buttons[0] : null;
if (buttons.length > 1) {
const titles = query(header, 'h1,h2,h3,h4,h5,h6,[role="heading"]').filter(own);
const leading = titles.length === 1 ? buttons.filter(candidate => !candidate.contains(titles[0])
&& (candidate.compareDocumentPosition(titles[0]) & Node.DOCUMENT_POSITION_FOLLOWING)) : [];
button = leading.length === 1 ? leading[0] : null;
}
return button && attribute(button, 'aria-label') && button.type === 'button' && !button.disabled
&& !button.form && !button.hasAttribute('form') ? button : null;
};
Expand Down
11 changes: 7 additions & 4 deletions src/firefox/src/agent/adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -16396,10 +16396,13 @@ const ADAPTERS = [
fullPageCapture: { infiniteScroll: isRedditInfiniteScrollUrl },
notes: `
- Prefer old.reddit.com — its DOM is simpler and easier to automate. If you land on www.reddit.com or new.reddit.com, navigate to the old.reddit.com equivalent first (swap the subdomain, keep the path).
- old.reddit.com and new.reddit.com have completely different DOMs. Check the URL before assuming selectors.
- Comments are nested deeply; "load more comments" links require clicking to expand.
- Voting requires being logged in; the up/down arrows are siblings of each post/comment.
- Post composer: title, body (markdown editor), flair selector. "Submit" button is at the bottom.`,
- On old.reddit.com /r/<subreddit>/submit, choose the "text" tab for a text/self post before filling the separate title and Markdown body textareas. Modern Reddit has a different composer; check the URL first.
- Before opening old Reddit's flair picker, scroll the "choose a flair" row into the upper half of the viewport so the popup and its bottom button fit. After scrolling, observe the new positions before clicking.
- Old Reddit post flair is a two-step popup, not a native select: click "select", choose the appropriate flair, then explicitly click the popup's "apply" button. Clicking an option only previews it; clicking outside dismisses the popup without applying it.
- Choosing a flair inserts a preview and "(remove flair)" row ABOVE "apply", moving that button DOWN. Re-read the picker for a fresh AX reference in element mode, or call take_screenshot in vision mode, then single-click the actual current "apply" button. Do not reuse its pre-selection coordinates, press Enter on the option, or double-click the option to apply it.
- A flair chip can appear on the main form even before it is applied. After explicitly clicking "apply", verify that the popup closes and the intended flair remains. If submit reports "Your post must contain post flair.", preserve the title/body, reopen the picker and explicitly apply the intended flair before retrying submit.
- Submit only after applying any required flair. Verify the resulting /r/<subreddit>/comments/<id>/ permalink and posted title/body in the intended community before marking that item published or moving to the next subreddit; observe the result before retrying a submit.
- Comments are nested deeply; "load more comments" links require clicking to expand.`,
},
{
name: 'youtube',
Expand Down
Loading
Loading