Skip to content
Open
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
61 changes: 41 additions & 20 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
DEFAULT_AUTO_APPLY_DEFAULT_PRESET,
DEFAULT_CONFIRM_ON_QUIT,
DEFAULT_FILE_OPEN_MODE,
DEFAULT_OPTION_AS_META_KEY,
DEFAULT_OPEN_LINKS_IN_APP,
DEFAULT_SHOW_PRESETS_BAR,
DEFAULT_SHOW_RESOURCE_MONITOR,
Expand Down Expand Up @@ -658,30 +659,50 @@ export const createSettingsRouter = () => {
return { success: true };
}),

getOpenLinksInApp: publicProcedure.query(() => {
const row = getSettings();
return row.openLinksInApp ?? DEFAULT_OPEN_LINKS_IN_APP;
}),
getOpenLinksInApp: publicProcedure.query(() => {
const row = getSettings();
return row.openLinksInApp ?? DEFAULT_OPEN_LINKS_IN_APP;
}),

setOpenLinksInApp: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, openLinksInApp: input.enabled })
.onConflictDoUpdate({
target: settings.id,
set: { openLinksInApp: input.enabled },
})
.run();
setOpenLinksInApp: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, openLinksInApp: input.enabled })
.onConflictDoUpdate({
target: settings.id,
set: { openLinksInApp: input.enabled },
})
.run();

return { success: true };
return { success: true };
}),

getOptionAsMetaKey: publicProcedure.query(() => {
const row = getSettings();
return row.optionAsMetaKey ?? DEFAULT_OPTION_AS_META_KEY;
}),

getDefaultEditor: publicProcedure.query(() => {
const row = getSettings();
return row.defaultEditor ?? null;
}),
setOptionAsMetaKey: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, optionAsMetaKey: input.enabled })
.onConflictDoUpdate({
target: settings.id,
set: { optionAsMetaKey: input.enabled },
})
.run();

return { success: true };
}),

getDefaultEditor: publicProcedure.query(() => {
const row = getSettings();
return row.defaultEditor ?? null;
}),

setDefaultEditor: publicProcedure
.input(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
SETTING_ITEM_ID.BEHAVIOR_OPEN_LINKS_IN_APP,
visibleItems,
);
const showOptionAsMetaKey = isItemVisible(
SETTING_ITEM_ID.BEHAVIOR_OPTION_AS_META_KEY,
visibleItems,
);
const isMac =
typeof navigator !== "undefined" &&
navigator.platform.toLowerCase().includes("mac");
Comment on lines +67 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current platform detection usage in settings routes.
# Expected result: `BehaviorSettings.tsx` should align with `getPlatform.useQuery` usage
# and avoid direct `navigator.platform` checks.
rg -nP --type=tsx 'navigator\.platform|getPlatform\.useQuery' apps/desktop/src/renderer/routes/_authenticated/settings -C2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for platform detection patterns without type restriction
echo "=== Platform detection patterns in settings routes ==="
rg -n 'navigator\.platform|getPlatform' apps/desktop/src/renderer/routes/_authenticated/settings --max-count=20

echo ""
echo "=== Checking getPlatform implementation ==="
fd 'trpc' apps/desktop/src --type f | head -5

echo ""
echo "=== Search for getPlatform definition ==="
rg -n 'getPlatform' apps/desktop/src/lib --max-count=20

Repository: superset-sh/superset

Length of output: 1060


🏁 Script executed:

#!/bin/bash
# Get the full context of BehaviorSettings.tsx around the flagged lines
echo "=== BehaviorSettings.tsx around lines 67-69 ==="
sed -n '60,75p' apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx

echo ""
echo "=== BehaviorSettings.tsx around lines 509-513 ==="
sed -n '502,520p' apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx

Repository: superset-sh/superset

Length of output: 1175


🏁 Script executed:

#!/bin/bash
# Check layout.tsx to understand the referenced pattern
echo "=== Layout.tsx platform detection pattern ==="
sed -n '70,85p' apps/desktop/src/renderer/routes/_authenticated/settings/layout.tsx

Repository: superset-sh/superset

Length of output: 620


🏁 Script executed:

#!/bin/bash
# Verify getPlatform implementation - check what it returns for macOS
echo "=== getPlatform implementation ==="
sed -n '40,50p' apps/desktop/src/lib/trpc/routers/window.ts

echo ""
echo "=== Check all platform-related checks in BehaviorSettings.tsx ==="
rg -n 'isMac|platform|navigator' apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx

Repository: superset-sh/superset

Length of output: 496


🏁 Script executed:

#!/bin/bash
# Check the imports and see if electronTrpc is already imported in BehaviorSettings.tsx
echo "=== BehaviorSettings.tsx imports section ==="
head -30 apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx

Repository: superset-sh/superset

Length of output: 1048


Replace navigator.platform detection with canonical tRPC platform source.

BehaviorSettings.tsx uses navigator.platform for platform detection while other settings routes (layout.tsx, GeneralSettings.tsx) use the canonical electronTrpc.window.getPlatform.useQuery() pattern. This violates the coding guideline: "For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc".

♻️ Proposed diff
-	const isMac =
-		typeof navigator !== "undefined" &&
-		navigator.platform.toLowerCase().includes("mac");
+	const { data: platform } = electronTrpc.window.getPlatform.useQuery();
+	const isMac = platform === undefined || platform === "darwin";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isMac =
typeof navigator !== "undefined" &&
navigator.platform.toLowerCase().includes("mac");
const { data: platform } = electronTrpc.window.getPlatform.useQuery();
const isMac = platform === undefined || platform === "darwin";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`
around lines 67 - 69, Replace the direct navigator.platform check in
BehaviorSettings.tsx with the canonical Electron tRPC platform query: remove the
navigator-based isMac calculation and instead call
electronTrpc.window.getPlatform.useQuery() (importing electronTrpc) inside the
component, read the returned platform string (or its loading/default state) and
derive isMac by checking platform.toLowerCase().includes("mac"); ensure you use
the query's data safely (fallback while loading) and update any references to
the old isMac variable to use the new derived value.


const utils = electronTrpc.useUtils();

Expand Down Expand Up @@ -266,6 +273,28 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
},
},
);
const { data: optionAsMetaKey, isLoading: isOptionAsMetaKeyLoading } =
electronTrpc.settings.getOptionAsMetaKey.useQuery();
const setOptionAsMetaKey =
electronTrpc.settings.setOptionAsMetaKey.useMutation({
onMutate: async ({ enabled }) => {
await utils.settings.getOptionAsMetaKey.cancel();
const previous = utils.settings.getOptionAsMetaKey.getData();
utils.settings.getOptionAsMetaKey.setData(undefined, enabled);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous !== undefined) {
utils.settings.getOptionAsMetaKey.setData(
undefined,
context.previous,
);
}
},
onSettled: () => {
utils.settings.getOptionAsMetaKey.invalidate();
},
});

const previewPrefix =
resolveBranchPrefix({
Expand Down Expand Up @@ -432,34 +461,63 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
</div>
)}

{showOpenLinksInApp && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label
htmlFor="open-links-in-app"
className="text-sm font-medium"
>
Open links in app browser
</Label>
<p className="text-xs text-muted-foreground">
Open links from chat and terminal in the built-in browser
instead of your default browser
</p>
{showOpenLinksInApp && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label
htmlFor="open-links-in-app"
className="text-sm font-medium"
>
Open links in app browser
</Label>
<p className="text-xs text-muted-foreground">
Open links from chat and terminal in the built-in browser
instead of your default browser
</p>
</div>
<Switch
id="open-links-in-app"
checked={openLinksInApp ?? false}
onCheckedChange={(enabled) =>
setOpenLinksInApp.mutate({ enabled })
}
disabled={isOpenLinksInAppLoading || setOpenLinksInApp.isPending}
/>
</div>
<Switch
id="open-links-in-app"
checked={openLinksInApp ?? false}
onCheckedChange={(enabled) =>
setOpenLinksInApp.mutate({ enabled })
}
disabled={isOpenLinksInAppLoading || setOpenLinksInApp.isPending}
/>
</div>
)}
)}

{showOptionAsMetaKey && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label
htmlFor="option-as-meta-key"
className="text-sm font-medium"
>
Use Option as Meta key
</Label>
<p className="text-xs text-muted-foreground">
On macOS, send Escape + letter for Option + letter in terminal
instead of inserting special characters
</p>
</div>
<Switch
id="option-as-meta-key"
checked={optionAsMetaKey ?? false}
onCheckedChange={(enabled) =>
setOptionAsMetaKey.mutate({ enabled })
}
disabled={
!isMac ||
isOptionAsMetaKeyLoading ||
setOptionAsMetaKey.isPending
}
/>
</div>
)}

{showWorktreeLocation && (
<div className="space-y-0.5">
<Label className="text-sm font-medium">Worktree location</Label>
{showWorktreeLocation && (
<div className="space-y-0.5">
<Label className="text-sm font-medium">Worktree location</Label>
<p className="text-xs text-muted-foreground">
Base directory for new worktrees
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,10 @@ describe("settings search - font settings", () => {
expect(editorFont?.section).toBe("appearance");
expect(terminalFont?.section).toBe("appearance");
});

it('searching "option meta" returns the Option as Meta key setting', () => {
const results = searchSettings("option meta");
const ids = getIds(results);
expect(ids).toContain(SETTING_ITEM_ID.BEHAVIOR_OPTION_AS_META_KEY);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const SETTING_ITEM_ID = {
BEHAVIOR_RESOURCE_MONITOR: "behavior-resource-monitor",
BEHAVIOR_WORKTREE_LOCATION: "behavior-worktree-location",
BEHAVIOR_OPEN_LINKS_IN_APP: "behavior-open-links-in-app",
BEHAVIOR_OPTION_AS_META_KEY: "behavior-option-as-meta-key",

TERMINAL_PRESETS: "terminal-presets",
TERMINAL_QUICK_ADD: "terminal-quick-add",
Expand Down Expand Up @@ -481,6 +482,26 @@ export const SETTINGS_ITEMS: SettingsItem[] = [
"url",
],
},
{
id: SETTING_ITEM_ID.BEHAVIOR_OPTION_AS_META_KEY,
section: "behavior",
title: "Use Option as Meta key",
description:
"On macOS, send Escape + letter for Option + letter in terminal",
keywords: [
"option",
"option meta",
"alt",
"meta",
"escape",
"terminal",
"macos",
"shortcut",
"bindings",
"claude code",
"special characters",
],
},
{
id: SETTING_ITEM_ID.TERMINAL_PRESETS,
section: "terminal",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useTabsStore } from "renderer/stores/tabs/store";
import { useTerminalTheme } from "renderer/stores/theme";
import { DEFAULT_OPTION_AS_META_KEY } from "shared/constants";
import { SessionKilledOverlay } from "./components";
import {
DEFAULT_TERMINAL_FONT_FAMILY,
Expand Down Expand Up @@ -124,6 +125,8 @@ export const Terminal = ({ paneId, tabId, workspaceId }: TerminalProps) => {
// URL click handler - opens in app browser or system browser based on setting
const { data: openLinksInApp } =
electronTrpc.settings.getOpenLinksInApp.useQuery();
const { data: optionAsMetaKey } =
electronTrpc.settings.getOptionAsMetaKey.useQuery();
const openInBrowserPane = useTabsStore((s) => s.openInBrowserPane);
const handleUrlClickRef = useRef<((url: string) => void) | undefined>(
undefined,
Expand Down Expand Up @@ -340,20 +343,21 @@ export const Terminal = ({ paneId, tabId, workspaceId }: TerminalProps) => {
maybeApplyInitialState,
flushPendingEvents,
resetModes,
isAlternateScreenRef,
isBracketedPasteRef,
setPaneNameRef,
renameUnnamedWorkspaceRef,
handleTerminalFocusRef,
registerClearCallbackRef,
unregisterClearCallbackRef,
registerScrollToBottomCallbackRef,
unregisterScrollToBottomCallbackRef,
registerGetSelectionCallbackRef,
unregisterGetSelectionCallbackRef,
registerPasteCallbackRef,
unregisterPasteCallbackRef,
});
isAlternateScreenRef,
isBracketedPasteRef,
setPaneNameRef,
renameUnnamedWorkspaceRef,
handleTerminalFocusRef,
registerClearCallbackRef,
unregisterClearCallbackRef,
registerScrollToBottomCallbackRef,
unregisterScrollToBottomCallbackRef,
registerGetSelectionCallbackRef,
unregisterGetSelectionCallbackRef,
registerPasteCallbackRef,
unregisterPasteCallbackRef,
useOptionAsMetaKey: optionAsMetaKey ?? DEFAULT_OPTION_AS_META_KEY,
});

useEffect(() => {
const xterm = xtermRef.current;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,82 @@ describe("setupKeyboardHandler", () => {
expect(onWrite).toHaveBeenCalledWith("\x1bf");
});

it("maps Option+letter to Meta+letter on macOS when enabled", () => {
// @ts-expect-error - mocking navigator for tests
globalThis.navigator = { platform: "MacIntel" };

const captured: { handler: ((event: KeyboardEvent) => boolean) | null } = {
handler: null,
};
const xterm = {
attachCustomKeyEventHandler: (
next: (event: KeyboardEvent) => boolean,
) => {
captured.handler = next;
},
};

const onWrite = mock(() => {});
setupKeyboardHandler(xterm as unknown as XTerm, {
onWrite,
isOptionAsMetaKeyEnabled: () => true,
});

captured.handler?.({
type: "keydown",
key: "π",
code: "KeyP",
altKey: true,
metaKey: false,
ctrlKey: false,
shiftKey: false,
} as KeyboardEvent);
captured.handler?.({
type: "keydown",
key: "Í",
code: "KeyF",
altKey: true,
metaKey: false,
ctrlKey: false,
shiftKey: true,
} as KeyboardEvent);

expect(onWrite).toHaveBeenCalledWith("\x1bp");
expect(onWrite).toHaveBeenCalledWith("\x1bF");
});

it("does not map Option+letter on macOS when disabled", () => {
// @ts-expect-error - mocking navigator for tests
globalThis.navigator = { platform: "MacIntel" };

const captured: { handler: ((event: KeyboardEvent) => boolean) | null } = {
handler: null,
};
const xterm = {
attachCustomKeyEventHandler: (
next: (event: KeyboardEvent) => boolean,
) => {
captured.handler = next;
},
};

const onWrite = mock(() => {});
setupKeyboardHandler(xterm as unknown as XTerm, { onWrite });

const result = captured.handler?.({
type: "keydown",
key: "π",
code: "KeyP",
altKey: true,
metaKey: false,
ctrlKey: false,
shiftKey: false,
} as KeyboardEvent);

expect(result).toBeTrue();
expect(onWrite).not.toHaveBeenCalled();
});

it("maps Ctrl+Left/Right to Meta+B/F on Windows", () => {
// @ts-expect-error - mocking navigator for tests
globalThis.navigator = { platform: "Win32" };
Expand Down
Loading