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
9 changes: 8 additions & 1 deletion KEYBINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`](
{ "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },
{ "key": "mod+o", "command": "editor.openFavorite" }
{ "key": "mod+o", "command": "editor.openFavorite" },
{ "key": "mod+=", "command": "view.zoomIn" },
{ "key": "mod+plus", "command": "view.zoomIn" },
{ "key": "mod+-", "command": "view.zoomOut" },
{ "key": "mod+0", "command": "view.zoomReset" }
]
```

Expand All @@ -54,6 +58,9 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged
- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
- `editor.openFavorite`: open current project/worktree in the last-used editor
- `view.zoomIn`: scale the whole UI up one step (persisted to localStorage)
- `view.zoomOut`: scale the whole UI down one step (persisted to localStorage)
- `view.zoomReset`: reset the UI zoom back to 100 %
- `script.{id}.run`: run a project script by id (for example `script.test.run`)

### Key Syntax
Expand Down
44 changes: 40 additions & 4 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
const SET_WINDOW_BUTTON_VISIBILITY_CHANNEL = "desktop:set-window-button-visibility";
const SET_ZOOM_FACTOR_CHANNEL = "desktop:set-zoom-factor";
const ZOOM_MIN = 0.75;
const ZOOM_MAX = 1.75;
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand Down Expand Up @@ -648,10 +651,30 @@ function configureApplicationMenu(): void {
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+=" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false },
{ role: "zoomOut" },
// Dispatch to the renderer instead of using Electron's role-based zoom.
// The renderer owns the persisted zoom factor; native roles would
// bypass our storage and the two would drift.
{
label: "Actual Size",
accelerator: "CmdOrCtrl+0",
click: () => dispatchMenuAction("view-zoom-reset"),
},
{
label: "Zoom In",
accelerator: "CmdOrCtrl+=",
click: () => dispatchMenuAction("view-zoom-in"),
},
{
label: "Zoom In",
accelerator: "CmdOrCtrl+Plus",
visible: false,
click: () => dispatchMenuAction("view-zoom-in"),
},
{
label: "Zoom Out",
accelerator: "CmdOrCtrl+-",
click: () => dispatchMenuAction("view-zoom-out"),
},
{ type: "separator" },
{ role: "togglefullscreen" },
],
Expand Down Expand Up @@ -1195,6 +1218,19 @@ function registerIpcHandlers(): void {
// applies the value through a CSS custom-property.
});

ipcMain.removeHandler(SET_ZOOM_FACTOR_CHANNEL);
ipcMain.handle(SET_ZOOM_FACTOR_CHANNEL, async (event, rawFactor: unknown) => {
// Scale the requesting webContents. We clamp to the same [0.75, 1.75]
// range the renderer enforces so a malicious or buggy call can't drive
// the UI into an unreadable state.
const contents = event.sender;
if (!contents || typeof contents.setZoomFactor !== "function") return;
const numeric = typeof rawFactor === "number" ? rawFactor : Number(rawFactor);
const factor = Number.isFinite(numeric) ? numeric : 1;
const clamped = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, factor));
contents.setZoomFactor(clamped);
});

ipcMain.removeHandler(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL);
ipcMain.handle(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL, async (event, rawVisible: unknown) => {
if (process.platform !== "darwin" || typeof rawVisible !== "boolean") {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
const SET_WINDOW_BUTTON_VISIBILITY_CHANNEL = "desktop:set-window-button-visibility";
const SET_ZOOM_FACTOR_CHANNEL = "desktop:set-zoom-factor";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand Down Expand Up @@ -42,6 +43,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
setSidebarOpacity: (opacity) => ipcRenderer.invoke(SET_SIDEBAR_OPACITY_CHANNEL, opacity),
setWindowButtonVisibility: (visible) =>
ipcRenderer.invoke(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL, visible),
setZoomFactor: (factor) => ipcRenderer.invoke(SET_ZOOM_FACTOR_CHANNEL, factor),
showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position),
openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url),
onMenuAction: (listener) => {
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";

import {
AppSettingsSchema,
clampSidebarProjectRowHeight,
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE,
DEFAULT_SIDEBAR_FONT_SIZE,
Expand All @@ -11,6 +12,8 @@ import {
DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT,
getProviderStartOptions,
resolveBrowserPreviewStartPageUrl,
SIDEBAR_PROJECT_ROW_HEIGHT_MAX,
SIDEBAR_PROJECT_ROW_HEIGHT_MIN,
} from "./appSettings";

describe("AppSettingsSchema", () => {
Expand Down Expand Up @@ -62,6 +65,34 @@ describe("AppSettingsSchema", () => {
});
});

describe("clampSidebarProjectRowHeight", () => {
it("exposes the expected accessibility-minded bounds", () => {
expect(SIDEBAR_PROJECT_ROW_HEIGHT_MIN).toBe(32);
expect(SIDEBAR_PROJECT_ROW_HEIGHT_MAX).toBe(72);
expect(DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT).toBe(32);
});

it("clamps below-floor values up to the new floor of 32", () => {
expect(clampSidebarProjectRowHeight(0)).toBe(32);
expect(clampSidebarProjectRowHeight(24)).toBe(32); // legacy floor
expect(clampSidebarProjectRowHeight(28)).toBe(32); // legacy default
expect(clampSidebarProjectRowHeight(31)).toBe(32);
});

it("accepts in-range values and rounds fractional input", () => {
expect(clampSidebarProjectRowHeight(32)).toBe(32);
expect(clampSidebarProjectRowHeight(48)).toBe(48);
expect(clampSidebarProjectRowHeight(71.4)).toBe(71);
expect(clampSidebarProjectRowHeight(72)).toBe(72);
});

it("clamps above-ceiling values down to the new max of 72", () => {
expect(clampSidebarProjectRowHeight(73)).toBe(72);
expect(clampSidebarProjectRowHeight(120)).toBe(72);
expect(clampSidebarProjectRowHeight(Number.POSITIVE_INFINITY)).toBe(72);
});
});

describe("getProviderStartOptions", () => {
it("includes the Claude binary path when configured", () => {
expect(
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ const MAX_CUSTOM_MODEL_COUNT = 32;
export const MAX_CUSTOM_MODEL_LENGTH = 256;
const BACKGROUND_IMAGE_KEY = "okcode:background-image";
const BACKGROUND_OPACITY_KEY = "okcode:background-opacity";
export const SIDEBAR_PROJECT_ROW_HEIGHT_MIN = 24;
export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 44;
export const DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT = 28;
export const SIDEBAR_PROJECT_ROW_HEIGHT_MIN = 32;
export const SIDEBAR_PROJECT_ROW_HEIGHT_MAX = 72;
export const DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT = 32;
export const SIDEBAR_THREAD_ROW_HEIGHT_MIN = 24;
export const SIDEBAR_THREAD_ROW_HEIGHT_MAX = 44;
export const DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT = 28;
Expand Down Expand Up @@ -237,7 +237,7 @@ function clampBackgroundOpacity(value: number): number {
return Math.max(0.05, Math.min(1, value));
}

function clampSidebarProjectRowHeight(value: number): number {
export function clampSidebarProjectRowHeight(value: number): number {
return Math.round(
Math.max(SIDEBAR_PROJECT_ROW_HEIGHT_MIN, Math.min(SIDEBAR_PROJECT_ROW_HEIGHT_MAX, value)),
);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/CodeMirrorViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const baseExtensions: Extension[] = [
backgroundColor: "var(--background)",
},
".cm-scroller": {
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace",
fontFamily:
"var(--font-code, var(--font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace))",
overflow: "auto",
},
".cm-gutters": {
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,19 @@ function TerminalViewport({
let disposed = false;

const fitAddon = new FitAddon();
// Resolve `--font-code` at mount time so the terminal picks up the user's
// Code font selection. xterm.js renders via canvas and does not accept
// CSS vars in `fontFamily`, so we have to snapshot the value here.
const resolvedCodeFont =
(typeof window !== "undefined" &&
getComputedStyle(document.documentElement).getPropertyValue("--font-code").trim()) ||
'"JetBrains Mono", "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
const terminal = new Terminal({
cursorBlink: true,
lineHeight: 1.2,
fontSize: getStoredFontSizeOverride() ?? 12,
scrollback: 5_000,
fontFamily: '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
fontFamily: resolvedCodeFont,
theme: terminalThemeFromApp(),
});
terminal.loadAddon(fitAddon);
Expand Down
Loading
Loading