feat: split general settings pages (#1383)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { AppearanceSettings } from "@/components/settings/general-settings";
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { getUserSettingsInitialState } from "../user-settings-state";
|
||||
|
||||
export default async function GeneralAppearancePage() {
|
||||
const initialState = await getUserSettingsInitialState();
|
||||
|
||||
return (
|
||||
<StateProvider initialState={initialState}>
|
||||
<AppearanceSettings />
|
||||
</StateProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function GeneralChangesPanelPage() {
|
||||
redirect("/settings/general/appearance");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function GeneralChatInputPage() {
|
||||
redirect("/settings/general/keyboard-shortcuts");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { KeyboardShortcutsSettings } from "@/components/settings/general-settings";
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { getUserSettingsInitialState } from "../user-settings-state";
|
||||
|
||||
export default async function GeneralKeyboardShortcutsPage() {
|
||||
const initialState = await getUserSettingsInitialState();
|
||||
|
||||
return (
|
||||
<StateProvider initialState={initialState}>
|
||||
<KeyboardShortcutsSettings />
|
||||
</StateProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
import { GeneralSettings } from "@/components/settings/general-settings";
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { fetchUserSettings } from "@/lib/api";
|
||||
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
|
||||
|
||||
export default async function GeneralSettingsPage() {
|
||||
let initialState = {};
|
||||
try {
|
||||
const response = await fetchUserSettings({ cache: "no-store" });
|
||||
const mapped = mapUserSettingsResponse(response);
|
||||
initialState = { userSettings: mapped.loaded ? mapped : undefined };
|
||||
} catch {
|
||||
initialState = {};
|
||||
}
|
||||
|
||||
return (
|
||||
<StateProvider initialState={initialState}>
|
||||
<GeneralSettings />
|
||||
</StateProvider>
|
||||
);
|
||||
export default function GeneralSettingsPage() {
|
||||
return <GeneralSettings />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function GeneralResourceMetricsPage() {
|
||||
redirect("/settings/general/appearance");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function GeneralShellPage() {
|
||||
redirect("/settings/general/terminal");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TerminalSettings } from "@/components/settings/terminal-settings";
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { getUserSettingsInitialState } from "../user-settings-state";
|
||||
|
||||
export default async function GeneralTerminalPage() {
|
||||
const initialState = await getUserSettingsInitialState();
|
||||
|
||||
return (
|
||||
<StateProvider initialState={initialState}>
|
||||
<TerminalSettings />
|
||||
</StateProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { fetchUserSettings } from "@/lib/api";
|
||||
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
|
||||
|
||||
export async function getUserSettingsInitialState() {
|
||||
try {
|
||||
const response = await fetchUserSettings({ cache: "no-store" });
|
||||
const mapped = mapUserSettingsResponse(response);
|
||||
return { userSettings: mapped.loaded ? mapped : undefined };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { IconBell, IconCode, IconSettings } from "@tabler/icons-react";
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
import { GENERAL_NAV_ITEMS } from "@/components/settings/general-nav";
|
||||
import { SettingsGroup, SettingsLeaf } from "./settings-nav-primitives";
|
||||
|
||||
const GENERAL_HREF = "/settings/general";
|
||||
const NOTIFICATIONS_HREF = "/settings/general/notifications";
|
||||
const EDITORS_HREF = "/settings/general/editors";
|
||||
|
||||
type GeneralGroupProps = {
|
||||
pathname: string;
|
||||
@@ -23,20 +22,16 @@ export function GeneralGroup({ pathname, expanded, onToggle }: GeneralGroupProps
|
||||
expanded={expanded}
|
||||
onToggle={onToggle}
|
||||
>
|
||||
<SettingsLeaf
|
||||
href={NOTIFICATIONS_HREF}
|
||||
label="Notifications"
|
||||
icon={IconBell}
|
||||
isActive={pathname === NOTIFICATIONS_HREF}
|
||||
depth={1}
|
||||
/>
|
||||
<SettingsLeaf
|
||||
href={EDITORS_HREF}
|
||||
label="Editors"
|
||||
icon={IconCode}
|
||||
isActive={pathname === EDITORS_HREF}
|
||||
depth={1}
|
||||
/>
|
||||
{GENERAL_NAV_ITEMS.map(({ href, label, icon }) => (
|
||||
<SettingsLeaf
|
||||
key={href}
|
||||
href={href}
|
||||
label={label}
|
||||
icon={icon}
|
||||
isActive={pathname === href}
|
||||
depth={1}
|
||||
/>
|
||||
))}
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ type SettingsLeafProps = {
|
||||
|
||||
const LEAF_DEPTH_PADDING = ["px-2.5", "pl-7 pr-2.5", "pl-10 pr-2.5"] as const;
|
||||
const GROUP_DEPTH_PADDING = ["pl-2.5 pr-1", "pl-7 pr-1", "pl-10 pr-1"] as const;
|
||||
const NAV_FOCUS_CLASS =
|
||||
"outline-none focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:ring-offset-1";
|
||||
|
||||
function clampDepth(depth: number, max: number): number {
|
||||
if (depth < 0) return 0;
|
||||
@@ -44,6 +46,7 @@ export function SettingsLeaf({
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 py-1.5 text-[13px] font-medium rounded-md cursor-pointer",
|
||||
NAV_FOCUS_CLASS,
|
||||
LEAF_DEPTH_PADDING[clampDepth(depth, LEAF_DEPTH_PADDING.length - 1)],
|
||||
isActive ? ACTIVE_CLASS : INACTIVE_CLASS,
|
||||
)}
|
||||
@@ -112,7 +115,10 @@ export function SettingsGroup({
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex flex-1 min-w-0 items-center gap-2 py-1.5 text-[13px] font-medium cursor-pointer"
|
||||
className={cn(
|
||||
"flex flex-1 min-w-0 items-center gap-2 py-1.5 text-[13px] font-medium cursor-pointer",
|
||||
NAV_FOCUS_CLASS,
|
||||
)}
|
||||
>
|
||||
{labelInner}
|
||||
</Link>
|
||||
@@ -120,7 +126,10 @@ export function SettingsGroup({
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
className="flex flex-1 min-w-0 items-center gap-2 py-1.5 text-[13px] font-medium cursor-pointer text-left"
|
||||
className={cn(
|
||||
"flex flex-1 min-w-0 items-center gap-2 py-1.5 text-[13px] font-medium cursor-pointer text-left",
|
||||
NAV_FOCUS_CLASS,
|
||||
)}
|
||||
>
|
||||
{labelInner}
|
||||
</button>
|
||||
@@ -130,7 +139,10 @@ export function SettingsGroup({
|
||||
onClick={toggle}
|
||||
aria-label={expanded ? `Collapse ${label}` : `Expand ${label}`}
|
||||
aria-expanded={expanded}
|
||||
className="shrink-0 flex h-5 w-5 items-center justify-center text-muted-foreground/60 hover:text-foreground/80 cursor-pointer transition-colors"
|
||||
className={cn(
|
||||
"shrink-0 flex h-5 w-5 items-center justify-center text-muted-foreground/60 hover:text-foreground/80 cursor-pointer transition-colors",
|
||||
NAV_FOCUS_CLASS,
|
||||
)}
|
||||
>
|
||||
<IconChevronRight
|
||||
className={cn("h-3 w-3 transition-transform", expanded && "rotate-90")}
|
||||
|
||||
@@ -16,7 +16,10 @@ describe("settingsGroupIdForPath", () => {
|
||||
expect(settingsGroupIdForPath("/settings/workspace/ws-1/repositories")).toBe("workspaces");
|
||||
expect(settingsGroupIdForPath("/settings/system/logs")).toBe("system");
|
||||
expect(settingsGroupIdForPath("/settings/system/feature-toggles")).toBe("system");
|
||||
// Editors/Secrets live under /settings/general so they belong to General.
|
||||
// General subpages stay under /settings/general so they belong to General.
|
||||
expect(settingsGroupIdForPath("/settings/general/appearance")).toBe("general");
|
||||
expect(settingsGroupIdForPath("/settings/general/terminal")).toBe("general");
|
||||
expect(settingsGroupIdForPath("/settings/general/keyboard-shortcuts")).toBe("general");
|
||||
expect(settingsGroupIdForPath("/settings/general/editors")).toBe("general");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { IconBell, IconCommand, IconCode, IconPalette, IconTerminal2 } from "@tabler/icons-react";
|
||||
import type { Icon as TablerIcon } from "@tabler/icons-react";
|
||||
|
||||
export type GeneralNavItem = {
|
||||
href: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: TablerIcon;
|
||||
};
|
||||
|
||||
export const GENERAL_NAV_ITEMS: GeneralNavItem[] = [
|
||||
{
|
||||
href: "/settings/general/appearance",
|
||||
label: "Appearance",
|
||||
description: "Theme, metrics, and changes panel preferences",
|
||||
icon: IconPalette,
|
||||
},
|
||||
{
|
||||
href: "/settings/general/terminal",
|
||||
label: "Terminal",
|
||||
description: "Shell, terminal fonts, and link behavior",
|
||||
icon: IconTerminal2,
|
||||
},
|
||||
{
|
||||
href: "/settings/general/notifications",
|
||||
label: "Notifications",
|
||||
description: "Providers and notification events",
|
||||
icon: IconBell,
|
||||
},
|
||||
{
|
||||
href: "/settings/general/editors",
|
||||
label: "Editors",
|
||||
description: "Editor integrations and defaults",
|
||||
icon: IconCode,
|
||||
},
|
||||
{
|
||||
href: "/settings/general/keyboard-shortcuts",
|
||||
label: "Keyboard Shortcuts",
|
||||
description: "Chat input and command shortcuts",
|
||||
icon: IconCommand,
|
||||
},
|
||||
];
|
||||
@@ -1,40 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
IconActivity,
|
||||
IconCommand,
|
||||
IconPalette,
|
||||
IconServer,
|
||||
IconKeyboard,
|
||||
IconTerminal2,
|
||||
IconGitBranch,
|
||||
IconActivity,
|
||||
} from "@tabler/icons-react";
|
||||
import { Badge } from "@kandev/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
|
||||
import { Label } from "@kandev/ui/label";
|
||||
import { Input } from "@kandev/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@kandev/ui/select";
|
||||
import { Select, SelectContent, SelectItem, 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 { KeyboardShortcutsCard } from "@/components/settings/keyboard-shortcuts-card";
|
||||
import { SystemMetricsSettingsCard } from "@/components/settings/system-metrics-settings-card";
|
||||
import { getBackendConfig } from "@/lib/config";
|
||||
import { GENERAL_NAV_ITEMS } from "@/components/settings/general-nav";
|
||||
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";
|
||||
import type { Theme } from "@/lib/settings/types";
|
||||
|
||||
function ThemeSettingsCard() {
|
||||
@@ -178,304 +163,33 @@ function ChangesPanelLayoutCard() {
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
setUserSettings({
|
||||
...storeApi.getState().userSettings,
|
||||
terminalLinkBehavior: previous,
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="terminal-link-behavior">Open links in</Label>
|
||||
<Select
|
||||
value={userSettings.terminalLinkBehavior}
|
||||
onValueChange={(v) => handleChange(v as "new_tab" | "browser_panel")}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<SelectTrigger id="terminal-link-behavior">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new_tab">New browser tab</SelectItem>
|
||||
<SelectItem value="browser_panel">Built-in browser panel</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Click a URL in the terminal to open it.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const CUSTOM_VALUE = "__custom__";
|
||||
const CATEGORY_LABELS: Record<FontCategory, string> = {
|
||||
icons: "Nerd Fonts",
|
||||
ligatures: "Programming",
|
||||
system: "System",
|
||||
};
|
||||
const CATEGORY_BADGES: Partial<Record<FontCategory, string>> = {
|
||||
icons: "Icons",
|
||||
ligatures: "Ligatures",
|
||||
};
|
||||
const FONT_GROUPS: Record<string, typeof TERMINAL_FONT_PRESETS> = TERMINAL_FONT_PRESETS.reduce(
|
||||
(acc, p) => {
|
||||
(acc[p.category] ??= []).push(p);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof TERMINAL_FONT_PRESETS>,
|
||||
);
|
||||
const FONT_CATEGORIES: FontCategory[] = ["icons", "ligatures", "system"];
|
||||
|
||||
function FontGroupOptions() {
|
||||
return FONT_CATEGORIES.map((category) => (
|
||||
<SelectGroup key={category}>
|
||||
<SelectLabel className="flex items-center gap-2">
|
||||
{CATEGORY_LABELS[category]}
|
||||
{CATEGORY_BADGES[category] && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{CATEGORY_BADGES[category]}
|
||||
</Badge>
|
||||
)}
|
||||
</SelectLabel>
|
||||
{(FONT_GROUPS[category] ?? []).map((preset) => (
|
||||
<SelectItem key={preset.value} value={preset.value}>
|
||||
{preset.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
));
|
||||
}
|
||||
|
||||
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 (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 {
|
||||
setUserSettings({ ...storeApi.getState().userSettings, terminalFontSize: previous });
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFontSizeBlur = () => {
|
||||
const v = Math.min(24, Math.max(8, fontSize));
|
||||
setFontSize(v);
|
||||
saveFontSize(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Font Size</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="terminal-font-size">Font Size</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id="terminal-font-size"
|
||||
type="number"
|
||||
min={8}
|
||||
max={24}
|
||||
value={fontSize}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px (8-24)</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Set the font size for the terminal. Default is 13px.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
setUserSettings({ ...current, terminalFontFamily: value || null });
|
||||
await updateUserSettings({
|
||||
workspace_id: current.workspaceId || "",
|
||||
repository_ids: current.repositoryIds || [],
|
||||
terminal_font_family: value,
|
||||
});
|
||||
} catch {
|
||||
setUserSettings({
|
||||
...storeApi.getState().userSettings,
|
||||
terminalFontFamily: previous,
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
if (value === CUSTOM_VALUE) {
|
||||
setIsCustom(true);
|
||||
return;
|
||||
}
|
||||
setIsCustom(false);
|
||||
setCustomValue("");
|
||||
saveFontFamily(value === "default" ? "" : value);
|
||||
};
|
||||
|
||||
const handleCustomBlur = () => {
|
||||
const trimmed = customValue.trim();
|
||||
if (trimmed) saveFontFamily(trimmed);
|
||||
};
|
||||
|
||||
const selectValue = isCustom ? CUSTOM_VALUE : userSettings.terminalFontFamily || "default";
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Font</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="terminal-font">Font Family</Label>
|
||||
<Select value={selectValue} onValueChange={handleSelectChange} disabled={isSaving}>
|
||||
<SelectTrigger id="terminal-font" data-testid="terminal-font-select">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default (Menlo / Monaco)</SelectItem>
|
||||
<FontGroupOptions />
|
||||
<SelectSeparator />
|
||||
<SelectItem value={CUSTOM_VALUE}>Custom...</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isCustom && (
|
||||
<Input
|
||||
placeholder='e.g. "My Custom Font"'
|
||||
value={customValue}
|
||||
onChange={(e) => setCustomValue(e.target.value)}
|
||||
onBlur={handleCustomBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCustomBlur();
|
||||
}}
|
||||
data-testid="terminal-font-custom-input"
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose a monospace font for the terminal. Nerd Fonts include icons for CLI tools.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function BackendConnectionCard() {
|
||||
const [backendUrl] = useState<string>(() => getBackendConfig().apiBaseUrl);
|
||||
const displayBackendUrl = backendUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Backend Server URL</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="backend-url">Server URL</Label>
|
||||
<Input
|
||||
id="backend-url"
|
||||
type="url"
|
||||
value={displayBackendUrl}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder="http://localhost:38429"
|
||||
className="cursor-default"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Backend URL is provided at runtime for SSR and WebSocket connections.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function GeneralSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">General Settings</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Manage your application preferences and notifications
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{GENERAL_NAV_ITEMS.map(({ href, label, description, icon: Icon }) => (
|
||||
<Link key={href} href={href} className="cursor-pointer">
|
||||
<Card className="h-full transition-colors hover:bg-muted/40">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
{label}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<Separator />
|
||||
|
||||
export function AppearanceSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SettingsSection
|
||||
icon={<IconPalette className="h-5 w-5" />}
|
||||
title="Appearance"
|
||||
@@ -486,42 +200,6 @@ export function GeneralSettings() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<ShellSettingsCard />
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconTerminal2 className="h-5 w-5" />}
|
||||
title="Terminal"
|
||||
description="Configure terminal appearance and behavior"
|
||||
>
|
||||
<TerminalFontCard />
|
||||
<TerminalFontSizeCard />
|
||||
<TerminalLinksCard />
|
||||
</SettingsSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconActivity className="h-5 w-5" />}
|
||||
title="Resource Metrics"
|
||||
description="Configure backend and execution resource sampling"
|
||||
>
|
||||
<SystemMetricsSettingsCard />
|
||||
</SettingsSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconKeyboard className="h-5 w-5" />}
|
||||
title="Chat Input"
|
||||
description="Configure chat input behavior"
|
||||
>
|
||||
<ChatSubmitKeyCard />
|
||||
</SettingsSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconGitBranch className="h-5 w-5" />}
|
||||
title="Changes Panel"
|
||||
@@ -532,6 +210,30 @@ export function GeneralSettings() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconActivity className="h-5 w-5" />}
|
||||
title="Resource Metrics"
|
||||
description="Configure backend and execution resource sampling"
|
||||
>
|
||||
<SystemMetricsSettingsCard />
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SettingsSection
|
||||
icon={<IconKeyboard className="h-5 w-5" />}
|
||||
title="Chat Input"
|
||||
description="Configure chat input behavior"
|
||||
>
|
||||
<ChatSubmitKeyCard />
|
||||
</SettingsSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconCommand className="h-5 w-5" />}
|
||||
title="Keyboard Shortcuts"
|
||||
@@ -539,16 +241,6 @@ export function GeneralSettings() {
|
||||
>
|
||||
<KeyboardShortcutsCard />
|
||||
</SettingsSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconServer className="h-5 w-5" />}
|
||||
title="Backend Connection"
|
||||
description="Configure the backend server URL"
|
||||
>
|
||||
<BackendConnectionCard />
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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. */}
|
||||
<div
|
||||
data-testid="settings-scroll-container"
|
||||
className="flex min-w-0 min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain p-4 pt-0 pb-20"
|
||||
className="flex min-w-0 min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain p-4 pb-20"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<FontCategory, string> = {
|
||||
icons: "Nerd Fonts",
|
||||
ligatures: "Programming",
|
||||
system: "System",
|
||||
};
|
||||
const CATEGORY_BADGES: Partial<Record<FontCategory, string>> = {
|
||||
icons: "Icons",
|
||||
ligatures: "Ligatures",
|
||||
};
|
||||
const FONT_GROUPS: Record<string, typeof TERMINAL_FONT_PRESETS> = TERMINAL_FONT_PRESETS.reduce(
|
||||
(acc, p) => {
|
||||
(acc[p.category] ??= []).push(p);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof TERMINAL_FONT_PRESETS>,
|
||||
);
|
||||
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) => (
|
||||
<SelectGroup key={category}>
|
||||
<SelectLabel className="flex items-center gap-2">
|
||||
{CATEGORY_LABELS[category]}
|
||||
{CATEGORY_BADGES[category] && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
{CATEGORY_BADGES[category]}
|
||||
</Badge>
|
||||
)}
|
||||
</SelectLabel>
|
||||
{(FONT_GROUPS[category] ?? []).map((preset) => (
|
||||
<SelectItem key={preset.value} value={preset.value}>
|
||||
{preset.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
));
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Font Size</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="terminal-font-size">Font Size</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id="terminal-font-size"
|
||||
type="number"
|
||||
min={8}
|
||||
max={24}
|
||||
value={fontSize}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">px (8-24)</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Set the font size for the terminal. Default is 13px.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Font</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="terminal-font">Font Family</Label>
|
||||
<Select value={selectValue} onValueChange={handleSelectChange} disabled={isSaving}>
|
||||
<SelectTrigger id="terminal-font" data-testid="terminal-font-select">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default (Menlo / Monaco)</SelectItem>
|
||||
<FontGroupOptions />
|
||||
<SelectSeparator />
|
||||
<SelectItem value={CUSTOM_VALUE}>Custom...</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isCustom && (
|
||||
<Input
|
||||
placeholder='e.g. "My Custom Font"'
|
||||
value={customValue}
|
||||
onChange={(e) => setCustomValue(e.target.value)}
|
||||
onBlur={handleCustomBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCustomBlur();
|
||||
}}
|
||||
data-testid="terminal-font-custom-input"
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose a monospace font for the terminal. Nerd Fonts include icons for CLI tools.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Terminal Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="terminal-link-behavior">Open links in</Label>
|
||||
<Select
|
||||
value={userSettings.terminalLinkBehavior}
|
||||
onValueChange={(v) => void handleChange(v as "new_tab" | "browser_panel")}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<SelectTrigger id="terminal-link-behavior">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new_tab">New browser tab</SelectItem>
|
||||
<SelectItem value="browser_panel">Built-in browser panel</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Click a URL in the terminal to open it.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function TerminalSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<ShellSettingsCard />
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsSection
|
||||
icon={<IconTerminal2 className="h-5 w-5" />}
|
||||
title="Terminal"
|
||||
description="Configure terminal appearance and behavior"
|
||||
>
|
||||
<TerminalFontCard />
|
||||
<TerminalFontSizeCard />
|
||||
<TerminalLinksCard />
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user