diff --git a/src/chrome/src/context-menu-storage.js b/src/chrome/src/context-menu-storage.js index 395c1fe96..41c01742f 100644 --- a/src/chrome/src/context-menu-storage.js +++ b/src/chrome/src/context-menu-storage.js @@ -7,10 +7,17 @@ export const SELECTION_SHORTCUT_ACTIONS = Object.freeze({ summarize: 'Summarize this selected text clearly and concisely.', 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.', + proofread: 'Proofread this selected text. First verify that the selection is complete enough to edit. Never infer or reconstruct text beyond its boundaries. If an edge is visibly cut mid-word or the passage is otherwise clearly incomplete, say so and ask the user to select the complete passage instead of supplying a corrected version. For a complete selection, tie every claimed error to exact selected wording, distinguish actual errors from optional style suggestions, and provide one complete corrected version that fixes every listed error without unrelated additions.', 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.', }); +// Some fixed actions carry model-only guardrails that should not crowd the +// user-visible chat bubble or history title. Match the complete trusted action +// text so custom prompts that happen to start similarly remain untouched. +const SELECTION_SHORTCUT_DISPLAY_INSTRUCTIONS = Object.freeze({ + proofread: 'Proofread this selected 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 @@ -168,6 +175,12 @@ export function formatSelectionPromptForDisplay(promptText) { instruction = instruction.slice(CUSTOM_QUESTION_PREFIX.length).trim(); } else { instruction = stripResponseLanguageInstruction(instruction); + for (const [actionId, displayInstruction] of Object.entries(SELECTION_SHORTCUT_DISPLAY_INSTRUCTIONS)) { + if (instruction === SELECTION_SHORTCUT_ACTIONS[actionId]) { + instruction = displayInstruction; + break; + } + } if (instruction === GENERIC_CONTEXT_MENU_INSTRUCTION) instruction = ''; } diff --git a/src/firefox/src/context-menu-storage.js b/src/firefox/src/context-menu-storage.js index 8af96f570..022304e43 100644 --- a/src/firefox/src/context-menu-storage.js +++ b/src/firefox/src/context-menu-storage.js @@ -7,10 +7,17 @@ export const SELECTION_SHORTCUT_ACTIONS = Object.freeze({ summarize: 'Summarize this selected text clearly and concisely.', 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.', + proofread: 'Proofread this selected text. First verify that the selection is complete enough to edit. Never infer or reconstruct text beyond its boundaries. If an edge is visibly cut mid-word or the passage is otherwise clearly incomplete, say so and ask the user to select the complete passage instead of supplying a corrected version. For a complete selection, tie every claimed error to exact selected wording, distinguish actual errors from optional style suggestions, and provide one complete corrected version that fixes every listed error without unrelated additions.', 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.', }); +// Some fixed actions carry model-only guardrails that should not crowd the +// user-visible chat bubble or history title. Match the complete trusted action +// text so custom prompts that happen to start similarly remain untouched. +const SELECTION_SHORTCUT_DISPLAY_INSTRUCTIONS = Object.freeze({ + proofread: 'Proofread this selected 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 @@ -168,6 +175,12 @@ export function formatSelectionPromptForDisplay(promptText) { instruction = instruction.slice(CUSTOM_QUESTION_PREFIX.length).trim(); } else { instruction = stripResponseLanguageInstruction(instruction); + for (const [actionId, displayInstruction] of Object.entries(SELECTION_SHORTCUT_DISPLAY_INSTRUCTIONS)) { + if (instruction === SELECTION_SHORTCUT_ACTIONS[actionId]) { + instruction = displayInstruction; + break; + } + } if (instruction === GENERIC_CONTEXT_MENU_INSTRUCTION) instruction = ''; } diff --git a/test/run.js b/test/run.js index 67d605f57..10a479f10 100644 --- a/test/run.js +++ b/test/run.js @@ -50109,6 +50109,13 @@ test('selection shortcut builds allowlisted prompts with an untrusted selection assert.match(prompt, /\nselected page words\n<\/untrusted_page_content>/, `${label}: ${action} should wrap only the page selection`); } + const proofread = buildSelectionPrompt('A visibly cut wor', 'proofread'); + assert.match(proofread, /Never infer or reconstruct text beyond its boundaries/, `${label}: proofreading may not invent missing source text`); + assert.match(proofread, /visibly cut mid-word/, `${label}: proofreading should detect an incomplete selection edge`); + assert.match(proofread, /tie every claimed error to exact selected wording/, `${label}: proofreading critiques must stay source-grounded`); + assert.match(proofread, /distinguish actual errors from optional style suggestions/, `${label}: proofreading should not present preferences as errors`); + assert.match(proofread, /one complete corrected version that fixes every listed error without unrelated additions/, `${label}: proofreading should reconcile its own issue list and avoid stale output`); + const localizedPreset = buildSelectionPrompt('这里有 Electron 和 Tauri', 'explain', '', 'zh'); assert.match(localizedPreset, /^Explain this selected text in plain language\. Respond in Chinese\./, `${label}: fixed selection actions should request the interface language`); assert.match(localizedPreset, /This English template does not set the reply language/, `${label}: English explain/quiz templates should not override the interface language`); @@ -51503,6 +51510,24 @@ test('selection prompt display formatter hides untrusted wrappers from the chat `${label}: fixed actions should keep their instruction and show the selection cleanly`, ); + const proofread = buildSelectionPrompt('A visibly cut wor', 'proofread'); + const proofreadDisplay = formatSelectionPromptForDisplay(proofread); + assert.equal( + proofreadDisplay, + 'Proofread this selected text.\n\nSelected text:\nA visibly cut wor', + `${label}: proofread should keep its guardrails model-facing and use a concise display label`, + ); + assert.doesNotMatch(proofreadDisplay, /Never infer|visibly cut mid-word|exact selected wording/, `${label}: proofread display must hide model-only guardrails`); + assert.ok(proofreadDisplay.indexOf('A visibly cut wor') < 140, `${label}: proofread history titles should reach the selected text before truncation`); + assert.match(proofread, /Never infer or reconstruct text beyond its boundaries/, `${label}: model prompt must retain the proofread guardrails`); + + const localizedProofread = buildSelectionPrompt('需要校对的文本', 'proofread', '', 'zh'); + assert.equal( + formatSelectionPromptForDisplay(localizedProofread), + 'Proofread this selected text.\n\nSelected text:\n需要校对的文本', + `${label}: localized proofread prompts should use the same concise display label`, + ); + const generic = buildContextMenuPrompt('native fallback'); assert.equal( formatSelectionPromptForDisplay(generic),