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
7 changes: 6 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ new default IDs can still be migrated into existing installations.
read-only article fallback is ready after a structured blocking `pageGate`.
Webmail adapter runs preactivate the prompt-only Humanizer skill so a composed
reply is rewritten without spending a model-visible `load_skill` hop.
Selected-text runs route on the explicit `humanize` shortcut action instead,
which the durable selection scope carries across follow-up turns: they
suppress page context, so the adapter is unreliable there, and their empty
tool list leaves no `load_skill` to fall back on. The free-form selection
question box does not imply a writing request and does not activate it.
- Tool exposure: `buildSkillToolDefinitions()` reads manifests only from active
skills' Markdown bodies (never from Agent Skills frontmatter) and appends
compatible schemas to `getToolsForMode(...)` at LLM-call time, respecting
Expand Down Expand Up @@ -380,7 +385,7 @@ tracks as a successful video or hand ffmpeg work to the user.
| Find, read, copy, or enter a code visible in browser email/message content | OTP / verification-code helper | Ask, Act, Dev | Prompt-only; after loading it guides existing page tools. |
| Create and use a temporary mailbox for an unimportant signup | Disposable email (Mail.tm) | Act, Dev | Not shown to Ask. It may overlap with OTP during a verification flow, so both can be loaded. |
| Read a YouTube transcript, fetch a blocked NYTimes article, or resolve/download supported public media | FreeSkillz.xyz | Ask, Act, Dev | Ask can load the skill but still cannot see its Act-only `download_public_media` tool. |
| Draft or rewrite an email reply, message, or post the user will send | Humanizer | Ask, Act, Dev | Prompt-only; preactivated on webmail adapters and otherwise routed by catalog. Returns final text only. |
| Draft or rewrite an email reply, message, or post the user will send | Humanizer | Ask, Act, Dev | Prompt-only; preactivated on webmail adapters and on the explicit Humanize selected-text shortcut, otherwise routed by catalog. Returns final text only. |
| Look up weather or a short forecast | Open-Meteo weather | Ask, Act, Dev | Read-only tools remain subject to their manifest filters. |
| Find books, ISBNs, authors, or publication data | Open Library | Ask, Act, Dev | Read-only tools remain subject to their manifest filters. |
| Search or summarize an encyclopedia topic | Wikipedia | Ask, Act, Dev | Read-only Wikipedia REST/Action API tools; results are untrusted. |
Expand Down
10 changes: 10 additions & 0 deletions docs/fr/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ catalogue normal lorsque la demande porte sur la rédaction ou la réécriture
d'un texte. Elle ne renvoie que le texte final et n'explique pas ses
modifications, sauf si vous le demandez.

Sélectionnez du texte n'importe où et une entrée **Humanize** apparaît, dans la
fenêtre flottante comme dans le menu contextuel. Cette entrée explicite
préactive la compétence sur tous les sites ; les tours suivants de la même
conversation la conservent. Les actions prédéfinies — Summarize, Explain, Quiz
me, Proofread, Translate — et les questions libres saisies dans la zone de
sélection ne le font pas, car elles n'établissent pas une demande de rédaction
structurée. Ce routage existe parce qu'une exécution sur texte sélectionné
n'embarque aucun outil : le catalogue lui est inaccessible, et une compétence
absente au démarrage ne peut plus être chargée ensuite.

Elle ne réécrit que la prose destinée à un lecteur humain. Le contenu cité, les
adresses, les codes, les prix, les valeurs de champs de formulaire et la
formulation que vous fournissez mot pour mot restent intacts.
Expand Down
9 changes: 9 additions & 0 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ through the catalog like anywhere else. Elsewhere it loads through the normal
catalog when the request is about drafting or rewriting text. It returns only
the final text; it does not report what it changed unless you ask.

Select text anywhere and you get a **Humanize** entry, both in the popup and in
the right-click menu. That explicit entry preactivates the skill on any site;
later turns in the same selected-text conversation keep it. The canned readers
— Summarize, Explain, Quiz me, Proofread, Translate — and free-form questions
typed into the selection box do not, since they do not establish a structured
writing request. This routing exists because a selected-text run carries no
tools at all: the catalog is out of reach, so a skill that is not loaded when
the run starts cannot be loaded later.

It rewrites only prose being composed for a human reader. Quoted material,
addresses, codes, prices, form-field values, and wording you supplied verbatim
are left alone.
Expand Down
6 changes: 6 additions & 0 deletions docs/zh-CN/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ Markdown 正文前会移除 frontmatter。在设置中输入的名称及 `webbra
通过目录加载。在其他站点,当请求涉及起草或改写文本时,它按正常目录加载。它只返回最终文本;除非你主动询问,否则
不会说明改动了什么。

在任意页面选中文本后,浮窗和右键菜单里都会出现 **Humanize** 项。它会在任意站点预激活
该技能,并且同一段选中文本会话的后续轮次也会保留它。预设的阅读类操作——Summarize、
Explain、Quiz me、Proofread、Translate——以及在选区输入框中输入的自由问题不会预激活,
因为它们没有以结构化方式表明写作请求。这样路由是因为选中文本的运行完全不携带工具:技能
目录对它不可见,运行开始时没有加载的技能之后也无法再加载。

它只改写面向真人读者的正文。引用内容、地址、验证码、价格、表单字段值,以及你逐字提供
的措辞都保持不变。

Expand Down
54 changes: 43 additions & 11 deletions src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ import {
} from './workflows.js';
import { mergeRedactionFrameRegions, mapRegionsToImage, pixelateDataUrl } from './screenshot-redaction.js';
import { buildTrustedRuntimeContext, stripTrustedRuntimeContext } from './runtime-context.js';
import { SELECTION_ONLY_SOURCE_GROUNDING } from '../context-menu-storage.js';
import {
isSelectionProseAction,
normalizeSelectionAction,
SELECTION_ONLY_SOURCE_GROUNDING,
} from '../context-menu-storage.js';
import { resolveSavedDownload } from '../download-result.js';
import { executeChromeWebStoreSkillTool, isTrustedChromeWebStoreSkillTool } from '../chrome-web-store-release.js';
import { chromeProtectedPageFailure, isChromeProtectedPageDomTool } from '../chrome-protected-pages.js';
Expand Down Expand Up @@ -6726,6 +6730,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
excludedFingerprints: Array.isArray(entry.selectionGroundingScope.excludedFingerprints)
? entry.selectionGroundingScope.excludedFingerprints.filter(value => typeof value === 'string')
: [],
action: normalizeSelectionAction(entry.selectionGroundingScope.action),
});
}
if (
Expand Down Expand Up @@ -10105,8 +10110,22 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
}
}

_preactivateHumanizerSkillForRun(tabId, mode) {
if (!HUMANIZER_SKILL_SITE_ADAPTERS.has(this._activeSkillSiteAdapter(tabId))) return false;
/**
* Two independent routes, because they fail differently.
*
* An ordinary run matches the webmail adapter and can still reach the skill
* through load_skill if the match misses. A selected-text run cannot: it
* carries no tools at all, and it suppresses page context, which leaves
* lastSeenAdapter stale or empty. So it routes only on the explicit Humanize
* shortcut action, which the durable selection scope keeps across follow-up
* turns. The canned readers and general-purpose custom question box do not
* establish a writing request and would only pay for the skill body in
* tokens.
*/
_preactivateHumanizerSkillForRun(tabId, mode, { selectionOnly = false, selectionAction = '' } = {}) {
if (selectionOnly
? !isSelectionProseAction(selectionAction)
: !HUMANIZER_SKILL_SITE_ADAPTERS.has(this._activeSkillSiteAdapter(tabId))) return false;
const tier = this._resolvePromptTier();
if (tier === 'compact') return false;
const owner = this._eligibleSkills(mode, tier).find((skill) => skill.id === 'humanizer');
Expand Down Expand Up @@ -13278,6 +13297,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
excludedFingerprints: messages.slice(1).map(message =>
this._selectionGroundingMessageFingerprint(message)
),
// Only the opening turn carries the shortcut action. Store it on the
// scope so "now make it warmer" is still recognizable as the writing
// flow the user started, without re-trusting a resent field.
action: normalizeSelectionAction(runOptions?.selectionAction),
};
this.selectionGroundingScopes.set(tabId, scope);
} else if (
Expand All @@ -13291,6 +13314,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
...runOptions,
sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING,
selectionGroundingScopeStarted: explicitSelection,
selectionAction: normalizeSelectionAction(scope?.action),
};
}

Expand Down Expand Up @@ -19815,10 +19839,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
sourceBoundMessagesAtTrim = new Set(rawMessages);
sourceBoundTrimmedMessages = this._emergencyTrimModelCopy(currentModelMessages);
};
if (!selectionOnly) {
this._preactivateNyTimesSkillForRun(tabId, mode);
this._preactivateHumanizerSkillForRun(tabId, mode);
}
// NYTimes preactivation only buys a fetch tool, which a source-bound run
// cannot call; Humanizer is prompt-only and a selected-text writing
// shortcut has no other way to load it, so it runs on both paths.
if (!selectionOnly) this._preactivateNyTimesSkillForRun(tabId, mode);
this._preactivateHumanizerSkillForRun(tabId, mode, {
selectionOnly,
selectionAction: runOptions?.selectionAction,
});

const provider = this.providerManager.getActive();

Expand Down Expand Up @@ -20594,10 +20622,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
sourceBoundMessagesAtTrim = new Set(rawMessages);
sourceBoundTrimmedMessages = this._emergencyTrimModelCopy(currentModelMessages);
};
if (!selectionOnly) {
this._preactivateNyTimesSkillForRun(tabId, mode);
this._preactivateHumanizerSkillForRun(tabId, mode);
}
// NYTimes preactivation only buys a fetch tool, which a source-bound run
// cannot call; Humanizer is prompt-only and a selected-text writing
// shortcut has no other way to load it, so it runs on both paths.
if (!selectionOnly) this._preactivateNyTimesSkillForRun(tabId, mode);
this._preactivateHumanizerSkillForRun(tabId, mode, {
selectionOnly,
selectionAction: runOptions?.selectionAction,
});

const provider = this.providerManager.getActive();

Expand Down
24 changes: 21 additions & 3 deletions src/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
SELECTION_TRANSLATION_LANGUAGES,
buildContextMenuPrompt,
buildSelectionPrompt,
normalizeSelectionAction,
createContextMenuStorage,
} from './context-menu-storage.js';
import { createTabChatHandoffCoordinator } from './ui/tab-chat-persistence.js';
Expand Down Expand Up @@ -216,6 +217,7 @@ function createContextMenus() {
['explain', 'Explain'],
['quiz', 'Quiz me'],
['proofread', 'Proofread'],
['humanize', 'Humanize'],
]) {
create({ id: `${CONTEXT_MENU_ACTION_PREFIX}${action}`, parentId: CONTEXT_MENU_ASK_SELECTION_ID, title, contexts: ['selection'] });
}
Expand Down Expand Up @@ -1208,11 +1210,14 @@ async function handleContextMenuAsk(info, tab) {
}

let text = '';
let selectionAction = '';
if (menuItemId === CONTEXT_MENU_GENERIC_ASK_ID) {
text = buildContextMenuPrompt(info.selectionText);
} else if (menuItemId.startsWith(CONTEXT_MENU_ACTION_PREFIX)) {
text = buildSelectionPrompt(info.selectionText, menuItemId.slice(CONTEXT_MENU_ACTION_PREFIX.length));
selectionAction = normalizeSelectionAction(menuItemId.slice(CONTEXT_MENU_ACTION_PREFIX.length));
text = buildSelectionPrompt(info.selectionText, selectionAction);
} else if (menuItemId.startsWith(CONTEXT_MENU_TRANSLATE_PREFIX)) {
selectionAction = 'translate';
text = buildSelectionPrompt(info.selectionText, 'translate', '', menuItemId.slice(CONTEXT_MENU_TRANSLATE_PREFIX.length));
}
if (!text) return;
Expand All @@ -1222,6 +1227,7 @@ async function handleContextMenuAsk(info, tab) {
tabId: tab.id,
text,
sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING,
...(selectionAction ? { selectionAction } : {}),
createdAt: Date.now(),
};

Expand All @@ -1245,6 +1251,7 @@ chrome.contextMenus?.onClicked?.addListener?.((info, tab) => {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg?.type !== 'WB_SELECTION_SHORTCUT_SUBMIT') return;
const tab = sender?.tab;
const selectionAction = normalizeSelectionAction(msg.action);
const text = buildSelectionPrompt(msg.selectionText, msg.action, msg.question, msg.language);
if (!tab?.id || !text) {
sendResponse({ ok: false, queued: false, requiresManualOpen: false, error: 'Invalid selection shortcut request.' });
Expand All @@ -1256,6 +1263,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
tabId: tab.id,
text,
sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING,
...(selectionAction ? { selectionAction } : {}),
createdAt: Date.now(),
};

Expand Down Expand Up @@ -2274,7 +2282,12 @@ async function handleMessage(msg, sender) {
...(msg.recommendedAction ? { recommendedAction: msg.recommendedAction } : {}),
...(msg.foreground ? { foreground: true } : {}),
...(msg.sourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
? { sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING }
? {
sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING,
...(normalizeSelectionAction(msg.selectionAction)
? { selectionAction: normalizeSelectionAction(msg.selectionAction) }
: {}),
}
: {}),
locale: msg.locale,
intentFailureMessage: msg.intentFailureMessage,
Expand Down Expand Up @@ -2394,7 +2407,12 @@ async function handleMessage(msg, sender) {
...(msg.recommendedAction ? { recommendedAction: msg.recommendedAction } : {}),
...(msg.foreground ? { foreground: true } : {}),
...(msg.sourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
? { sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING }
? {
sourceGrounding: SELECTION_ONLY_SOURCE_GROUNDING,
...(normalizeSelectionAction(msg.selectionAction)
? { selectionAction: normalizeSelectionAction(msg.selectionAction) }
: {}),
}
: {}),
locale: msg.locale,
intentFailureMessage: msg.intentFailureMessage,
Expand Down
5 changes: 4 additions & 1 deletion src/chrome/src/content/selection-shortcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@
transform:translateY(-1px);
}
.popup {
position:fixed; width:min(${POPUP_WIDTH}px,calc(100vw - 16px)); padding:12px;
position:fixed; width:min(${POPUP_WIDTH}px,calc(100vw - 16px));
max-height:calc(100vh - 16px); overflow-y:auto; overscroll-behavior:contain;
padding:12px;
border:1px solid var(--border); border-radius:16px; background:var(--bg);
color:var(--text); box-shadow:var(--shadow); pointer-events:auto;
font:15px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
Expand Down Expand Up @@ -201,6 +203,7 @@
<button class="action" type="button" data-action="explain">Explain</button>
<button class="action" type="button" data-action="quiz">Quiz me</button>
<button class="action" type="button" data-action="proofread">Proofread</button>
<button class="action" type="button" data-action="humanize">Humanize</button>
<button class="action" type="button" data-action="translate">Translate</button>
</div>
<div class="question-wrap">
Expand Down
17 changes: 17 additions & 0 deletions src/chrome/src/context-menu-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,25 @@ export const SELECTION_SHORTCUT_ACTIONS = Object.freeze({
explain: 'Explain this selected text in plain language.',
quiz: 'Quiz me on this selected text. Ask one question at a time and wait for my answer.',
proofread: 'Proofread this selected text. Identify errors and provide a corrected version while preserving its meaning and tone.',
humanize: 'Rewrite this selected text so it reads as human writing rather than AI output. Keep every claim, the language, and the author\'s intent; return only the rewritten text.',
});

// Selected-text runs carry no tools, so `load_skill` cannot rescue a writing
// request mid-run: a prose skill either rides in at run start or never. Keep
// this limited to explicit structured writing actions; `custom` is the
// general-purpose question box and does not imply a rewrite request.
export const SELECTION_PROSE_ACTIONS = Object.freeze(['humanize']);

export function normalizeSelectionAction(value) {
const action = String(value == null ? '' : value).trim();
if (action === 'custom' || action === 'translate') return action;
return Object.prototype.hasOwnProperty.call(SELECTION_SHORTCUT_ACTIONS, action) ? action : '';
}

export function isSelectionProseAction(value) {
return SELECTION_PROSE_ACTIONS.includes(normalizeSelectionAction(value));
}

// Structured provenance for selected-text shortcuts. Keep this independent
// from localized/user-visible prompt wording so downstream code never has to
// infer the source boundary with regexes or language-specific keywords.
Expand Down
14 changes: 12 additions & 2 deletions src/chrome/src/ui/context-menu-prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* The Chrome and Firefox copies of this file are identical — edit both together.
*/

import { SELECTION_ONLY_SOURCE_GROUNDING } from '../context-menu-storage.js';
import { SELECTION_ONLY_SOURCE_GROUNDING, normalizeSelectionAction } from '../context-menu-storage.js';

export function createContextMenuPromptHandler({
getCurrentTabId,
Expand Down Expand Up @@ -36,7 +36,16 @@ export function createContextMenuPromptHandler({
const sourceGrounding = payload?.sourceGrounding === SELECTION_ONLY_SOURCE_GROUNDING
? SELECTION_ONLY_SOURCE_GROUNDING
: null;
return { id, tabId, text, ...(sourceGrounding ? { sourceGrounding } : {}) };
// Only a source-bound prompt has a shortcut action to report; anything
// else would be an unattributed id riding into the run options.
const selectionAction = sourceGrounding ? normalizeSelectionAction(payload?.selectionAction) : '';
return {
id,
tabId,
text,
...(sourceGrounding ? { sourceGrounding } : {}),
...(selectionAction ? { selectionAction } : {}),
};
}

function contextMenuPromptMatchesCurrentTab(payload) {
Expand Down Expand Up @@ -206,6 +215,7 @@ export function createContextMenuPromptHandler({
rejectedClaim = result || { reason: 'claim-lost' };
},
...(payload.sourceGrounding ? { sourceGrounding: payload.sourceGrounding } : {}),
...(payload.selectionAction ? { selectionAction: payload.selectionAction } : {}),
});
} catch { /* storage recovery can retry the prompt later */ }
runningContextMenuPromptId = null;
Expand Down
Loading
Loading