Skip to content

Commit f63d350

Browse files
committed
fix(firefox): implement documented global keyboard shortcuts
Port Chrome's sidepanel handleGlobalKeydown to Firefox so /help shortcuts actually work: Ctrl/Cmd+/ focus, mode switches, and Escape abort. Slash autocomplete Escape still takes precedence via defaultPrevented.
1 parent 7182c21 commit f63d350

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

src/firefox/src/ui/sidepanel.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9311,6 +9311,55 @@ async function sendToBackground(action, data = {}) {
93119311
return response;
93129312
}
93139313

9314+
async function handleGlobalKeydown(e) {
9315+
if (e.defaultPrevented) return;
9316+
9317+
// Don't steal shortcuts from other input elements (e.g. schedule form fields)
9318+
const tag = e.target?.tagName;
9319+
const isOtherFormField = e.target !== inputEl && (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT');
9320+
9321+
if (e.key === 'Escape') {
9322+
const slashMenuOpen = !!slashCommandMenuEl && !slashCommandMenuEl.classList.contains('hidden');
9323+
if (slashMenuOpen) return;
9324+
if (isProcessing) {
9325+
e.preventDefault();
9326+
abortRun();
9327+
return;
9328+
}
9329+
if (isOtherFormField) return;
9330+
}
9331+
9332+
if (isOtherFormField) return;
9333+
9334+
const mod = e.ctrlKey || e.metaKey;
9335+
9336+
// Ctrl+/ (Cmd+/ on Mac): focus input
9337+
if (mod && e.key === '/') {
9338+
e.preventDefault();
9339+
inputEl.focus();
9340+
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
9341+
return;
9342+
}
9343+
// Ctrl+Shift+A: switch to Ask mode (blocked while agent is running)
9344+
if (mod && e.shiftKey && e.key === 'A' && !isProcessing) {
9345+
e.preventDefault();
9346+
setMode('ask');
9347+
return;
9348+
}
9349+
// Ctrl+Shift+X: switch to Act mode (blocked while agent is running)
9350+
if (mod && e.shiftKey && e.key === 'X' && !isProcessing) {
9351+
e.preventDefault();
9352+
await ensureActMode();
9353+
return;
9354+
}
9355+
// Ctrl+Shift+D: switch to Dev mode (blocked while agent is running)
9356+
if (mod && e.shiftKey && e.key === 'D' && !isProcessing) {
9357+
e.preventDefault();
9358+
await ensureDevMode();
9359+
return;
9360+
}
9361+
}
9362+
93149363
// --- Mode Toggle ---
93159364

93169365
function positionModeHighlight(btn, { instant = false } = {}) {
@@ -9823,6 +9872,8 @@ if (attachBtn && fileAttachInput) {
98239872

98249873
sendBtn.addEventListener('click', sendMessage);
98259874

9875+
document.addEventListener('keydown', handleGlobalKeydown, true);
9876+
98269877
queuedMessagesEl?.addEventListener('click', (e) => {
98279878
const btn = e.target.closest('button[data-queue-action][data-queue-id]');
98289879
if (!btn) return;

test/run.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16596,6 +16596,30 @@ test('chrome sidepanel Escape abort honors slash autocomplete dismissal', () =>
1659616596
assert.equal(abortCall < recordingEscapeCall, true, 'chrome: agent-run Escape abort should take precedence over recording stop');
1659716597
});
1659816598

16599+
test('firefox sidepanel implements documented global keyboard shortcuts', () => {
16600+
const panel = fs.readFileSync(path.join(ROOT, 'src/firefox/src/ui/sidepanel.js'), 'utf8');
16601+
const locale = fs.readFileSync(path.join(ROOT, 'src/firefox/src/ui/locales/en.js'), 'utf8');
16602+
16603+
const globalHandlerStart = panel.indexOf('async function handleGlobalKeydown(e)');
16604+
const defaultPreventedGuard = panel.indexOf('if (e.defaultPrevented) return;', globalHandlerStart);
16605+
const abortCall = panel.indexOf('abortRun();', globalHandlerStart);
16606+
assert.notEqual(globalHandlerStart, -1, 'firefox: global keydown handler missing');
16607+
assert.notEqual(defaultPreventedGuard, -1, 'firefox: global keydown handler should honor consumed key events');
16608+
assert.notEqual(abortCall, -1, 'firefox: Escape abort shortcut missing');
16609+
assert.equal(defaultPreventedGuard < abortCall, true, 'firefox: consumed slash-menu Escape should not reach abortRun');
16610+
assert.match(panel, /document\.addEventListener\('keydown', handleGlobalKeydown, true\)/, 'firefox: shortcuts should run in capture phase');
16611+
assert.match(panel, /mod && e\.key === '\/'/, 'firefox: Ctrl/Cmd+/ focus shortcut missing');
16612+
assert.match(panel, /mod && e\.shiftKey && e\.key === 'A'/, 'firefox: Ask mode shortcut missing');
16613+
assert.match(panel, /mod && e\.shiftKey && e\.key === 'X'/, 'firefox: Act mode shortcut missing');
16614+
assert.match(panel, /mod && e\.shiftKey && e\.key === 'D'/, 'firefox: Dev mode shortcut missing');
16615+
assert.match(panel, /await ensureActMode\(\)/, 'firefox: Act shortcut should call ensureActMode');
16616+
assert.match(panel, /await ensureDevMode\(\)/, 'firefox: Dev shortcut should call ensureDevMode');
16617+
16618+
for (const shortcut of ['Ctrl/Cmd+/', 'Ctrl/Cmd+Shift+A', 'Ctrl/Cmd+Shift+X', 'Ctrl/Cmd+Shift+D', 'Escape']) {
16619+
assert.match(locale, new RegExp(escapeRegExpLiteral(shortcut)), `firefox: /help should mention ${shortcut}`);
16620+
}
16621+
});
16622+
1659916623
test('chrome double Escape stops active recordings from sidepanel and content pages', () => {
1660016624
const panel = fs.readFileSync(path.join(ROOT, 'src/chrome/src/ui/sidepanel.js'), 'utf8');
1660116625
const content = fs.readFileSync(path.join(ROOT, 'src/chrome/src/content/content.js'), 'utf8');

0 commit comments

Comments
 (0)