-
-
General Settings
-
- Manage your application preferences and notifications
-
+
+ {GENERAL_NAV_ITEMS.map(({ href, label, description, icon: Icon }) => (
+
+
+
+
+
+ {label}
+
+
+
+ {description}
+
+
+
+ ))}
+
+ );
+}
-
-
+export function AppearanceSettings() {
+ return (
+
}
title="Appearance"
@@ -486,42 +200,6 @@ export function GeneralSettings() {
-
-
-
-
- }
- title="Terminal"
- description="Configure terminal appearance and behavior"
- >
-
-
-
-
-
-
-
- }
- title="Resource Metrics"
- description="Configure backend and execution resource sampling"
- >
-
-
-
-
-
- }
- title="Chat Input"
- description="Configure chat input behavior"
- >
-
-
-
-
-
}
title="Changes Panel"
@@ -532,6 +210,30 @@ export function GeneralSettings() {
+ }
+ title="Resource Metrics"
+ description="Configure backend and execution resource sampling"
+ >
+
+
+
+ );
+}
+
+export function KeyboardShortcutsSettings() {
+ return (
+
+ }
+ title="Chat Input"
+ description="Configure chat input behavior"
+ >
+
+
+
+
+
}
title="Keyboard Shortcuts"
@@ -539,16 +241,6 @@ export function GeneralSettings() {
>
-
-
-
- }
- title="Backend Connection"
- description="Configure the backend server URL"
- >
-
-
);
}
diff --git a/apps/web/components/settings/settings-layout-client.tsx b/apps/web/components/settings/settings-layout-client.tsx
index 4e8ef689ec..ce3048a3b4 100644
--- a/apps/web/components/settings/settings-layout-client.tsx
+++ b/apps/web/components/settings/settings-layout-client.tsx
@@ -112,13 +112,13 @@ function SettingsShell({
backHref={backHref}
backLabel={backLabel}
parents={parents}
- className="h-16 border-b-0"
+ className="h-10"
/>
{/* Scroll the content, not the topbar: min-h-0 lets this flex child
shrink below its content height so overflow-y-auto can take effect. */}
{children}
diff --git a/apps/web/components/settings/terminal-settings.test.ts b/apps/web/components/settings/terminal-settings.test.ts
new file mode 100644
index 0000000000..7c178135f2
--- /dev/null
+++ b/apps/web/components/settings/terminal-settings.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from "vitest";
+import { normalizeTerminalFontSize } from "./terminal-settings";
+
+describe("normalizeTerminalFontSize", () => {
+ it("clamps finite font sizes to the supported range", () => {
+ expect(normalizeTerminalFontSize(6, 13)).toBe(8);
+ expect(normalizeTerminalFontSize(18, 13)).toBe(18);
+ expect(normalizeTerminalFontSize(30, 13)).toBe(24);
+ });
+
+ it("uses the fallback when the input is not finite", () => {
+ expect(normalizeTerminalFontSize(Number.NaN, 15)).toBe(15);
+ expect(normalizeTerminalFontSize(Number.POSITIVE_INFINITY, 15)).toBe(15);
+ });
+});
diff --git a/apps/web/components/settings/terminal-settings.tsx b/apps/web/components/settings/terminal-settings.tsx
new file mode 100644
index 0000000000..0ce6035d23
--- /dev/null
+++ b/apps/web/components/settings/terminal-settings.tsx
@@ -0,0 +1,311 @@
+"use client";
+
+import { useState } from "react";
+import { IconTerminal2 } from "@tabler/icons-react";
+import { Badge } from "@kandev/ui/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
+import { Input } from "@kandev/ui/input";
+import { Label } from "@kandev/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from "@kandev/ui/select";
+import { Separator } from "@kandev/ui/separator";
+import { SettingsSection } from "@/components/settings/settings-section";
+import { ShellSettingsCard } from "@/components/settings/shell-settings-card";
+import { useAppStore, useAppStoreApi } from "@/components/state-provider";
+import { updateUserSettings } from "@/lib/api";
+import { TERMINAL_FONT_PRESETS } from "@/lib/terminal/terminal-font";
+import type { FontCategory } from "@/lib/terminal/terminal-font";
+
+const CUSTOM_VALUE = "__custom__";
+const CATEGORY_LABELS: Record
= {
+ icons: "Nerd Fonts",
+ ligatures: "Programming",
+ system: "System",
+};
+const CATEGORY_BADGES: Partial> = {
+ icons: "Icons",
+ ligatures: "Ligatures",
+};
+const FONT_GROUPS: Record = TERMINAL_FONT_PRESETS.reduce(
+ (acc, p) => {
+ (acc[p.category] ??= []).push(p);
+ return acc;
+ },
+ {} as Record,
+);
+const FONT_CATEGORIES: FontCategory[] = ["icons", "ligatures", "system"];
+
+export function normalizeTerminalFontSize(value: number, fallback: number): number {
+ const base = Number.isFinite(value) ? value : fallback;
+ return Math.min(24, Math.max(8, base));
+}
+
+function FontGroupOptions() {
+ return FONT_CATEGORIES.map((category) => (
+
+
+ {CATEGORY_LABELS[category]}
+ {CATEGORY_BADGES[category] && (
+
+ {CATEGORY_BADGES[category]}
+
+ )}
+
+ {(FONT_GROUPS[category] ?? []).map((preset) => (
+
+ {preset.label}
+
+ ))}
+
+ ));
+}
+
+function TerminalFontSizeCard() {
+ const storeApi = useAppStoreApi();
+ const userSettings = useAppStore((state) => state.userSettings);
+ const setUserSettings = useAppStore((state) => state.setUserSettings);
+ const [isSaving, setIsSaving] = useState(false);
+ const [fontSize, setFontSize] = useState(() => userSettings.terminalFontSize ?? 13);
+
+ const saveFontSize = async (value: number) => {
+ if (isSaving) return;
+ if (!Number.isFinite(value)) return;
+ if (value < 8 || value > 24) return;
+ setIsSaving(true);
+ const current = storeApi.getState().userSettings;
+ const previous = current.terminalFontSize;
+ try {
+ setUserSettings({ ...current, terminalFontSize: value });
+ await updateUserSettings({
+ workspace_id: current.workspaceId || "",
+ repository_ids: current.repositoryIds || [],
+ terminal_font_size: value,
+ });
+ } catch {
+ const latest = storeApi.getState().userSettings;
+ if (latest.workspaceId === current.workspaceId && latest.terminalFontSize === value) {
+ setUserSettings({ ...latest, terminalFontSize: previous });
+ }
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleFontSizeBlur = () => {
+ const value = normalizeTerminalFontSize(fontSize, userSettings.terminalFontSize ?? 13);
+ setFontSize(value);
+ void saveFontSize(value);
+ };
+
+ return (
+
+
+ Terminal Font Size
+
+
+
+
+
+ setFontSize(Number(e.target.value))}
+ onBlur={handleFontSizeBlur}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleFontSizeBlur();
+ }}
+ className="w-20"
+ disabled={isSaving}
+ data-testid="terminal-font-size-input"
+ />
+ px (8-24)
+
+
+ Set the font size for the terminal. Default is 13px.
+
+
+
+
+ );
+}
+
+function TerminalFontCard() {
+ const storeApi = useAppStoreApi();
+ const userSettings = useAppStore((state) => state.userSettings);
+ const setUserSettings = useAppStore((state) => state.setUserSettings);
+ const [isSaving, setIsSaving] = useState(false);
+ const [isCustom, setIsCustom] = useState(() => {
+ const current = userSettings.terminalFontFamily;
+ if (!current) return false;
+ return !TERMINAL_FONT_PRESETS.some((p) => p.value === current);
+ });
+ const [customValue, setCustomValue] = useState(
+ () => (isCustom ? userSettings.terminalFontFamily : "") ?? "",
+ );
+
+ const saveFontFamily = async (value: string) => {
+ if (isSaving) return;
+ setIsSaving(true);
+ const current = storeApi.getState().userSettings;
+ const previous = current.terminalFontFamily;
+ const nextValue = value || null;
+ try {
+ setUserSettings({ ...current, terminalFontFamily: nextValue });
+ await updateUserSettings({
+ workspace_id: current.workspaceId || "",
+ repository_ids: current.repositoryIds || [],
+ terminal_font_family: value,
+ });
+ } catch {
+ const latest = storeApi.getState().userSettings;
+ if (latest.workspaceId === current.workspaceId && latest.terminalFontFamily === nextValue) {
+ setUserSettings({ ...latest, terminalFontFamily: previous });
+ }
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleSelectChange = (value: string) => {
+ if (value === CUSTOM_VALUE) {
+ setIsCustom(true);
+ return;
+ }
+ setIsCustom(false);
+ setCustomValue("");
+ void saveFontFamily(value === "default" ? "" : value);
+ };
+
+ const handleCustomBlur = () => {
+ const trimmed = customValue.trim();
+ if (trimmed) void saveFontFamily(trimmed);
+ };
+
+ const selectValue = isCustom ? CUSTOM_VALUE : userSettings.terminalFontFamily || "default";
+
+ return (
+
+
+ Terminal Font
+
+
+
+
+
+ {isCustom && (
+
setCustomValue(e.target.value)}
+ onBlur={handleCustomBlur}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleCustomBlur();
+ }}
+ data-testid="terminal-font-custom-input"
+ />
+ )}
+
+ Choose a monospace font for the terminal. Nerd Fonts include icons for CLI tools.
+
+
+
+
+ );
+}
+
+function TerminalLinksCard() {
+ const userSettings = useAppStore((state) => state.userSettings);
+ const setUserSettings = useAppStore((state) => state.setUserSettings);
+ const storeApi = useAppStoreApi();
+ const [isSaving, setIsSaving] = useState(false);
+
+ const handleChange = async (value: "new_tab" | "browser_panel") => {
+ if (isSaving) return;
+ setIsSaving(true);
+ const current = storeApi.getState().userSettings;
+ const previous = current.terminalLinkBehavior;
+ try {
+ setUserSettings({ ...current, terminalLinkBehavior: value });
+ await updateUserSettings({
+ workspace_id: current.workspaceId || "",
+ repository_ids: current.repositoryIds || [],
+ terminal_link_behavior: value,
+ });
+ } catch {
+ const latest = storeApi.getState().userSettings;
+ if (latest.workspaceId === current.workspaceId && latest.terminalLinkBehavior === value) {
+ setUserSettings({ ...latest, terminalLinkBehavior: previous });
+ }
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+ Terminal Links
+
+
+
+
+
+
Click a URL in the terminal to open it.
+
+
+
+ );
+}
+
+export function TerminalSettings() {
+ return (
+
+
+
+
+
+ }
+ title="Terminal"
+ description="Configure terminal appearance and behavior"
+ >
+
+
+
+
+
+ );
+}
diff --git a/apps/web/e2e/tests/settings/keyboard-shortcuts.spec.ts b/apps/web/e2e/tests/settings/keyboard-shortcuts.spec.ts
index d32203a41b..47ba7c9450 100644
--- a/apps/web/e2e/tests/settings/keyboard-shortcuts.spec.ts
+++ b/apps/web/e2e/tests/settings/keyboard-shortcuts.spec.ts
@@ -1,13 +1,17 @@
import { test, expect } from "../../fixtures/test-base";
+const KEYBOARD_SETTINGS_PATH = "/settings/general/keyboard-shortcuts";
+
test.describe("Keyboard Shortcuts Settings", () => {
test.describe.configure({ retries: 1 });
test("settings page shows all configurable shortcuts", async ({ testPage }) => {
- await testPage.goto("/settings/general");
+ await testPage.goto(KEYBOARD_SETTINGS_PATH);
+
+ await expect(testPage.locator("#chat-submit-key")).toBeVisible({ timeout: 10_000 });
// Original 3 shortcuts
- await expect(testPage.getByTestId("shortcut-recorder-SEARCH")).toBeVisible({ timeout: 10_000 });
+ await expect(testPage.getByTestId("shortcut-recorder-SEARCH")).toBeVisible();
await expect(testPage.getByTestId("shortcut-recorder-FILE_SEARCH")).toBeVisible();
await expect(testPage.getByTestId("shortcut-recorder-QUICK_CHAT")).toBeVisible();
@@ -30,7 +34,7 @@ test.describe("Keyboard Shortcuts Settings", () => {
keyboard_shortcuts: {},
});
- await testPage.goto("/settings/general");
+ await testPage.goto(KEYBOARD_SETTINGS_PATH);
// Find the BOTTOM_TERMINAL recorder and verify it shows the default (Cmd+J or Ctrl+J)
const recorder = testPage.getByTestId("shortcut-recorder-BOTTOM_TERMINAL");
@@ -49,7 +53,7 @@ test.describe("Keyboard Shortcuts Settings", () => {
await expect(recorder).toContainText("T", { timeout: 3_000 });
// Reload the page and verify the shortcut persisted
- await testPage.goto("/settings/general");
+ await testPage.goto(KEYBOARD_SETTINGS_PATH);
const recorderAfterReload = testPage.getByTestId("shortcut-recorder-BOTTOM_TERMINAL");
await expect(recorderAfterReload).toBeVisible({ timeout: 10_000 });
await expect(recorderAfterReload).toContainText("T");
@@ -68,7 +72,7 @@ test.describe("Keyboard Shortcuts Settings", () => {
},
});
- await testPage.goto("/settings/general");
+ await testPage.goto(KEYBOARD_SETTINGS_PATH);
const recorder = testPage.getByTestId("shortcut-recorder-TOGGLE_SIDEBAR");
await expect(recorder).toBeVisible({ timeout: 10_000 });
diff --git a/apps/web/e2e/tests/settings/mobile-general-settings.spec.ts b/apps/web/e2e/tests/settings/mobile-general-settings.spec.ts
new file mode 100644
index 0000000000..a4a9597f13
--- /dev/null
+++ b/apps/web/e2e/tests/settings/mobile-general-settings.spec.ts
@@ -0,0 +1,18 @@
+import { test, expect } from "../../fixtures/test-base";
+
+test.describe("Mobile general settings", () => {
+ test("opens a dedicated General settings page from the overview", async ({ testPage }) => {
+ await testPage.goto("/settings/general");
+
+ await expect(testPage.getByRole("link", { name: /Terminal/ })).toBeVisible({
+ timeout: 15_000,
+ });
+
+ await testPage.getByRole("link", { name: /Terminal/ }).click();
+
+ await expect(testPage).toHaveURL(/\/settings\/general\/terminal$/);
+ await expect(testPage.getByRole("heading", { name: "Terminal", exact: true })).toBeVisible();
+ await expect(testPage.getByTestId("terminal-font-select")).toBeVisible();
+ await expect(testPage.getByTestId("terminal-font-size-input")).toBeVisible();
+ });
+});
diff --git a/apps/web/e2e/tests/terminal/terminal-settings.spec.ts b/apps/web/e2e/tests/terminal/terminal-settings.spec.ts
index f8b1a99ba4..43f035be22 100644
--- a/apps/web/e2e/tests/terminal/terminal-settings.spec.ts
+++ b/apps/web/e2e/tests/terminal/terminal-settings.spec.ts
@@ -4,6 +4,8 @@ import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { SessionPage } from "../../pages/session-page";
+const TERMINAL_SETTINGS_PATH = "/settings/general/terminal";
+
// The full font stack value for "JetBrains Mono" preset
const JB_MONO_STACK = '"JetBrains Mono", "Fira Code", Menlo, Consolas, monospace';
@@ -116,11 +118,13 @@ test.describe("Terminal font settings", () => {
});
test("settings page shows font and size controls", async ({ testPage }) => {
- await testPage.goto("/settings/general");
+ await testPage.goto(TERMINAL_SETTINGS_PATH);
+
+ await expect(testPage.getByText("Preferred Shell")).toBeVisible({ timeout: 10_000 });
// Font family selector
const fontSelect = testPage.getByTestId("terminal-font-select");
- await expect(fontSelect).toBeVisible({ timeout: 10_000 });
+ await expect(fontSelect).toBeVisible();
await fontSelect.click();
// Verify a preset is listed (exact match to avoid matching Nerd Font variant)
@@ -172,7 +176,7 @@ test.describe("Terminal link settings", () => {
});
test("settings page shows terminal links card and allows toggling", async ({ testPage }) => {
- await testPage.goto("/settings");
+ await testPage.goto(TERMINAL_SETTINGS_PATH);
// Terminal Links section visible
await expect(testPage.locator("text=Terminal Links").first()).toBeVisible({ timeout: 10_000 });