1<script lang="ts">
2 import { onMount } from "svelte";
3 import { ModeWatcher } from "mode-watcher";
4 import ConfigShell from "./config/ConfigShell.svelte";
5 import ShortcutsTable from "./config/ShortcutsTable.svelte";
6 import * as AlertDialog from "$lib/components/ui/alert-dialog";
7 import { Button } from "$lib/components/ui/button";
8 import { ColorPicker } from "$lib/components/ui/color-picker";
9 import { Input } from "$lib/components/ui/input";
10 import { Label } from "$lib/components/ui/label";
11 import { Spinner } from "$lib/components/ui/spinner";
12 import * as Card from "$lib/components/ui/card";
13 import * as Tabs from "$lib/components/ui/tabs";
14 import { FlaskConical, RefreshCw } from "@lucide/svelte";
15
16 type ColorPalette = {
17 background: string;
18 surface: string;
19 surface_variant: string;
20 text: string;
21 muted: string;
22 accent: string;
23 border: string;
24 };
25
26 type SearchShortcut = {
27 url: string;
28 description: string;
29 };
30
31 type ResolvedPerformance = {
32 skia_cpu_threads: number;
33 skia_gpu_threads: number;
34 web_process_memory_mb: number;
35 network_process_memory_mb: number;
36 webview_pool_prewarm: number;
37 conservative_threshold: number;
38 strict_threshold: number;
39 };
40
41 type HardwareInfo = {
42 cpu_cores: number;
43 cpu_threads: number;
44 total_ram_mb: number;
45 gpu_vendor: string;
46 gpu_name: string;
47 vram_mb: number;
48 };
49
50 type CustomPerformance = {
51 skia_cpu_threads: number;
52 skia_gpu_threads: number;
53 web_process_memory_mb: number;
54 network_process_memory_mb: number;
55 webview_pool_prewarm: number;
56 };
57
58 type PerformanceConfig = {
59 profile: string;
60 custom: CustomPerformance;
61 resolved: ResolvedPerformance;
62 hardware: HardwareInfo;
63 };
64
65 type ConfigDTO = {
66 appearance: {
67 sans_font: string;
68 serif_font: string;
69 monospace_font: string;
70 default_font_size: number;
71 color_scheme: string;
72 light_palette: ColorPalette;
73 dark_palette: ColorPalette;
74 };
75 performance: PerformanceConfig;
76 default_ui_scale: number;
77 default_search_engine: string;
78 search_shortcuts: Record<string, SearchShortcut>;
79 };
80
81 const PERFORMANCE_PROFILES = [
82 { value: "default", label: "Default", description: "No tuning, uses WebKit defaults. Recommended for most users." },
83 { value: "lite", label: "Lite", description: "Reduced resource usage for low-RAM systems (< 4GB) or battery saving." },
84 { value: "max", label: "Max", description: "Maximum responsiveness, scales based on detected hardware." },
85 { value: "custom", label: "Custom", description: "Manual control via config file. Edit config.toml to set individual values." },
86 ];
87
88 const PALETTE_FIELDS: Array<{ key: keyof ColorPalette; label: string }> = [
89 { key: "background", label: "Background" },
90 { key: "surface", label: "Surface" },
91 { key: "surface_variant", label: "Surface Variant" },
92 { key: "text", label: "Text" },
93 { key: "muted", label: "Muted" },
94 { key: "accent", label: "Accent" },
95 { key: "border", label: "Border" },
96 ];
97
98 function isHexColor(value: string): boolean {
99 return /^#[0-9a-fA-F]{6}$/.test(value);
100 }
101
102 // UI state
103 let loading = $state(true);
104 let loadError = $state<string | null>(null);
105 let config = $state<ConfigDTO | null>(null);
106 let saving = $state(false);
107 let saveSuccess = $state(false);
108 let saveError = $state<string | null>(null);
109 let showRestartWarning = $state(false);
110 let themeEvents = $state(0);
111 let activeTab = $state("appearance");
112
113 // Load config from API
114 async function loadConfig() {
115 loading = true;
116 loadError = null;
117 try {
118 const response = await fetch("/api/config");
119 if (!response.ok) throw new Error("Failed to fetch config");
120 config = (await response.json()) as ConfigDTO;
121 } catch (e: any) {
122 loadError = e.message;
123 console.error("[config] load failed", e);
124 } finally {
125 loading = false;
126 }
127 }
128
129 // Reload config (used after save to get normalized values)
130 function reloadConfig() {
131 loadConfig();
132 }
133
134 let resetDialogOpen = $state(false);
135
136 async function doResetToDefaults() {
137 saveSuccess = false;
138 try {
139 const response = await fetch("/api/config/default");
140 if (!response.ok) throw new Error("Failed to fetch default config");
141 config = (await response.json()) as ConfigDTO;
142 } catch (e: any) {
143 console.error("[config] reset defaults failed", e);
144 }
145 resetDialogOpen = false;
146 }
147
148 function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
149 const bridge = (window as any).webkit?.messageHandlers?.dumber;
150 if (bridge && typeof bridge.postMessage === "function") {
151 return bridge;
152 }
153 return null;
154 }
155
156 function getWebViewId(): number {
157 return (window as any).__dumber_webview_id || 0;
158 }
159
160 async function saveConfig() {
161 saving = true;
162 saveSuccess = false;
163 saveError = null;
164
165 try {
166 const bridge = getWebKitBridge();
167 const webviewId = getWebViewId();
168
169 console.debug("[config] save click", {
170 hasBridge: Boolean(bridge),
171 webviewId,
172 hasConfig: Boolean(config),
173 });
174
175 if (!bridge) {
176 throw new Error("WebKit bridge not available (not running inside Dumber)");
177 }
178 if (!config) {
179 throw new Error("Config not loaded");
180 }
181
182 let timeout: number | null = window.setTimeout(() => {
183 if (saving) {
184 console.warn("[config] save timed out (no callback)");
185 saveError = "Save timed out (no response from native handler)";
186 saving = false;
187 }
188 }, 8000);
189
190 const clearSaveTimeout = () => {
191 if (timeout != null) {
192 window.clearTimeout(timeout);
193 timeout = null;
194 }
195 };
196
197 (window as any).__dumber_config_saved = (resp?: unknown) => {
198 clearSaveTimeout();
199 console.debug("[config] save success", resp);
200 saveSuccess = true;
201 saving = false;
202
203 // Show restart warning if performance tab was active
204 if (activeTab === "performance") {
205 showRestartWarning = true;
206 }
207
208 // Refresh config from backend (in case watcher normalized values)
209 reloadConfig();
210
211 setTimeout(() => {
212 saveSuccess = false;
213 }, 3000);
214 };
215 (window as any).__dumber_config_error = (msg: unknown) => {
216 clearSaveTimeout();
217 console.error("[config] save error", msg);
218 saveError = typeof msg === "string" ? msg : "Failed to save config";
219 saving = false;
220 };
221
222 const payload = $state.snapshot(config);
223
224 try {
225 bridge.postMessage({
226 type: "save_config",
227 webview_id: webviewId,
228 payload,
229 });
230 console.debug("[config] postMessage sent", { payloadBytes: JSON.stringify(payload).length });
231 } catch (postErr) {
232 console.error("[config] postMessage threw", postErr);
233 throw postErr;
234 }
235 } catch (e: any) {
236 console.error("[config] save exception", e);
237 saveError = e.message;
238 saving = false;
239 }
240 }
241
242 function updateShortcuts(nextShortcuts: Record<string, SearchShortcut>) {
243 if (!config) return;
244 config.search_shortcuts = nextShortcuts;
245 }
246
247 onMount(() => {
248 console.debug("[config] mount", {
249 webviewId: (window as any).__dumber_webview_id,
250 hasBridge: Boolean((window as any).webkit?.messageHandlers?.dumber),
251 });
252
253 const onThemeChanged = (e: Event) => {
254 themeEvents += 1;
255 console.debug("[config] theme changed", (e as CustomEvent).detail);
256 };
257 window.addEventListener("dumber:theme-changed", onThemeChanged);
258
259 // Load config on mount
260 loadConfig();
261
262 return () => {
263 window.removeEventListener("dumber:theme-changed", onThemeChanged);
264 };
265 });
266</script>
267
268<ModeWatcher />
269<ConfigShell>
270 <div class="flex w-full flex-col gap-6 px-6 py-8">
271 {#if loading}
272 <!-- Loading state with spinner -->
273 <div class="flex flex-col items-center justify-center gap-4 py-16">
274 <Spinner class="size-8" />
275 <div class="text-xs font-semibold uppercase tracking-[0.4em] text-muted-foreground">
276 Loading configuration…
277 </div>
278 </div>
279 {:else if loadError}
280 <!-- Error state -->
281 <div class="flex flex-col items-center justify-center gap-4 py-16">
282 <Card.Root class="max-w-md">
283 <Card.Header>
284 <Card.Title class="text-destructive">Failed to load config</Card.Title>
285 <Card.Description>{loadError}</Card.Description>
286 </Card.Header>
287 <Card.Content>
288 <Button variant="outline" onclick={reloadConfig}>Try again</Button>
289 </Card.Content>
290 </Card.Root>
291 </div>
292 {:else if config}
293 <Tabs.Root bind:value={activeTab} class="w-full">
294 <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
295 <div class="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground">
296 Config Workspace
297 </div>
298 <Tabs.List class="bg-muted/60">
299 <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
300 <Tabs.Trigger value="search">Search</Tabs.Trigger>
301 <Tabs.Trigger value="performance">Performance</Tabs.Trigger>
302 </Tabs.List>
303 </div>
304
305 <Tabs.Content value="appearance">
306 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">
307 <Card.Header>
308 <Card.Title>Appearance</Card.Title>
309 <Card.Description>Fonts, palette, and theme preferences.</Card.Description>
310 </Card.Header>
311 <Card.Content class="space-y-8">
312 <div class="grid gap-6 md:grid-cols-2">
313 <div class="space-y-2">
314 <Label for="sans_font">Sans-Serif Font</Label>
315 <Input id="sans_font" bind:value={config.appearance.sans_font} />
316 </div>
317 <div class="space-y-2">
318 <Label for="serif_font">Serif Font</Label>
319 <Input id="serif_font" bind:value={config.appearance.serif_font} />
320 </div>
321 <div class="space-y-2">
322 <Label for="monospace_font">Monospace Font</Label>
323 <Input id="monospace_font" bind:value={config.appearance.monospace_font} />
324 </div>
325 <div class="space-y-2">
326 <Label for="font_size">Default Font Size</Label>
327 <Input id="font_size" type="number" bind:value={config.appearance.default_font_size} />
328 </div>
329 <div class="space-y-2">
330 <Label for="color_scheme">Color Scheme</Label>
331 <select
332 id="color_scheme"
333 bind:value={config.appearance.color_scheme}
334 class="flex h-10 w-full border border-border bg-background px-3 py-2 text-sm text-foreground ring-offset-background transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
335 >
336 <option value="default">Follow System</option>
337 <option value="prefer-dark">Always Dark</option>
338 <option value="prefer-light">Always Light</option>
339 </select>
340 </div>
341 <div class="space-y-2">
342 <Label for="ui_scale">UI Scale</Label>
343 <Input id="ui_scale" type="number" step="0.1" bind:value={config.default_ui_scale} />
344 </div>
345 </div>
346
347 <div class="space-y-6">
348 <div class="space-y-3">
349 <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
350 Theme: Light
351 </div>
352 <div class="grid gap-4 md:grid-cols-2">
353 {#each PALETTE_FIELDS as field (field.key)}
354 <div class="space-y-2">
355 <Label for={`light_${field.key}`}>{field.label}</Label>
356 <div class="flex items-center gap-3">
357 <ColorPicker
358 id={`light_${field.key}`}
359 value={config.appearance.light_palette[field.key]}
360 onValueChange={(v) => {
361 config!.appearance.light_palette[field.key] = v;
362 }}
363 />
364 <Input bind:value={config.appearance.light_palette[field.key]} />
365 </div>
366 {#if !isHexColor(config.appearance.light_palette[field.key])}
367 <p class="text-xs text-muted-foreground">Expected hex color like #RRGGBB</p>
368 {/if}
369 </div>
370 {/each}
371 </div>
372 </div>
373
374 <div class="space-y-3">
375 <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
376 Theme: Dark
377 </div>
378 <div class="grid gap-4 md:grid-cols-2">
379 {#each PALETTE_FIELDS as field (field.key)}
380 <div class="space-y-2">
381 <Label for={`dark_${field.key}`}>{field.label}</Label>
382 <div class="flex items-center gap-3">
383 <ColorPicker
384 id={`dark_${field.key}`}
385 value={config.appearance.dark_palette[field.key]}
386 onValueChange={(v) => {
387 config!.appearance.dark_palette[field.key] = v;
388 }}
389 />
390 <Input bind:value={config.appearance.dark_palette[field.key]} />
391 </div>
392 {#if !isHexColor(config.appearance.dark_palette[field.key])}
393 <p class="text-xs text-muted-foreground">Expected hex color like #RRGGBB</p>
394 {/if}
395 </div>
396 {/each}
397 </div>
398 </div>
399 </div>
400 </Card.Content>
401 </Card.Root>
402 </Tabs.Content>
403
404 <Tabs.Content value="search">
405 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">
406 <Card.Header>
407 <Card.Title>Search</Card.Title>
408 <Card.Description>Default engine and shortcuts.</Card.Description>
409 </Card.Header>
410 <Card.Content class="space-y-8">
411 <div class="space-y-2">
412 <Label for="search_engine">Default Search Engine (use %s for query)</Label>
413 <Input id="search_engine" bind:value={config.default_search_engine} />
414 </div>
415
416 <div class="space-y-3">
417 <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
418 Search Shortcuts
419 </div>
420 <p class="text-sm text-muted-foreground">Map prefixes like g to URL templates.</p>
421 <ShortcutsTable shortcuts={config.search_shortcuts} onUpdate={updateShortcuts} />
422 </div>
423 </Card.Content>
424 </Card.Root>
425 </Tabs.Content>
426
427 <Tabs.Content value="performance">
428 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">
429 <Card.Header>
430 <Card.Title>Performance Profile</Card.Title>
431 <Card.Description>
432 WebKitGTK rendering and memory tuning presets.
433 </Card.Description>
434 </Card.Header>
435 <Card.Content class="space-y-6">
436 <!-- Detected Hardware Card -->
437 {#if config.performance?.hardware}
438 {@const hw = config.performance.hardware}
439 <div class="rounded-md border border-border bg-muted/30 px-4 py-3">
440 <div class="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-muted-foreground">
441 Detected Hardware
442 </div>
443 <div class="flex flex-wrap gap-x-6 gap-y-1 text-sm">
444 <div><span class="text-muted-foreground">CPU:</span> <span class="font-mono">{hw.cpu_cores}c/{hw.cpu_threads}t</span></div>
445 <div><span class="text-muted-foreground">RAM:</span> <span class="font-mono">{hw.total_ram_mb > 0 ? (hw.total_ram_mb / 1024).toFixed(0) + ' GB' : '?'}</span></div>
446 <div><span class="text-muted-foreground">GPU:</span> <span class="font-mono capitalize">{hw.gpu_vendor || 'Unknown'}</span></div>
447 <div><span class="text-muted-foreground">VRAM:</span> <span class="font-mono">{hw.vram_mb > 0 ? (hw.vram_mb / 1024).toFixed(0) + ' GB' : (hw.gpu_vendor === 'intel' ? 'Shared' : '?')}</span></div>
448 </div>
449 </div>
450 {/if}
451
452 <!-- Experimental feature warning -->
453 <div class="flex items-start gap-3 rounded-md border border-info/30 bg-info/10 px-4 py-3">
454 <FlaskConical class="mt-0.5 size-4 shrink-0 text-info" />
455 <div class="space-y-1">
456 <p class="text-sm font-medium text-info">Experimental Feature</p>
457 <p class="text-xs text-muted-foreground">
458 Performance profiles are experimental. Results may vary depending on your hardware and WebKitGTK version.
459 </p>
460 </div>
461 </div>
462
463 <!-- Profile selector -->
464 {#if config.performance}
465 <div class="space-y-4">
466 {#each PERFORMANCE_PROFILES as profile (profile.value)}
467 <label
468 class="flex cursor-pointer items-start gap-4 rounded-md border p-4 transition-colors hover:bg-muted/50 {config.performance.profile === profile.value ? 'border-primary bg-primary/5' : ''}"
469 >
470 <input
471 type="radio"
472 name="performance_profile"
473 value={profile.value}
474 checked={config.performance.profile === profile.value}
475 onchange={() => config!.performance.profile = profile.value}
476 class="mt-1 accent-primary"
477 />
478 <div class="space-y-1">
479 <div class="font-medium">{profile.label}</div>
480 <div class="text-sm text-muted-foreground">{profile.description}</div>
481 </div>
482 </label>
483 {/each}
484 </div>
485 {:else}
486 <div class="text-sm text-muted-foreground">
487 Performance settings not available. This may be due to a configuration error.
488 </div>
489 {/if}
490
491 <!-- Custom profile fields -->
492 {#if config.performance?.profile === "custom"}
493 <div class="space-y-4 rounded-md border border-border bg-muted/20 p-4">
494 <div class="text-sm font-semibold uppercase tracking-[0.15em] text-muted-foreground">
495 Custom Settings
496 </div>
497 <div class="grid gap-4 md:grid-cols-2">
498 <div class="space-y-2">
499 <Label for="skia_cpu_threads">Skia CPU Threads</Label>
500 <Input
501 id="skia_cpu_threads"
502 type="number"
503 min="0"
504 max="8"
505 bind:value={config.performance.custom.skia_cpu_threads}
506 />
507 <p class="text-xs text-muted-foreground">0 = unset (WebKit default). Max 8.</p>
508 </div>
509 <div class="space-y-2">
510 <Label for="skia_gpu_threads">Skia GPU Threads</Label>
511 <Input
512 id="skia_gpu_threads"
513 type="number"
514 min="-1"
515 bind:value={config.performance.custom.skia_gpu_threads}
516 />
517 <p class="text-xs text-muted-foreground">-1 = unset, 0 = disable GPU tiles.</p>
518 </div>
519 <div class="space-y-2">
520 <Label for="web_process_memory">Web Process Memory (MB)</Label>
521 <Input
522 id="web_process_memory"
523 type="number"
524 min="0"
525 step="256"
526 bind:value={config.performance.custom.web_process_memory_mb}
527 />
528 <p class="text-xs text-muted-foreground">0 = unset (WebKit default).</p>
529 </div>
530 <div class="space-y-2">
531 <Label for="network_process_memory">Network Process Memory (MB)</Label>
532 <Input
533 id="network_process_memory"
534 type="number"
535 min="0"
536 step="128"
537 bind:value={config.performance.custom.network_process_memory_mb}
538 />
539 <p class="text-xs text-muted-foreground">0 = unset (WebKit default).</p>
540 </div>
541 <div class="space-y-2">
542 <Label for="webview_pool_prewarm">WebView Pool Prewarm</Label>
543 <Input
544 id="webview_pool_prewarm"
545 type="number"
546 min="0"
547 max="20"
548 bind:value={config.performance.custom.webview_pool_prewarm}
549 />
550 <p class="text-xs text-muted-foreground">Pre-created WebViews at startup.</p>
551 </div>
552 </div>
553 </div>
554 {/if}
555
556 <!-- Resolved values display -->
557 {#if config.performance?.resolved}
558 <div class="space-y-3">
559 <div class="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
560 Resolved Settings (applied on restart)
561 </div>
562 <div class="space-y-4 rounded-md border border-border bg-muted/20 p-4 text-sm">
563 <div class="space-y-1">
564 <div class="flex justify-between">
565 <span class="font-medium">Skia CPU Threads</span>
566 <span class="font-mono">{config.performance.resolved.skia_cpu_threads === 0 ? 'unset' : config.performance.resolved.skia_cpu_threads}</span>
567 </div>
568 <p class="text-xs text-muted-foreground">Parallel threads for CPU-based 2D rendering. More threads = faster paint operations.</p>
569 </div>
570 <div class="space-y-1">
571 <div class="flex justify-between">
572 <span class="font-medium">Skia GPU Threads</span>
573 <span class="font-mono">{config.performance.resolved.skia_gpu_threads === -1 ? 'unset' : config.performance.resolved.skia_gpu_threads}</span>
574 </div>
575 <p class="text-xs text-muted-foreground">Threads for GPU texture uploads and shader compilation.</p>
576 </div>
577 <div class="space-y-1">
578 <div class="flex justify-between">
579 <span class="font-medium">Web Process Memory</span>
580 <span class="font-mono">{config.performance.resolved.web_process_memory_mb === 0 ? 'unset' : config.performance.resolved.web_process_memory_mb + ' MB'}</span>
581 </div>
582 <p class="text-xs text-muted-foreground">Memory limit for web content processes (JavaScript, DOM, rendering).</p>
583 </div>
584 <div class="space-y-1">
585 <div class="flex justify-between">
586 <span class="font-medium">Network Process Memory</span>
587 <span class="font-mono">{config.performance.resolved.network_process_memory_mb === 0 ? 'unset' : config.performance.resolved.network_process_memory_mb + ' MB'}</span>
588 </div>
589 <p class="text-xs text-muted-foreground">Memory limit for network operations (HTTP cache, connections).</p>
590 </div>
591 <div class="space-y-1">
592 <div class="flex justify-between">
593 <span class="font-medium">WebView Pool Prewarm</span>
594 <span class="font-mono">{config.performance.resolved.webview_pool_prewarm}</span>
595 </div>
596 <p class="text-xs text-muted-foreground">Pre-created WebViews at startup for faster new tab/pane creation.</p>
597 </div>
598 </div>
599 </div>
600 {/if}
601 </Card.Content>
602 </Card.Root>
603 </Tabs.Content>
604 </Tabs.Root>
605
606 <div class="flex flex-wrap items-center justify-between gap-4 border-t border-border px-6 py-4">
607 <div class="flex items-center gap-3 text-xs uppercase tracking-[0.3em] text-muted-foreground">
608 {#if saveSuccess}
609 <span class="text-primary">Saved</span>
610 {:else}
611 <span>Ready</span>
612 {/if}
613 {#if themeEvents > 0}
614 <span>Theme x{themeEvents}</span>
615 {/if}
616 </div>
617 <div class="flex items-center gap-3">
618 <AlertDialog.Root bind:open={resetDialogOpen}>
619 <AlertDialog.Trigger disabled={saving}>
620 {#snippet child({ props })}
621 <Button variant="outline" {...props} type="button">
622 Reset Defaults
623 </Button>
624 {/snippet}
625 </AlertDialog.Trigger>
626 <AlertDialog.Content>
627 <AlertDialog.Header>
628 <AlertDialog.Title>Reset to defaults?</AlertDialog.Title>
629 <AlertDialog.Description>
630 This will reset all settings on this page to their default values.
631 You will still need to click Save to persist the changes.
632 </AlertDialog.Description>
633 </AlertDialog.Header>
634 <AlertDialog.Footer>
635 <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
636 <AlertDialog.Action onclick={doResetToDefaults}>
637 Reset Defaults
638 </AlertDialog.Action>
639 </AlertDialog.Footer>
640 </AlertDialog.Content>
641 </AlertDialog.Root>
642 <Button onclick={saveConfig} disabled={saving} type="button">
643 {saving ? "Saving…" : "Save"}
644 </Button>
645 </div>
646 </div>
647
648 <!-- Save error display -->
649 {#if saveError}
650 <div class="border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
651 {saveError}
652 </div>
653 {/if}
654
655 <!-- Restart warning after saving performance settings -->
656 {#if showRestartWarning}
657 <div class="flex items-start gap-3 border border-warning/30 bg-warning/10 px-4 py-3">
658 <RefreshCw class="mt-0.5 size-4 shrink-0 text-warning" />
659 <div class="space-y-1">
660 <p class="text-sm font-medium text-warning">Restart Required</p>
661 <p class="text-xs text-muted-foreground">
662 Performance settings are applied at browser startup. Restart Dumber for changes to take effect.
663 </p>
664 </div>
665 </div>
666 {/if}
667 {/if}
668 </div>
669</ConfigShell>