Skip to content

Commit 0b0417a

Browse files
refactor(web): Cmd+O presses the Open in editor button instead of a per-picker listener (#25667)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 82dc892 commit 0b0417a

4 files changed

Lines changed: 48 additions & 38 deletions

File tree

‎apps/web/src/appInit.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { getRouter, type AppRouter } from "./router";
4343
import { appAtomRegistry } from "./rpc/atomRegistry";
4444
import { primaryServerConfigAtom } from "./state/server";
4545
import { installChatRouteCommands } from "./shortcuts/chatRouteCommands";
46+
import { registerOpenFavoriteEditorCommand } from "./components/chat/OpenInPickerShortcut";
4647
import { resolveThreadRouteTarget } from "./threadRoutes";
4748

4849
/** Writes appearance settings onto the document, touching only the groups that changed. */
@@ -170,6 +171,7 @@ export function startAppServices(): AppRouter {
170171
startPreviewTitleRecording();
171172
startBrowserProfileReporting();
172173
installChatRouteCommands(router);
174+
registerOpenFavoriteEditorCommand();
173175
startThreadOverlayRetention(router);
174176
startMainAppLocationTracking(router);
175177
startBackgroundThreadAnnouncements(appAtomRegistry, router);

‎apps/web/src/components/chat/OpenInPicker.browser.test.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ vi.mock(import("../../remoteOpen"), async (importOriginal) => ({
6666
}));
6767

6868
import { OpenInPicker } from "./OpenInPicker";
69-
import { useOpenFavoriteEditorShortcut } from "./OpenInPickerShortcut";
69+
import {
70+
registerOpenFavoriteEditorCommand,
71+
useOpenFavoriteEditorShortcut,
72+
} from "./OpenInPickerShortcut";
7073

7174
const primary = EnvironmentId.make("primary");
7275
const selected = EnvironmentId.make("selected");
@@ -88,6 +91,9 @@ async function setup() {
8891
stack.use(_platform);
8992
const _storage = { [Symbol.dispose]: () => window.localStorage.clear() };
9093
stack.use(_storage);
94+
// The app registers this command at startup (appInit).
95+
const _openFavorite = { [Symbol.dispose]: registerOpenFavoriteEditorCommand() };
96+
stack.use(_openFavorite);
9197
const screen = stack.use(await renderUi(null));
9298
async function renderPicker(
9399
availableEditors: readonly EditorId[] = editors,
@@ -171,10 +177,10 @@ describe("host editor access", () => {
171177
state.allowed.delete(selected);
172178
state.allowed.add(primary);
173179
await click(primaryButton());
174-
const event = await pressOpenFavorite();
180+
// The shortcut is consumed and opens nothing once the grant is gone.
181+
expect((await pressOpenFavorite()).defaultPrevented).toBe(true);
175182
expect(state.run).not.toHaveBeenCalled();
176183
expect(state.setPreferred).not.toHaveBeenCalled();
177-
expect(event.defaultPrevented).toBe(false);
178184

179185
await renderPicker();
180186
expect(primaryButton().disabled).toBe(true);

‎apps/web/src/components/chat/OpenInPicker.tsx‎

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
type EnvironmentId,
77
type ResolvedKeybindingsConfig,
88
} from "@t3tools/contracts";
9-
import { memo, use, useEffect, useRef } from "react";
9+
import { memo, use, useRef } from "react";
1010
import { BackgroundThreadContext } from "./backgroundThread";
11-
import { isOpenFavoriteEditorShortcut, shortcutLabelForCommand } from "../../keybindings";
11+
import { shortcutLabelForCommand } from "../../keybindings";
12+
import { OPEN_FAVORITE_EDITOR_ATTRIBUTE } from "./OpenInPickerShortcut";
1213
import { usePreferredEditor } from "../../editorPreferences";
1314
import { editorLabelForPlatform } from "../../editorLabels";
1415
import {
@@ -281,38 +282,6 @@ export const OpenInPicker = memo(function OpenInPicker({
281282
);
282283

283284
const backgroundThread = use(BackgroundThreadContext);
284-
useEffect(() => {
285-
if (!enableShortcut || !canOpenEditor || backgroundThread) return;
286-
const handler = (e: globalThis.KeyboardEvent) => {
287-
if (e.defaultPrevented || !isOpenFavoriteEditorShortcut(e, keybindings)) return;
288-
if (!openInCwd) return;
289-
if (!preferredEditor) return;
290-
if (
291-
remote.mode === "local-exec" &&
292-
!readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)
293-
)
294-
return;
295-
296-
e.preventDefault();
297-
const label = editorLabelForPlatform(preferredEditor, navigator.platform);
298-
void openInEditor(preferredEditor).then((opened) => {
299-
if (opened) announce(`Opened in ${label}`);
300-
});
301-
};
302-
window.addEventListener("keydown", handler, true);
303-
return () => window.removeEventListener("keydown", handler, true);
304-
}, [
305-
backgroundThread,
306-
canOpenEditor,
307-
enableShortcut,
308-
environmentId,
309-
keybindings,
310-
openInCwd,
311-
openInEditor,
312-
preferredEditor,
313-
remote.mode,
314-
]);
315-
// Derive after the shortcut effect to preserve the editor list's compiler cache.
316285
const options = resolveOpenInOptions(navigator.platform, effectiveEditors);
317286
const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;
318287
const primaryLabel = isPanel ? `Open in ${primaryOption?.label ?? "editor"}` : "Open";
@@ -402,7 +371,14 @@ export const OpenInPicker = memo(function OpenInPicker({
402371
part="primary"
403372
panel={isPanel}
404373
disabled={!preferredEditor || !openInCwd || !canOpenEditor}
405-
onClick={() => openInEditor(preferredEditor)}
374+
{...(enableShortcut && !backgroundThread ? { [OPEN_FAVORITE_EDITOR_ATTRIBUTE]: "" } : {})}
375+
onClick={() => {
376+
if (!preferredEditor) return;
377+
const label = editorLabelForPlatform(preferredEditor, navigator.platform);
378+
void openInEditor(preferredEditor).then((opened) => {
379+
if (opened) announce(`Opened in ${label}`);
380+
});
381+
}}
406382
>
407383
{primaryOption?.Icon ? (
408384
<primaryOption.Icon

‎apps/web/src/components/chat/OpenInPickerShortcut.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,35 @@ import { editorLabelForPlatform } from "../../editorLabels";
66
import { isOpenFavoriteEditorShortcut } from "../../keybindings";
77
import { announce } from "../../lib/announce";
88
import { useRemoteOpenState } from "../../remoteOpen";
9+
import { registerCommands } from "../../shortcuts/dispatcher";
910
import { shellEnvironment } from "../../state/shell";
1011
import { useAtomCommand } from "../../state/use-atom-command";
1112

13+
/** Marks an OpenInPicker's primary button as the target of `editor.openFavorite`. */
14+
export const OPEN_FAVORITE_EDITOR_ATTRIBUTE = "data-open-favorite-editor";
15+
16+
/**
17+
* `editor.openFavorite` presses the marked OpenInPicker primary button, so the shortcut opens
18+
* exactly what the button on screen would. A missing or disabled button leaves the chord to the
19+
* bubble-phase fallback below. Capture phase, so a focused terminal does not swallow the chord.
20+
*/
21+
export function registerOpenFavoriteEditorCommand(): () => void {
22+
return registerCommands(
23+
{
24+
"editor.openFavorite": (event) => {
25+
if (event.repeat) return false;
26+
const button = [
27+
...document.querySelectorAll<HTMLButtonElement>(`[${OPEN_FAVORITE_EDITOR_ATTRIBUTE}]`),
28+
].at(-1);
29+
if (button === undefined || button.disabled) return false;
30+
event.preventDefault();
31+
button.click();
32+
},
33+
},
34+
{ capture: true },
35+
);
36+
}
37+
1238
export function useOpenFavoriteEditorShortcut({
1339
enabled,
1440
environmentId,

0 commit comments

Comments
 (0)