Skip to content

Commit f191d6e

Browse files
aaditagrawalt3dotggclaudecursoragent
authored
sync: port upstream usage page, mobile settings sheet, desktop zoom (#224)
* fix(desktop): zoom shortcuts no longer die when the preview browser has focus (pingdotgg#5691) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(mobile): one sheet for model and thread settings (pingdotgg#5625) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(usage): usage page reading provider transcripts across environments (pingdotgg#5684) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(desktop): zoom shortcuts no longer die when the preview browser has focus (pingdotgg#5691) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(mobile): one sheet for model and thread settings (pingdotgg#5625) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(usage): usage page reading provider transcripts across environments (pingdotgg#5684) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Theo Browne <me@t3.gg> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: aaditagrawal <aaditagrawal@users.noreply.github.com>
1 parent 012cdd1 commit f191d6e

44 files changed

Lines changed: 4982 additions & 493 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/app/DesktopLifecycle.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe("DesktopLifecycle", () => {
7878
handleBackendNotReady: Effect.void,
7979
flushMainWindowBounds: Effect.void,
8080
dispatchMenuAction: () => Effect.void,
81+
zoomMain: () => Effect.void,
8182
syncAppearance: Effect.void,
8283
});
8384

apps/desktop/src/backend/DesktopBackendPool.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function makePoolLayer(
9191
handleBackendNotReady: Effect.void,
9292
flushMainWindowBounds: Effect.void,
9393
dispatchMenuAction: () => Effect.die("unexpected menu action"),
94+
zoomMain: () => Effect.die("unexpected zoom"),
9495
syncAppearance: Effect.void,
9596
} satisfies DesktopWindow.DesktopWindow["Service"]),
9697
),

apps/desktop/src/window/DesktopApplicationMenu.test.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
8181
handleBackendNotReady: Effect.void,
8282
flushMainWindowBounds: Effect.void,
8383
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
84+
zoomMain: (direction) =>
85+
Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),
8486
syncAppearance: Effect.void,
8587
} satisfies DesktopWindow.DesktopWindow["Service"]);
8688

@@ -94,32 +96,38 @@ const makeElectronMenuLayer = (
9496
showContextMenu: () => Effect.succeed(Option.none()),
9597
} satisfies ElectronMenu.ElectronMenu["Service"]);
9698

99+
const configureMenu = (
100+
selectedAction: Deferred.Deferred<string>,
101+
applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,
102+
) =>
103+
Effect.gen(function* () {
104+
const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
105+
yield* menu.configure;
106+
}).pipe(
107+
Effect.provide(
108+
DesktopApplicationMenu.layer.pipe(
109+
Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),
110+
Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),
111+
Layer.provideMerge(desktopUpdatesLayer),
112+
Layer.provideMerge(electronDialogLayer),
113+
Layer.provideMerge(electronAppLayer),
114+
Layer.provideMerge(
115+
DesktopEnvironment.layer(environmentInput).pipe(
116+
Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
117+
),
118+
),
119+
),
120+
),
121+
);
122+
97123
describe("DesktopApplicationMenu", () => {
98124
it.effect("installs the native menu and routes Settings through DesktopWindow", () =>
99125
Effect.gen(function* () {
100126
const selectedAction = yield* Deferred.make<string>();
101127
const applicationMenuTemplate =
102128
yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();
103129

104-
yield* Effect.gen(function* () {
105-
const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
106-
yield* menu.configure;
107-
}).pipe(
108-
Effect.provide(
109-
DesktopApplicationMenu.layer.pipe(
110-
Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),
111-
Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),
112-
Layer.provideMerge(desktopUpdatesLayer),
113-
Layer.provideMerge(electronDialogLayer),
114-
Layer.provideMerge(electronAppLayer),
115-
Layer.provideMerge(
116-
DesktopEnvironment.layer(environmentInput).pipe(
117-
Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
118-
),
119-
),
120-
),
121-
),
122-
);
130+
yield* configureMenu(selectedAction, applicationMenuTemplate);
123131

124132
const template = yield* Deferred.await(applicationMenuTemplate);
125133
const fileMenu = template.find((item) => item.label === "File");
@@ -138,4 +146,38 @@ describe("DesktopApplicationMenu", () => {
138146
assert.equal(yield* Deferred.await(selectedAction), "open-settings");
139147
}),
140148
);
149+
150+
// Zoom must route through DesktopWindow.zoomMain instead of the Electron
151+
// zoom roles: the roles zoom whichever webContents has focus, which breaks
152+
// app zoom while an embedded preview WebContentsView holds focus.
153+
it.effect("routes View menu zoom to the main window instead of zoom roles", () =>
154+
Effect.gen(function* () {
155+
const selectedAction = yield* Deferred.make<string>();
156+
const applicationMenuTemplate =
157+
yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();
158+
159+
yield* configureMenu(selectedAction, applicationMenuTemplate);
160+
161+
const template = yield* Deferred.await(applicationMenuTemplate);
162+
const viewMenu = template.find((item) => item.label === "View");
163+
assert.isDefined(viewMenu);
164+
if (!Array.isArray(viewMenu.submenu)) {
165+
throw new Error("Expected View menu submenu to be an array.");
166+
}
167+
168+
assert.isUndefined(
169+
viewMenu.submenu.find((item) => item.role?.toLowerCase().includes("zoom")),
170+
);
171+
172+
const zoomIn = viewMenu.submenu.find((item) => item.label === "Zoom In");
173+
assert.isDefined(zoomIn);
174+
assert.equal(zoomIn.accelerator, "CmdOrCtrl+=");
175+
if (typeof zoomIn.click !== "function") {
176+
throw new Error("Expected Zoom In menu item to have a click handler.");
177+
}
178+
179+
zoomIn.click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);
180+
assert.equal(yield* Deferred.await(selectedAction), "zoom-in");
181+
}),
182+
);
141183
});

apps/desktop/src/window/DesktopApplicationMenu.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function
4949
yield* desktopWindow.dispatchMenuAction(action);
5050
});
5151

52+
const zoomMainWindow = Effect.fn("desktop.menu.zoomMainWindow")(function* (
53+
direction: DesktopWindow.MainWindowZoomDirection,
54+
): Effect.fn.Return<void, never, DesktopWindow.DesktopWindow> {
55+
const desktopWindow = yield* DesktopWindow.DesktopWindow;
56+
yield* desktopWindow.zoomMain(direction);
57+
});
58+
5259
const checkForUpdatesFromMenu = Effect.gen(function* () {
5360
const updates = yield* DesktopUpdates.DesktopUpdates;
5461
const electronDialog = yield* ElectronDialog.ElectronDialog;
@@ -127,6 +134,9 @@ export const make = Effect.gen(function* () {
127134
const settingsClick = () => {
128135
runMenuEffect("open-settings", dispatchMenuAction("open-settings"));
129136
};
137+
const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {
138+
runMenuEffect(`zoom-${direction}`, zoomMainWindow(direction));
139+
};
130140
const template: Electron.MenuItemConstructorOptions[] = [];
131141

132142
if (environment.platform === "darwin") {
@@ -181,10 +191,21 @@ export const make = Effect.gen(function* () {
181191
{ role: "forceReload" },
182192
{ role: "toggleDevTools" },
183193
{ type: "separator" },
184-
{ role: "resetZoom" },
185-
{ role: "zoomIn", accelerator: "CmdOrCtrl+=" },
186-
{ role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false },
187-
{ role: "zoomOut" },
194+
/*
195+
Not the zoom roles: those act on the focused webContents, so with
196+
an embedded preview WebContentsView focused they zoom the guest
197+
page and the app UI appears stuck. These always zoom the main
198+
window (see DesktopWindow.zoomMain).
199+
*/
200+
{ label: "Actual Size", accelerator: "CmdOrCtrl+0", click: zoomClick("reset") },
201+
{ label: "Zoom In", accelerator: "CmdOrCtrl+=", click: zoomClick("in") },
202+
{
203+
label: "Zoom In",
204+
accelerator: "CmdOrCtrl+Plus",
205+
visible: false,
206+
click: zoomClick("in"),
207+
},
208+
{ label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: zoomClick("out") },
188209
{ type: "separator" },
189210
{ role: "togglefullscreen" },
190211
],

apps/desktop/src/window/DesktopWindow.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export type DesktopWindowError =
6161
| ElectronWindow.ElectronWindowCreateError
6262
| PreviewManager.PreviewManagerError;
6363

64+
export type MainWindowZoomDirection = "in" | "out" | "reset";
65+
6466
export class DesktopWindow extends Context.Service<
6567
DesktopWindow,
6668
{
@@ -87,6 +89,12 @@ export class DesktopWindow extends Context.Service<
8789
readonly handleBackendNotReady: Effect.Effect<void>;
8890
readonly flushMainWindowBounds: Effect.Effect<void>;
8991
readonly dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>;
92+
// Zooms the main window's own webContents. The Electron `zoomIn`/`zoomOut`
93+
// menu roles act on whichever webContents has keyboard focus, so with an
94+
// embedded preview WebContentsView (or DevTools) focused they zoom the
95+
// guest page instead of the app UI. The menu routes here to always target
96+
// the main window.
97+
readonly zoomMain: (direction: MainWindowZoomDirection) => Effect.Effect<void>;
9098
readonly syncAppearance: Effect.Effect<void>;
9199
}
92100
>()("@t3tools/desktop/window/DesktopWindow") {}
@@ -836,6 +844,18 @@ export const make = Effect.gen(function* () {
836844

837845
send();
838846
}),
847+
zoomMain: Effect.fn("desktop.window.zoomMain")(function* (direction) {
848+
yield* Effect.annotateCurrentSpan({ direction });
849+
const window = yield* focusedMainWindow;
850+
if (Option.isNone(window) || window.value.isDestroyed()) {
851+
return;
852+
}
853+
const webContents = window.value.webContents;
854+
// Same step size as the Electron zoomIn/zoomOut menu roles.
855+
webContents.setZoomLevel(
856+
direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),
857+
);
858+
}),
839859
syncAppearance: Effect.gen(function* () {
840860
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
841861
yield* electronWindow.syncAllAppearance((window) =>

0 commit comments

Comments
 (0)