detached 7be6b41

feat(webui): config page improvements, bridge adaptation, vite build updates

- Extend ConfigPage with new sections and improved layout
- Update bridge.ts for internal HTTPS origin compatibility
- Update vite-plugin-pages for multi-page build
- Add webui config payload type
- Refactor webkit scheme_handler (extract shared logic)
- Update appearance coordinator for engine-agnostic theming
- Add cef-helper binary
commit
7be6b417da991aac50c99d1ff37d8c96fdf3732a
author
brice · · 2026-03-24 05:43 UTC
committer
brice · · 2026-03-28 04:38 UTC
parent
3aee6b3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +274 -131
A cef-helper +0 -0 binary file: no text diff

binary file: no text diff

A internal/infrastructure/config/webui_payload.go +123 -0
@@ -0,0 +1,123 @@
1+package config
2+
3+import "github.com/bnema/dumber/internal/application/port"
4+
5+// WebUIConfigAppearancePayload is the JSON shape expected by dumb://config.
6+type WebUIConfigAppearancePayload struct {
7+ SansFont string `json:"sans_font"`
8+ SerifFont string `json:"serif_font"`
9+ MonospaceFont string `json:"monospace_font"`
10+ DefaultFontSize int `json:"default_font_size"`
11+ ColorScheme string `json:"color_scheme"`
12+ LightPalette ColorPalette `json:"light_palette"`
13+ DarkPalette ColorPalette `json:"dark_palette"`
14+}
15+
16+// WebUIConfigHardwarePayload exposes detected hardware to the settings UI.
17+type WebUIConfigHardwarePayload struct {
18+ CPUCores int `json:"cpu_cores"`
19+ CPUThreads int `json:"cpu_threads"`
20+ TotalRAMMB int `json:"total_ram_mb"`
21+ GPUVendor string `json:"gpu_vendor"`
22+ GPUName string `json:"gpu_name"`
23+ VRAMMB int `json:"vram_mb"`
24+}
25+
26+// WebUIConfigCustomPerformancePayload holds user-editable custom profile values.
27+type WebUIConfigCustomPerformancePayload struct {
28+ SkiaCPUThreads int `json:"skia_cpu_threads"`
29+ SkiaGPUThreads int `json:"skia_gpu_threads"`
30+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
31+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
32+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
33+}
34+
35+// WebUIConfigResolvedPerformancePayload holds the resolved effective values.
36+type WebUIConfigResolvedPerformancePayload struct {
37+ SkiaCPUThreads int `json:"skia_cpu_threads"`
38+ SkiaGPUThreads int `json:"skia_gpu_threads"`
39+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
40+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
41+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
42+ ConservativeThreshold float64 `json:"conservative_threshold"`
43+ StrictThreshold float64 `json:"strict_threshold"`
44+}
45+
46+// WebUIConfigPerformancePayload is the performance tab payload expected by the UI.
47+type WebUIConfigPerformancePayload struct {
48+ Profile string `json:"profile"`
49+ Custom WebUIConfigCustomPerformancePayload `json:"custom"`
50+ Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"`
51+ Hardware WebUIConfigHardwarePayload `json:"hardware"`
52+}
53+
54+// WebUIConfigPayload is the full JSON shape expected by dumb://config.
55+type WebUIConfigPayload struct {
56+ Appearance WebUIConfigAppearancePayload `json:"appearance"`
57+ Performance WebUIConfigPerformancePayload `json:"performance"`
58+ DefaultUIScale float64 `json:"default_ui_scale"`
59+ DefaultSearchEngine string `json:"default_search_engine"`
60+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
61+ EngineType string `json:"engine_type"`
62+}
63+
64+// BuildWebUIConfigPayload projects the full config into the stable DTO used by
65+// the settings page across engines.
66+func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload {
67+ if cfg == nil {
68+ return WebUIConfigPayload{}
69+ }
70+
71+ perfCfg := PerformanceConfigFromEngine(&cfg.Engine)
72+ resolved := ResolvePerformanceProfile(&perfCfg, hw)
73+
74+ return WebUIConfigPayload{
75+ DefaultUIScale: cfg.DefaultUIScale,
76+ DefaultSearchEngine: cfg.DefaultSearchEngine,
77+ SearchShortcuts: cfg.SearchShortcuts,
78+ EngineType: cfg.Engine.ResolveEngineType(),
79+ Appearance: WebUIConfigAppearancePayload{
80+ SansFont: cfg.Appearance.SansFont,
81+ SerifFont: cfg.Appearance.SerifFont,
82+ MonospaceFont: cfg.Appearance.MonospaceFont,
83+ DefaultFontSize: cfg.Appearance.DefaultFontSize,
84+ ColorScheme: cfg.Appearance.ColorScheme,
85+ LightPalette: cfg.Appearance.LightPalette,
86+ DarkPalette: cfg.Appearance.DarkPalette,
87+ },
88+ Performance: WebUIConfigPerformancePayload{
89+ Profile: string(cfg.Engine.Profile),
90+ Custom: WebUIConfigCustomPerformancePayload{
91+ SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
92+ SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
93+ WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
94+ NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
95+ WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
96+ },
97+ Resolved: WebUIConfigResolvedPerformancePayload{
98+ SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
99+ SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
100+ WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
101+ NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB,
102+ WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount,
103+ ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
104+ StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
105+ },
106+ Hardware: buildWebUIHardwarePayload(hw),
107+ },
108+ }
109+}
110+
111+func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload {
112+ if hw == nil {
113+ return WebUIConfigHardwarePayload{}
114+ }
115+ return WebUIConfigHardwarePayload{
116+ CPUCores: hw.CPUCores,
117+ CPUThreads: hw.CPUThreads,
118+ TotalRAMMB: hw.TotalRAMMB(),
119+ GPUVendor: string(hw.GPUVendor),
120+ GPUName: hw.GPUName,
121+ VRAMMB: hw.VRAMMB(),
122+ }
123+}
@@ -0,0 +1,123 @@
1+package config
2+
3+import "github.com/bnema/dumber/internal/application/port"
4+
5+// WebUIConfigAppearancePayload is the JSON shape expected by dumb://config.
6+type WebUIConfigAppearancePayload struct {
7+ SansFont string `json:"sans_font"`
8+ SerifFont string `json:"serif_font"`
9+ MonospaceFont string `json:"monospace_font"`
10+ DefaultFontSize int `json:"default_font_size"`
11+ ColorScheme string `json:"color_scheme"`
12+ LightPalette ColorPalette `json:"light_palette"`
13+ DarkPalette ColorPalette `json:"dark_palette"`
14+}
15+
16+// WebUIConfigHardwarePayload exposes detected hardware to the settings UI.
17+type WebUIConfigHardwarePayload struct {
18+ CPUCores int `json:"cpu_cores"`
19+ CPUThreads int `json:"cpu_threads"`
20+ TotalRAMMB int `json:"total_ram_mb"`
21+ GPUVendor string `json:"gpu_vendor"`
22+ GPUName string `json:"gpu_name"`
23+ VRAMMB int `json:"vram_mb"`
24+}
25+
26+// WebUIConfigCustomPerformancePayload holds user-editable custom profile values.
27+type WebUIConfigCustomPerformancePayload struct {
28+ SkiaCPUThreads int `json:"skia_cpu_threads"`
29+ SkiaGPUThreads int `json:"skia_gpu_threads"`
30+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
31+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
32+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
33+}
34+
35+// WebUIConfigResolvedPerformancePayload holds the resolved effective values.
36+type WebUIConfigResolvedPerformancePayload struct {
37+ SkiaCPUThreads int `json:"skia_cpu_threads"`
38+ SkiaGPUThreads int `json:"skia_gpu_threads"`
39+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
40+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
41+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
42+ ConservativeThreshold float64 `json:"conservative_threshold"`
43+ StrictThreshold float64 `json:"strict_threshold"`
44+}
45+
46+// WebUIConfigPerformancePayload is the performance tab payload expected by the UI.
47+type WebUIConfigPerformancePayload struct {
48+ Profile string `json:"profile"`
49+ Custom WebUIConfigCustomPerformancePayload `json:"custom"`
50+ Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"`
51+ Hardware WebUIConfigHardwarePayload `json:"hardware"`
52+}
53+
54+// WebUIConfigPayload is the full JSON shape expected by dumb://config.
55+type WebUIConfigPayload struct {
56+ Appearance WebUIConfigAppearancePayload `json:"appearance"`
57+ Performance WebUIConfigPerformancePayload `json:"performance"`
58+ DefaultUIScale float64 `json:"default_ui_scale"`
59+ DefaultSearchEngine string `json:"default_search_engine"`
60+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
61+ EngineType string `json:"engine_type"`
62+}
63+
64+// BuildWebUIConfigPayload projects the full config into the stable DTO used by
65+// the settings page across engines.
66+func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload {
67+ if cfg == nil {
68+ return WebUIConfigPayload{}
69+ }
70+
71+ perfCfg := PerformanceConfigFromEngine(&cfg.Engine)
72+ resolved := ResolvePerformanceProfile(&perfCfg, hw)
73+
74+ return WebUIConfigPayload{
75+ DefaultUIScale: cfg.DefaultUIScale,
76+ DefaultSearchEngine: cfg.DefaultSearchEngine,
77+ SearchShortcuts: cfg.SearchShortcuts,
78+ EngineType: cfg.Engine.ResolveEngineType(),
79+ Appearance: WebUIConfigAppearancePayload{
80+ SansFont: cfg.Appearance.SansFont,
81+ SerifFont: cfg.Appearance.SerifFont,
82+ MonospaceFont: cfg.Appearance.MonospaceFont,
83+ DefaultFontSize: cfg.Appearance.DefaultFontSize,
84+ ColorScheme: cfg.Appearance.ColorScheme,
85+ LightPalette: cfg.Appearance.LightPalette,
86+ DarkPalette: cfg.Appearance.DarkPalette,
87+ },
88+ Performance: WebUIConfigPerformancePayload{
89+ Profile: string(cfg.Engine.Profile),
90+ Custom: WebUIConfigCustomPerformancePayload{
91+ SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
92+ SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
93+ WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
94+ NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
95+ WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
96+ },
97+ Resolved: WebUIConfigResolvedPerformancePayload{
98+ SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
99+ SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
100+ WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
101+ NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB,
102+ WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount,
103+ ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
104+ StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
105+ },
106+ Hardware: buildWebUIHardwarePayload(hw),
107+ },
108+ }
109+}
110+
111+func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload {
112+ if hw == nil {
113+ return WebUIConfigHardwarePayload{}
114+ }
115+ return WebUIConfigHardwarePayload{
116+ CPUCores: hw.CPUCores,
117+ CPUThreads: hw.CPUThreads,
118+ TotalRAMMB: hw.TotalRAMMB(),
119+ GPUVendor: string(hw.GPUVendor),
120+ GPUName: hw.GPUName,
121+ VRAMMB: hw.VRAMMB(),
122+ }
123+}
M internal/infrastructure/webkit/scheme_handler.go +1 -107
@@ -74,60 +74,6 @@ type DumbSchemeHandler struct {
7474 ctx context.Context
7575 }
7676
77-type configAppearancePayload struct {
78- SansFont string `json:"sans_font"`
79- SerifFont string `json:"serif_font"`
80- MonospaceFont string `json:"monospace_font"`
81- DefaultFontSize int `json:"default_font_size"`
82- ColorScheme string `json:"color_scheme"`
83- LightPalette config.ColorPalette `json:"light_palette"`
84- DarkPalette config.ColorPalette `json:"dark_palette"`
85-}
86-
87-type configHardwarePayload struct {
88- CPUCores int `json:"cpu_cores"`
89- CPUThreads int `json:"cpu_threads"`
90- TotalRAMMB int `json:"total_ram_mb"`
91- GPUVendor string `json:"gpu_vendor"`
92- GPUName string `json:"gpu_name"`
93- VRAMMB int `json:"vram_mb"`
94-}
95-
96-type configPerformancePayload struct {
97- Profile string `json:"profile"`
98- Custom configCustomPerformance `json:"custom"`
99- Resolved configResolvedPerformance `json:"resolved"`
100- Hardware configHardwarePayload `json:"hardware"`
101-}
102-
103-// configCustomPerformance holds user-editable fields for custom profile.
104-type configCustomPerformance struct {
105- SkiaCPUThreads int `json:"skia_cpu_threads"`
106- SkiaGPUThreads int `json:"skia_gpu_threads"`
107- WebProcessMemoryMB int `json:"web_process_memory_mb"`
108- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
109- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
110-}
111-
112-// configResolvedPerformance shows the actual values that will be applied at startup.
113-type configResolvedPerformance struct {
114- SkiaCPUThreads int `json:"skia_cpu_threads"`
115- SkiaGPUThreads int `json:"skia_gpu_threads"`
116- WebProcessMemoryMB int `json:"web_process_memory_mb"`
117- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
118- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
119- ConservativeThreshold float64 `json:"conservative_threshold"`
120- StrictThreshold float64 `json:"strict_threshold"`
121-}
122-
123-type configPayload struct {
124- Appearance configAppearancePayload `json:"appearance"`
125- Performance configPerformancePayload `json:"performance"`
126- DefaultUIScale float64 `json:"default_ui_scale"`
127- DefaultSearchEngine string `json:"default_search_engine"`
128- SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
129-}
130-
13177 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.
13278 func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
13379 log := logging.FromContext(ctx)
@@ -346,44 +292,7 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
346292 hw = &hwInfo
347293 }
348294
349- // Resolve performance profile with hardware info
350- perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine)
351- resolved := config.ResolvePerformanceProfile(&perfCfg, hw)
352-
353- resp := configPayload{
354- DefaultUIScale: cfg.DefaultUIScale,
355- DefaultSearchEngine: cfg.DefaultSearchEngine,
356- SearchShortcuts: cfg.SearchShortcuts,
357- Appearance: configAppearancePayload{
358- SansFont: cfg.Appearance.SansFont,
359- SerifFont: cfg.Appearance.SerifFont,
360- MonospaceFont: cfg.Appearance.MonospaceFont,
361- DefaultFontSize: cfg.Appearance.DefaultFontSize,
362- ColorScheme: cfg.Appearance.ColorScheme,
363- LightPalette: cfg.Appearance.LightPalette,
364- DarkPalette: cfg.Appearance.DarkPalette,
365- },
366- Performance: configPerformancePayload{
367- Profile: string(cfg.Engine.Profile),
368- Custom: configCustomPerformance{
369- SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
370- SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
371- WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
372- NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
373- WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
374- },
375- Resolved: configResolvedPerformance{
376- SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
377- SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
378- WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
379- NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB,
380- WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount,
381- ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
382- StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
383- },
384- Hardware: buildHardwarePayload(hw),
385- },
386- }
295+ resp := config.BuildWebUIConfigPayload(cfg, hw)
387296
388297 data, err := json.Marshal(resp)
389298 if err != nil {
@@ -401,21 +310,6 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
401310 }
402311 }
403312
404-// buildHardwarePayload converts HardwareInfo to JSON payload.
405-func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload {
406- if hw == nil {
407- return configHardwarePayload{}
408- }
409- return configHardwarePayload{
410- CPUCores: hw.CPUCores,
411- CPUThreads: hw.CPUThreads,
412- TotalRAMMB: hw.TotalRAMMB(),
413- GPUVendor: string(hw.GPUVendor),
414- GPUName: hw.GPUName,
415- VRAMMB: hw.VRAMMB(),
416- }
417-}
418-
419313 // RegisterPage registers a handler for a specific path.
420314 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {
421315 h.mu.Lock()
@@ -74,60 +74,6 @@ type DumbSchemeHandler struct {
74 ctx context.Context74 ctx context.Context
75 }75 }
76 76
77-type configAppearancePayload struct {
78- SansFont string `json:"sans_font"`
79- SerifFont string `json:"serif_font"`
80- MonospaceFont string `json:"monospace_font"`
81- DefaultFontSize int `json:"default_font_size"`
82- ColorScheme string `json:"color_scheme"`
83- LightPalette config.ColorPalette `json:"light_palette"`
84- DarkPalette config.ColorPalette `json:"dark_palette"`
85-}
86-
87-type configHardwarePayload struct {
88- CPUCores int `json:"cpu_cores"`
89- CPUThreads int `json:"cpu_threads"`
90- TotalRAMMB int `json:"total_ram_mb"`
91- GPUVendor string `json:"gpu_vendor"`
92- GPUName string `json:"gpu_name"`
93- VRAMMB int `json:"vram_mb"`
94-}
95-
96-type configPerformancePayload struct {
97- Profile string `json:"profile"`
98- Custom configCustomPerformance `json:"custom"`
99- Resolved configResolvedPerformance `json:"resolved"`
100- Hardware configHardwarePayload `json:"hardware"`
101-}
102-
103-// configCustomPerformance holds user-editable fields for custom profile.
104-type configCustomPerformance struct {
105- SkiaCPUThreads int `json:"skia_cpu_threads"`
106- SkiaGPUThreads int `json:"skia_gpu_threads"`
107- WebProcessMemoryMB int `json:"web_process_memory_mb"`
108- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
109- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
110-}
111-
112-// configResolvedPerformance shows the actual values that will be applied at startup.
113-type configResolvedPerformance struct {
114- SkiaCPUThreads int `json:"skia_cpu_threads"`
115- SkiaGPUThreads int `json:"skia_gpu_threads"`
116- WebProcessMemoryMB int `json:"web_process_memory_mb"`
117- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
118- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
119- ConservativeThreshold float64 `json:"conservative_threshold"`
120- StrictThreshold float64 `json:"strict_threshold"`
121-}
122-
123-type configPayload struct {
124- Appearance configAppearancePayload `json:"appearance"`
125- Performance configPerformancePayload `json:"performance"`
126- DefaultUIScale float64 `json:"default_ui_scale"`
127- DefaultSearchEngine string `json:"default_search_engine"`
128- SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
129-}
130-
131 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.77 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.
132 func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {78 func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
133 log := logging.FromContext(ctx)79 log := logging.FromContext(ctx)
@@ -346,44 +292,7 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
346 hw = &hwInfo292 hw = &hwInfo
347 }293 }
348 294
349- // Resolve performance profile with hardware info295+ resp := config.BuildWebUIConfigPayload(cfg, hw)
350- perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine)
351- resolved := config.ResolvePerformanceProfile(&perfCfg, hw)
352-
353- resp := configPayload{
354- DefaultUIScale: cfg.DefaultUIScale,
355- DefaultSearchEngine: cfg.DefaultSearchEngine,
356- SearchShortcuts: cfg.SearchShortcuts,
357- Appearance: configAppearancePayload{
358- SansFont: cfg.Appearance.SansFont,
359- SerifFont: cfg.Appearance.SerifFont,
360- MonospaceFont: cfg.Appearance.MonospaceFont,
361- DefaultFontSize: cfg.Appearance.DefaultFontSize,
362- ColorScheme: cfg.Appearance.ColorScheme,
363- LightPalette: cfg.Appearance.LightPalette,
364- DarkPalette: cfg.Appearance.DarkPalette,
365- },
366- Performance: configPerformancePayload{
367- Profile: string(cfg.Engine.Profile),
368- Custom: configCustomPerformance{
369- SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
370- SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
371- WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
372- NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
373- WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
374- },
375- Resolved: configResolvedPerformance{
376- SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
377- SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
378- WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
379- NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB,
380- WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount,
381- ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
382- StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
383- },
384- Hardware: buildHardwarePayload(hw),
385- },
386- }
387 296
388 data, err := json.Marshal(resp)297 data, err := json.Marshal(resp)
389 if err != nil {298 if err != nil {
@@ -401,21 +310,6 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
401 }310 }
402 }311 }
403 312
404-// buildHardwarePayload converts HardwareInfo to JSON payload.
405-func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload {
406- if hw == nil {
407- return configHardwarePayload{}
408- }
409- return configHardwarePayload{
410- CPUCores: hw.CPUCores,
411- CPUThreads: hw.CPUThreads,
412- TotalRAMMB: hw.TotalRAMMB(),
413- GPUVendor: string(hw.GPUVendor),
414- GPUName: hw.GPUName,
415- VRAMMB: hw.VRAMMB(),
416- }
417-}
418-
419 // RegisterPage registers a handler for a specific path.313 // RegisterPage registers a handler for a specific path.
420 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {314 func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) {
421 h.mu.Lock()315 h.mu.Lock()
M internal/ui/coordinator/content/appearance.go +10 -6
@@ -72,23 +72,27 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) {
7272 try {
7373 var cssText = %s;
7474 var prefersDark = %t;
75+ var root = document.documentElement;
7576
76- // Keep the global flag in sync
77+ // Keep the global flags in sync for both WebKit and CEF-backed pages.
7778 window.__dumber_gtk_prefers_dark = prefersDark;
79+ window.__dumber_cef_prefers_dark = prefersDark;
7880
7981 // Update dark/light class
8082 if (prefersDark) {
81- document.documentElement.classList.add('dark');
82- document.documentElement.classList.remove('light');
83+ root.classList.add('dark');
84+ root.classList.remove('light');
8385 } else {
84- document.documentElement.classList.add('light');
85- document.documentElement.classList.remove('dark');
86+ root.classList.add('light');
87+ root.classList.remove('dark');
8688 }
89+ root.style.colorScheme = prefersDark ? 'dark' : 'light';
8790
8891 // Update or insert theme style
89- var style = document.querySelector('style[data-dumber-theme-vars]');
92+ var style = document.getElementById('dumber-theme-vars') || document.querySelector('style[data-dumber-theme-vars]');
9093 if (!style) {
9194 style = document.createElement('style');
95+ style.id = 'dumber-theme-vars';
9296 style.setAttribute('data-dumber-theme-vars', '');
9397 (document.head || document.documentElement).appendChild(style);
9498 }
@@ -72,23 +72,27 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) {
72 try {72 try {
73 var cssText = %s;73 var cssText = %s;
74 var prefersDark = %t;74 var prefersDark = %t;
75+ var root = document.documentElement;
75 76
76- // Keep the global flag in sync77+ // Keep the global flags in sync for both WebKit and CEF-backed pages.
77 window.__dumber_gtk_prefers_dark = prefersDark;78 window.__dumber_gtk_prefers_dark = prefersDark;
79+ window.__dumber_cef_prefers_dark = prefersDark;
78 80
79 // Update dark/light class81 // Update dark/light class
80 if (prefersDark) {82 if (prefersDark) {
81- document.documentElement.classList.add('dark');83+ root.classList.add('dark');
82- document.documentElement.classList.remove('light');84+ root.classList.remove('light');
83 } else {85 } else {
84- document.documentElement.classList.add('light');86+ root.classList.add('light');
85- document.documentElement.classList.remove('dark');87+ root.classList.remove('dark');
86 }88 }
89+ root.style.colorScheme = prefersDark ? 'dark' : 'light';
87 90
88 // Update or insert theme style91 // Update or insert theme style
89- var style = document.querySelector('style[data-dumber-theme-vars]');92+ var style = document.getElementById('dumber-theme-vars') || document.querySelector('style[data-dumber-theme-vars]');
90 if (!style) {93 if (!style) {
91 style = document.createElement('style');94 style = document.createElement('style');
95+ style.id = 'dumber-theme-vars';
92 style.setAttribute('data-dumber-theme-vars', '');96 style.setAttribute('data-dumber-theme-vars', '');
93 (document.head || document.documentElement).appendChild(style);97 (document.head || document.documentElement).appendChild(style);
94 }98 }
M webui/src/lib/bridge.ts +6 -4
@@ -18,11 +18,13 @@ interface WebKitBridge {
1818 }
1919
2020 /**
21- * Get the WebKit message handler bridge, if available.
21+ * Get the native bridge, if available.
22+ * WebKit exposes `window.webkit.messageHandlers.dumber` while CEF injects
23+ * `window.dumber.postMessage`.
2224 */
2325 export function getWebKitBridge(): WebKitBridge | null {
2426 // eslint-disable-next-line @typescript-eslint/no-explicit-any
25- const bridge = (window as any).webkit?.messageHandlers?.dumber;
27+ const bridge = (window as any).webkit?.messageHandlers?.dumber ?? (window as any).dumber;
2628 return bridge && typeof bridge.postMessage === "function" ? bridge : null;
2729 }
2830
@@ -50,7 +52,7 @@ export function toPlainObject<T>(obj: T): T {
5052 }
5153
5254 /**
53- * Post a message to the Go backend via WebKit bridge.
55+ * Post a message to the Go backend via the native bridge.
5456 *
5557 * Automatically:
5658 * - Converts Svelte 5 Proxy objects to plain objects
@@ -62,7 +64,7 @@ export function toPlainObject<T>(obj: T): T {
6264 export function postMessage(msg: BridgeMessage): void {
6365 const bridge = getWebKitBridge();
6466 if (!bridge) {
65- throw new Error("WebKit bridge not available");
67+ throw new Error("Dumber bridge not available");
6668 }
6769
6870 // Ensure webviewId is set
@@ -18,11 +18,13 @@ interface WebKitBridge {
18 }18 }
19 19
20 /**20 /**
21- * Get the WebKit message handler bridge, if available.21+ * Get the native bridge, if available.
22+ * WebKit exposes `window.webkit.messageHandlers.dumber` while CEF injects
23+ * `window.dumber.postMessage`.
22 */24 */
23 export function getWebKitBridge(): WebKitBridge | null {25 export function getWebKitBridge(): WebKitBridge | null {
24 // eslint-disable-next-line @typescript-eslint/no-explicit-any26 // eslint-disable-next-line @typescript-eslint/no-explicit-any
25- const bridge = (window as any).webkit?.messageHandlers?.dumber;27+ const bridge = (window as any).webkit?.messageHandlers?.dumber ?? (window as any).dumber;
26 return bridge && typeof bridge.postMessage === "function" ? bridge : null;28 return bridge && typeof bridge.postMessage === "function" ? bridge : null;
27 }29 }
28 30
@@ -50,7 +52,7 @@ export function toPlainObject<T>(obj: T): T {
50 }52 }
51 53
52 /**54 /**
53- * Post a message to the Go backend via WebKit bridge.55+ * Post a message to the Go backend via the native bridge.
54 *56 *
55 * Automatically:57 * Automatically:
56 * - Converts Svelte 5 Proxy objects to plain objects58 * - Converts Svelte 5 Proxy objects to plain objects
@@ -62,7 +64,7 @@ export function toPlainObject<T>(obj: T): T {
62 export function postMessage(msg: BridgeMessage): void {64 export function postMessage(msg: BridgeMessage): void {
63 const bridge = getWebKitBridge();65 const bridge = getWebKitBridge();
64 if (!bridge) {66 if (!bridge) {
65- throw new Error("WebKit bridge not available");67+ throw new Error("Dumber bridge not available");
66 }68 }
67 69
68 // Ensure webviewId is set70 // Ensure webviewId is set
M webui/src/pages/ConfigPage.svelte +81 -7
@@ -1,6 +1,6 @@
11 <script lang="ts">
22 import { onMount } from "svelte";
3- import { ModeWatcher } from "mode-watcher";
3+ import { ModeWatcher, resetMode, setMode } from "mode-watcher";
44 import ConfigShell from "./config/ConfigShell.svelte";
55 import ShortcutsTable from "./config/ShortcutsTable.svelte";
66 import KeybindingsTab from "./config/KeybindingsTab.svelte";
@@ -83,6 +83,7 @@
8383 default_ui_scale: number;
8484 default_search_engine: string;
8585 search_shortcuts: Record<string, SearchShortcut>;
86+ engine_type: string;
8687 };
8788
8889 const PERFORMANCE_PROFILES = [
@@ -117,6 +118,49 @@
117118 let themeEvents = $state(0);
118119 let activeTab = $state("appearance");
119120
121+ function getResolvedDarkPreference(fallback?: boolean): boolean {
122+ if (typeof fallback === "boolean") {
123+ return fallback;
124+ }
125+ if (typeof window.__dumber_cef_prefers_dark === "boolean") {
126+ return window.__dumber_cef_prefers_dark;
127+ }
128+ if (typeof window.__dumber_gtk_prefers_dark === "boolean") {
129+ return window.__dumber_gtk_prefers_dark;
130+ }
131+ return document.documentElement.classList.contains("dark");
132+ }
133+
134+ function applyResolvedTheme(prefersDark: boolean): void {
135+ const root = document.documentElement;
136+ root.classList.toggle("dark", prefersDark);
137+ root.classList.toggle("light", !prefersDark);
138+ root.style.colorScheme = prefersDark ? "dark" : "light";
139+ window.__dumber_cef_prefers_dark = prefersDark;
140+ window.__dumber_gtk_prefers_dark = prefersDark;
141+ }
142+
143+ function syncModeWatcherPreference(colorScheme: string | undefined, prefersDark?: boolean): void {
144+ switch (colorScheme) {
145+ case "prefer-dark":
146+ setMode("dark");
147+ applyResolvedTheme(true);
148+ break;
149+ case "prefer-light":
150+ setMode("light");
151+ applyResolvedTheme(false);
152+ break;
153+ default:
154+ resetMode();
155+ applyResolvedTheme(getResolvedDarkPreference(prefersDark));
156+ break;
157+ }
158+ }
159+
160+ function isPerformanceSupported(cfg: ConfigDTO | null): boolean {
161+ return cfg?.engine_type !== "cef";
162+ }
163+
120164 // Load config from API
121165 async function loadConfig() {
122166 loading = true;
@@ -125,6 +169,10 @@
125169 const response = await fetch("/api/config");
126170 if (!response.ok) throw new Error("Failed to fetch config");
127171 config = (await response.json()) as ConfigDTO;
172+ syncModeWatcherPreference(config.appearance.color_scheme);
173+ if (!isPerformanceSupported(config) && activeTab === "performance") {
174+ activeTab = "appearance";
175+ }
128176 } catch (e: unknown) {
129177 loadError = getErrorMessage(e);
130178 console.error("[config] load failed", e);
@@ -146,6 +194,10 @@
146194 const response = await fetch("/api/config/default");
147195 if (!response.ok) throw new Error("Failed to fetch default config");
148196 config = (await response.json()) as ConfigDTO;
197+ syncModeWatcherPreference(config.appearance.color_scheme);
198+ if (!isPerformanceSupported(config) && activeTab === "performance") {
199+ activeTab = "appearance";
200+ }
149201 } catch (e: unknown) {
150202 console.error("[config] reset defaults failed", e);
151203 }
@@ -153,7 +205,7 @@
153205 }
154206
155207 function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
156- const bridge = window.webkit?.messageHandlers?.dumber;
208+ const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber;
157209 if (bridge && typeof bridge.postMessage === "function") {
158210 return bridge;
159211 }
@@ -180,7 +232,7 @@
180232 });
181233
182234 if (!bridge) {
183- throw new Error("WebKit bridge not available (not running inside Dumber)");
235+ throw new Error("Dumber bridge not available (not running inside Dumber)");
184236 }
185237 if (!config) {
186238 throw new Error("Config not loaded");
@@ -208,10 +260,12 @@
208260 saving = false;
209261
210262 // Show restart warning if performance tab was active
211- if (activeTab === "performance") {
263+ if (activeTab === "performance" && isPerformanceSupported(config)) {
212264 showRestartWarning = true;
213265 }
214266
267+ syncModeWatcherPreference(config?.appearance.color_scheme);
268+
215269 // Refresh config from backend (in case watcher normalized values)
216270 reloadConfig();
217271
@@ -253,12 +307,14 @@
253307 onMount(() => {
254308 console.debug("[config] mount", {
255309 webviewId: window.__dumber_webview_id,
256- hasBridge: Boolean(window.webkit?.messageHandlers?.dumber),
310+ hasBridge: Boolean(getWebKitBridge()),
257311 });
258312
259313 const onThemeChanged = (e: Event) => {
260314 themeEvents += 1;
261- console.debug("[config] theme changed", (e as CustomEvent).detail);
315+ const detail = (e as CustomEvent<{ prefersDark?: boolean }>).detail;
316+ syncModeWatcherPreference(config?.appearance.color_scheme, detail?.prefersDark);
317+ console.debug("[config] theme changed", detail);
262318 };
263319 window.addEventListener("dumber:theme-changed", onThemeChanged);
264320
@@ -305,10 +361,22 @@
305361 <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
306362 <Tabs.Trigger value="search">Search</Tabs.Trigger>
307363 <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger>
308- <Tabs.Trigger value="performance">Performance</Tabs.Trigger>
364+ <Tabs.Trigger
365+ value="performance"
366+ disabled={!isPerformanceSupported(config)}
367+ title={!isPerformanceSupported(config) ? "Performance tuning is only available with the WebKit engine" : undefined}
368+ >
369+ Performance
370+ </Tabs.Trigger>
309371 </Tabs.List>
310372 </div>
311373
374+ {#if !isPerformanceSupported(config)}
375+ <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
376+ Performance settings are disabled while using the CEF engine because these options are specific to WebKitGTK.
377+ </div>
378+ {/if}
379+
312380 <Tabs.Content value="appearance">
313381 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">
314382 <Card.Header>
@@ -449,6 +517,11 @@
449517 </Card.Description>
450518 </Card.Header>
451519 <Card.Content class="space-y-6">
520+ {#if !isPerformanceSupported(config)}
521+ <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
522+ This section is unavailable under the CEF engine.
523+ </div>
524+ {:else}
452525 <!-- Detected Hardware Card -->
453526 {#if config.performance?.hardware}
454527 {@const hw = config.performance.hardware}
@@ -614,6 +687,7 @@
614687 </div>
615688 </div>
616689 {/if}
690+ {/if}
617691 </Card.Content>
618692 </Card.Root>
619693 </Tabs.Content>
@@ -1,6 +1,6 @@
1 <script lang="ts">1 <script lang="ts">
2 import { onMount } from "svelte";2 import { onMount } from "svelte";
3- import { ModeWatcher } from "mode-watcher";3+ import { ModeWatcher, resetMode, setMode } from "mode-watcher";
4 import ConfigShell from "./config/ConfigShell.svelte";4 import ConfigShell from "./config/ConfigShell.svelte";
5 import ShortcutsTable from "./config/ShortcutsTable.svelte";5 import ShortcutsTable from "./config/ShortcutsTable.svelte";
6 import KeybindingsTab from "./config/KeybindingsTab.svelte";6 import KeybindingsTab from "./config/KeybindingsTab.svelte";
@@ -83,6 +83,7 @@
83 default_ui_scale: number;83 default_ui_scale: number;
84 default_search_engine: string;84 default_search_engine: string;
85 search_shortcuts: Record<string, SearchShortcut>;85 search_shortcuts: Record<string, SearchShortcut>;
86+ engine_type: string;
86 };87 };
87 88
88 const PERFORMANCE_PROFILES = [89 const PERFORMANCE_PROFILES = [
@@ -117,6 +118,49 @@
117 let themeEvents = $state(0);118 let themeEvents = $state(0);
118 let activeTab = $state("appearance");119 let activeTab = $state("appearance");
119 120
121+ function getResolvedDarkPreference(fallback?: boolean): boolean {
122+ if (typeof fallback === "boolean") {
123+ return fallback;
124+ }
125+ if (typeof window.__dumber_cef_prefers_dark === "boolean") {
126+ return window.__dumber_cef_prefers_dark;
127+ }
128+ if (typeof window.__dumber_gtk_prefers_dark === "boolean") {
129+ return window.__dumber_gtk_prefers_dark;
130+ }
131+ return document.documentElement.classList.contains("dark");
132+ }
133+
134+ function applyResolvedTheme(prefersDark: boolean): void {
135+ const root = document.documentElement;
136+ root.classList.toggle("dark", prefersDark);
137+ root.classList.toggle("light", !prefersDark);
138+ root.style.colorScheme = prefersDark ? "dark" : "light";
139+ window.__dumber_cef_prefers_dark = prefersDark;
140+ window.__dumber_gtk_prefers_dark = prefersDark;
141+ }
142+
143+ function syncModeWatcherPreference(colorScheme: string | undefined, prefersDark?: boolean): void {
144+ switch (colorScheme) {
145+ case "prefer-dark":
146+ setMode("dark");
147+ applyResolvedTheme(true);
148+ break;
149+ case "prefer-light":
150+ setMode("light");
151+ applyResolvedTheme(false);
152+ break;
153+ default:
154+ resetMode();
155+ applyResolvedTheme(getResolvedDarkPreference(prefersDark));
156+ break;
157+ }
158+ }
159+
160+ function isPerformanceSupported(cfg: ConfigDTO | null): boolean {
161+ return cfg?.engine_type !== "cef";
162+ }
163+
120 // Load config from API164 // Load config from API
121 async function loadConfig() {165 async function loadConfig() {
122 loading = true;166 loading = true;
@@ -125,6 +169,10 @@
125 const response = await fetch("/api/config");169 const response = await fetch("/api/config");
126 if (!response.ok) throw new Error("Failed to fetch config");170 if (!response.ok) throw new Error("Failed to fetch config");
127 config = (await response.json()) as ConfigDTO;171 config = (await response.json()) as ConfigDTO;
172+ syncModeWatcherPreference(config.appearance.color_scheme);
173+ if (!isPerformanceSupported(config) && activeTab === "performance") {
174+ activeTab = "appearance";
175+ }
128 } catch (e: unknown) {176 } catch (e: unknown) {
129 loadError = getErrorMessage(e);177 loadError = getErrorMessage(e);
130 console.error("[config] load failed", e);178 console.error("[config] load failed", e);
@@ -146,6 +194,10 @@
146 const response = await fetch("/api/config/default");194 const response = await fetch("/api/config/default");
147 if (!response.ok) throw new Error("Failed to fetch default config");195 if (!response.ok) throw new Error("Failed to fetch default config");
148 config = (await response.json()) as ConfigDTO;196 config = (await response.json()) as ConfigDTO;
197+ syncModeWatcherPreference(config.appearance.color_scheme);
198+ if (!isPerformanceSupported(config) && activeTab === "performance") {
199+ activeTab = "appearance";
200+ }
149 } catch (e: unknown) {201 } catch (e: unknown) {
150 console.error("[config] reset defaults failed", e);202 console.error("[config] reset defaults failed", e);
151 }203 }
@@ -153,7 +205,7 @@
153 }205 }
154 206
155 function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {207 function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
156- const bridge = window.webkit?.messageHandlers?.dumber;208+ const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber;
157 if (bridge && typeof bridge.postMessage === "function") {209 if (bridge && typeof bridge.postMessage === "function") {
158 return bridge;210 return bridge;
159 }211 }
@@ -180,7 +232,7 @@
180 });232 });
181 233
182 if (!bridge) {234 if (!bridge) {
183- throw new Error("WebKit bridge not available (not running inside Dumber)");235+ throw new Error("Dumber bridge not available (not running inside Dumber)");
184 }236 }
185 if (!config) {237 if (!config) {
186 throw new Error("Config not loaded");238 throw new Error("Config not loaded");
@@ -208,10 +260,12 @@
208 saving = false;260 saving = false;
209 261
210 // Show restart warning if performance tab was active262 // Show restart warning if performance tab was active
211- if (activeTab === "performance") {263+ if (activeTab === "performance" && isPerformanceSupported(config)) {
212 showRestartWarning = true;264 showRestartWarning = true;
213 }265 }
214 266
267+ syncModeWatcherPreference(config?.appearance.color_scheme);
268+
215 // Refresh config from backend (in case watcher normalized values)269 // Refresh config from backend (in case watcher normalized values)
216 reloadConfig();270 reloadConfig();
217 271
@@ -253,12 +307,14 @@
253 onMount(() => {307 onMount(() => {
254 console.debug("[config] mount", {308 console.debug("[config] mount", {
255 webviewId: window.__dumber_webview_id,309 webviewId: window.__dumber_webview_id,
256- hasBridge: Boolean(window.webkit?.messageHandlers?.dumber),310+ hasBridge: Boolean(getWebKitBridge()),
257 });311 });
258 312
259 const onThemeChanged = (e: Event) => {313 const onThemeChanged = (e: Event) => {
260 themeEvents += 1;314 themeEvents += 1;
261- console.debug("[config] theme changed", (e as CustomEvent).detail);315+ const detail = (e as CustomEvent<{ prefersDark?: boolean }>).detail;
316+ syncModeWatcherPreference(config?.appearance.color_scheme, detail?.prefersDark);
317+ console.debug("[config] theme changed", detail);
262 };318 };
263 window.addEventListener("dumber:theme-changed", onThemeChanged);319 window.addEventListener("dumber:theme-changed", onThemeChanged);
264 320
@@ -305,10 +361,22 @@
305 <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>361 <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
306 <Tabs.Trigger value="search">Search</Tabs.Trigger>362 <Tabs.Trigger value="search">Search</Tabs.Trigger>
307 <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger>363 <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger>
308- <Tabs.Trigger value="performance">Performance</Tabs.Trigger>364+ <Tabs.Trigger
365+ value="performance"
366+ disabled={!isPerformanceSupported(config)}
367+ title={!isPerformanceSupported(config) ? "Performance tuning is only available with the WebKit engine" : undefined}
368+ >
369+ Performance
370+ </Tabs.Trigger>
309 </Tabs.List>371 </Tabs.List>
310 </div>372 </div>
311 373
374+ {#if !isPerformanceSupported(config)}
375+ <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
376+ Performance settings are disabled while using the CEF engine because these options are specific to WebKitGTK.
377+ </div>
378+ {/if}
379+
312 <Tabs.Content value="appearance">380 <Tabs.Content value="appearance">
313 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">381 <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none">
314 <Card.Header>382 <Card.Header>
@@ -449,6 +517,11 @@
449 </Card.Description>517 </Card.Description>
450 </Card.Header>518 </Card.Header>
451 <Card.Content class="space-y-6">519 <Card.Content class="space-y-6">
520+ {#if !isPerformanceSupported(config)}
521+ <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
522+ This section is unavailable under the CEF engine.
523+ </div>
524+ {:else}
452 <!-- Detected Hardware Card -->525 <!-- Detected Hardware Card -->
453 {#if config.performance?.hardware}526 {#if config.performance?.hardware}
454 {@const hw = config.performance.hardware}527 {@const hw = config.performance.hardware}
@@ -614,6 +687,7 @@
614 </div>687 </div>
615 </div>688 </div>
616 {/if}689 {/if}
690+ {/if}
617 </Card.Content>691 </Card.Content>
618 </Card.Root>692 </Card.Root>
619 </Tabs.Content>693 </Tabs.Content>
M webui/src/pages/config/KeybindingsTab.svelte +2 -2
@@ -54,7 +54,7 @@
5454 error = null;
5555
5656 if (!getWebKitBridge()) {
57- error = "WebKit bridge not available";
57+ error = "Dumber bridge not available";
5858 loading = false;
5959 return;
6060 }
@@ -93,7 +93,7 @@
9393 if (!editingBinding) return;
9494
9595 if (!getWebKitBridge()) {
96- error = "WebKit bridge not available";
96+ error = "Dumber bridge not available";
9797 return;
9898 }
9999
@@ -54,7 +54,7 @@
54 error = null;54 error = null;
55 55
56 if (!getWebKitBridge()) {56 if (!getWebKitBridge()) {
57- error = "WebKit bridge not available";57+ error = "Dumber bridge not available";
58 loading = false;58 loading = false;
59 return;59 return;
60 }60 }
@@ -93,7 +93,7 @@
93 if (!editingBinding) return;93 if (!editingBinding) return;
94 94
95 if (!getWebKitBridge()) {95 if (!getWebKitBridge()) {
96- error = "WebKit bridge not available";96+ error = "Dumber bridge not available";
97 return;97 return;
98 }98 }
99 99
M webui/src/pages/homepage/messaging.ts +4 -4
@@ -1,6 +1,6 @@
11 // ═══════════════════════════════════════════════════════════════════════════════
2-// Homepage WebKit Bridge
3-// Communication layer between frontend and Go backend via WebKit message handlers
2+// Homepage bridge
3+// Communication layer between frontend and Go backend via WebKit or CEF bridge.
44 // ═══════════════════════════════════════════════════════════════════════════════
55
66 import type {
@@ -43,7 +43,7 @@ let callbacksInitialized = false;
4343 // ═══════════════════════════════════════════════════════════════════════════════
4444
4545 function getWebKitBridge(): DumberBridge | null {
46- const bridge = window.webkit?.messageHandlers?.dumber;
46+ const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber;
4747 if (bridge && typeof bridge.postMessage === 'function') {
4848 return bridge;
4949 }
@@ -178,7 +178,7 @@ async function sendMessage<T = unknown>(
178178
179179 const bridge = getWebKitBridge();
180180 if (!bridge) {
181- throw new Error('WebKit message handler not available');
181+ throw new Error('Dumber bridge not available');
182182 }
183183
184184 const requestId = generateRequestId(type);
@@ -1,6 +1,6 @@
1 // ═══════════════════════════════════════════════════════════════════════════════1 // ═══════════════════════════════════════════════════════════════════════════════
2-// Homepage WebKit Bridge2+// Homepage bridge
3-// Communication layer between frontend and Go backend via WebKit message handlers3+// Communication layer between frontend and Go backend via WebKit or CEF bridge.
4 // ═══════════════════════════════════════════════════════════════════════════════4 // ═══════════════════════════════════════════════════════════════════════════════
5 5
6 import type {6 import type {
@@ -43,7 +43,7 @@ let callbacksInitialized = false;
43 // ═══════════════════════════════════════════════════════════════════════════════43 // ═══════════════════════════════════════════════════════════════════════════════
44 44
45 function getWebKitBridge(): DumberBridge | null {45 function getWebKitBridge(): DumberBridge | null {
46- const bridge = window.webkit?.messageHandlers?.dumber;46+ const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber;
47 if (bridge && typeof bridge.postMessage === 'function') {47 if (bridge && typeof bridge.postMessage === 'function') {
48 return bridge;48 return bridge;
49 }49 }
@@ -178,7 +178,7 @@ async function sendMessage<T = unknown>(
178 178
179 const bridge = getWebKitBridge();179 const bridge = getWebKitBridge();
180 if (!bridge) {180 if (!bridge) {
181- throw new Error('WebKit message handler not available');181+ throw new Error('Dumber bridge not available');
182 }182 }
183 183
184 const requestId = generateRequestId(type);184 const requestId = generateRequestId(type);
M webui/src/types/dumber.d.ts +6 -0
@@ -77,6 +77,8 @@ declare global {
7777 __dumber_webview_id?: number;
7878 /** WebKit message handlers for native bridge */
7979 webkit?: DumberWebKit;
80+ /** CEF bridge shim injected on internal pages */
81+ dumber?: DumberBridge;
8082
8183 // ─────────────────────────────────────────────────────────────────────────
8284 // Config Page Callbacks
@@ -139,6 +141,10 @@ declare global {
139141 __dumber_color_scheme_manager?: ColorSchemeManager;
140142 /** Legacy theme setter */
141143 __dumber_setTheme?: (theme: ThemeMode) => void;
144+ /** Resolved prefers-dark state injected by the backend */
145+ __dumber_cef_prefers_dark?: boolean;
146+ /** Resolved prefers-dark state injected by the backend */
147+ __dumber_gtk_prefers_dark?: boolean;
142148 }
143149
144150 interface Document {
@@ -77,6 +77,8 @@ declare global {
77 __dumber_webview_id?: number;77 __dumber_webview_id?: number;
78 /** WebKit message handlers for native bridge */78 /** WebKit message handlers for native bridge */
79 webkit?: DumberWebKit;79 webkit?: DumberWebKit;
80+ /** CEF bridge shim injected on internal pages */
81+ dumber?: DumberBridge;
80 82
81 // ─────────────────────────────────────────────────────────────────────────83 // ─────────────────────────────────────────────────────────────────────────
82 // Config Page Callbacks84 // Config Page Callbacks
@@ -139,6 +141,10 @@ declare global {
139 __dumber_color_scheme_manager?: ColorSchemeManager;141 __dumber_color_scheme_manager?: ColorSchemeManager;
140 /** Legacy theme setter */142 /** Legacy theme setter */
141 __dumber_setTheme?: (theme: ThemeMode) => void;143 __dumber_setTheme?: (theme: ThemeMode) => void;
144+ /** Resolved prefers-dark state injected by the backend */
145+ __dumber_cef_prefers_dark?: boolean;
146+ /** Resolved prefers-dark state injected by the backend */
147+ __dumber_gtk_prefers_dark?: boolean;
142 }148 }
143 149
144 interface Document {150 interface Document {
M webui/vite-plugin-pages.ts +41 -1
@@ -15,6 +15,44 @@ interface PageConfig {
1515 filename?: string;
1616 }
1717
18+// Bootstrap a unified window.dumber bridge before the page bundle runs.
19+// On WebKit internal pages we forward to the native message handler.
20+// On CEF internal pages we fall back to the fetch-based /api/message bridge.
21+function getBridgeBootstrap(): string {
22+ return `<script>
23+ (function() {
24+ if (window.dumber && typeof window.dumber.postMessage === "function") {
25+ return;
26+ }
27+
28+ window.dumber = {
29+ postMessage: async function(msg) {
30+ const nativeBridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber;
31+ if (nativeBridge && typeof nativeBridge.postMessage === "function") {
32+ return nativeBridge.postMessage(msg);
33+ }
34+
35+ const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(msg))));
36+ const response = await fetch("/api/message", {
37+ method: "POST",
38+ headers: {
39+ "Content-Type": "application/json",
40+ "X-Dumber-Body": encoded
41+ }
42+ });
43+ const payload = await response.json();
44+ if (payload && payload._callback && typeof window[payload._callback] === "function") {
45+ window[payload._callback](payload.data);
46+ }
47+ return payload;
48+ }
49+ };
50+
51+ window.dispatchEvent(new CustomEvent("dumber:bridge-ready"));
52+ })();
53+ </script>`;
54+}
55+
1856 // Load the favicon SVG from assets
1957 function getFaviconSVG(): string {
2058 const logoPath = resolve(__dirname, "..", "assets", "logo.svg");
@@ -26,6 +64,7 @@ function generatePageHTML(config: PageConfig): string {
2664 const cssLink = config.css
2765 ? `<link rel="stylesheet" href="./${config.css}">`
2866 : "";
67+ const bridgeBootstrap = getBridgeBootstrap();
2968
3069 return `<!DOCTYPE html>
3170 <html lang="en">
@@ -33,7 +72,8 @@ function generatePageHTML(config: PageConfig): string {
3372 <meta charset="UTF-8">
3473 <meta name="viewport" content="width=device-width, initial-scale=1.0">
3574 <title>${config.title}</title>
36- <link rel="icon" type="image/x-icon" href="dumb://homepage/favicon.ico">
75+ <link rel="icon" type="image/png" href="/favicon.png">
76+ ${bridgeBootstrap}
3777 ${cssLink}
3878 </head>
3979 <body>
@@ -15,6 +15,44 @@ interface PageConfig {
15 filename?: string;15 filename?: string;
16 }16 }
17 17
18+// Bootstrap a unified window.dumber bridge before the page bundle runs.
19+// On WebKit internal pages we forward to the native message handler.
20+// On CEF internal pages we fall back to the fetch-based /api/message bridge.
21+function getBridgeBootstrap(): string {
22+ return `<script>
23+ (function() {
24+ if (window.dumber && typeof window.dumber.postMessage === "function") {
25+ return;
26+ }
27+
28+ window.dumber = {
29+ postMessage: async function(msg) {
30+ const nativeBridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber;
31+ if (nativeBridge && typeof nativeBridge.postMessage === "function") {
32+ return nativeBridge.postMessage(msg);
33+ }
34+
35+ const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(msg))));
36+ const response = await fetch("/api/message", {
37+ method: "POST",
38+ headers: {
39+ "Content-Type": "application/json",
40+ "X-Dumber-Body": encoded
41+ }
42+ });
43+ const payload = await response.json();
44+ if (payload && payload._callback && typeof window[payload._callback] === "function") {
45+ window[payload._callback](payload.data);
46+ }
47+ return payload;
48+ }
49+ };
50+
51+ window.dispatchEvent(new CustomEvent("dumber:bridge-ready"));
52+ })();
53+ </script>`;
54+}
55+
18 // Load the favicon SVG from assets56 // Load the favicon SVG from assets
19 function getFaviconSVG(): string {57 function getFaviconSVG(): string {
20 const logoPath = resolve(__dirname, "..", "assets", "logo.svg");58 const logoPath = resolve(__dirname, "..", "assets", "logo.svg");
@@ -26,6 +64,7 @@ function generatePageHTML(config: PageConfig): string {
26 const cssLink = config.css64 const cssLink = config.css
27 ? `<link rel="stylesheet" href="./${config.css}">`65 ? `<link rel="stylesheet" href="./${config.css}">`
28 : "";66 : "";
67+ const bridgeBootstrap = getBridgeBootstrap();
29 68
30 return `<!DOCTYPE html>69 return `<!DOCTYPE html>
31 <html lang="en">70 <html lang="en">
@@ -33,7 +72,8 @@ function generatePageHTML(config: PageConfig): string {
33 <meta charset="UTF-8">72 <meta charset="UTF-8">
34 <meta name="viewport" content="width=device-width, initial-scale=1.0">73 <meta name="viewport" content="width=device-width, initial-scale=1.0">
35 <title>${config.title}</title>74 <title>${config.title}</title>
36- <link rel="icon" type="image/x-icon" href="dumb://homepage/favicon.ico">75+ <link rel="icon" type="image/png" href="/favicon.png">
76+ ${bridgeBootstrap}
37 ${cssLink}77 ${cssLink}
38 </head>78 </head>
39 <body>79 <body>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close