detached b0316ff

feat(workspace): implement multi-pane window management and workspace system

commit
b0316ff1214c55048008745144c18918e8cba5ec
author
bamen · · 2025-09-22 05:55 UTC
committer
bamen · · 2025-09-22 18:37 UTC
parent
66de55a
signature
unsigned
24 files · +1555 -259
M Makefile +2 -2
@@ -43,8 +43,8 @@ build: build-frontend ## Build the application with GUI (frontend assets, then G
4343 $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
4444 @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
4545
46-build-frontend: ## Build Svelte GUI with Tailwind CSS
47- @echo "Building Svelte GUI with Tailwind CSS..."
46+build-frontend: ## Build Svelte GUI with Tailwind CSS and main-world script
47+ @echo "Building Svelte GUI with Tailwind CSS and main-world script..."
4848 @cd gui && npm install --silent && npm run build
4949 @echo "GUI build complete"
5050
@@ -43,8 +43,8 @@ build: build-frontend ## Build the application with GUI (frontend assets, then G
43 $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)43 $(GOENV) CGO_ENABLED=1 go build -p $(NPROCS) $(LDFLAGS) -tags=webkit_cgo -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
44 @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"44 @echo "✅ Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
45 45
46-build-frontend: ## Build Svelte GUI with Tailwind CSS46+build-frontend: ## Build Svelte GUI with Tailwind CSS and main-world script
47- @echo "Building Svelte GUI with Tailwind CSS..."47+ @echo "Building Svelte GUI with Tailwind CSS and main-world script..."
48 @cd gui && npm install --silent && npm run build48 @cd gui && npm install --silent && npm run build
49 @echo "GUI build complete"49 @echo "GUI build complete"
50 50
M gui/package.json +3 -1
@@ -4,13 +4,15 @@
44 "private": true,
55 "type": "module",
66 "scripts": {
7- "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage",
7+ "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage && npm run build:main-world",
88 "build:gui": "vite build --config vite.config.gui.ts",
99 "build:homepage": "vite build --config vite.config.homepage.ts",
1010 "build:color-scheme": "vite build --config vite.config.color-scheme.ts",
11+ "build:main-world": "vite build --config vite.config.main-world.ts",
1112 "dev": "npm run build:gui -- --watch",
1213 "dev:homepage": "npm run build:homepage -- --watch",
1314 "dev:color-scheme": "npm run build:color-scheme -- --watch",
15+ "dev:main-world": "npm run build:main-world -- --watch",
1416 "check": "svelte-check",
1517 "lint": "eslint src --fix",
1618 "fmt": "prettier --write .",
@@ -4,13 +4,15 @@
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "scripts": {6 "scripts": {
7- "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage",7+ "build": "npm run build:color-scheme && npm run build:gui && npm run build:homepage && npm run build:main-world",
8 "build:gui": "vite build --config vite.config.gui.ts",8 "build:gui": "vite build --config vite.config.gui.ts",
9 "build:homepage": "vite build --config vite.config.homepage.ts",9 "build:homepage": "vite build --config vite.config.homepage.ts",
10 "build:color-scheme": "vite build --config vite.config.color-scheme.ts",10 "build:color-scheme": "vite build --config vite.config.color-scheme.ts",
11+ "build:main-world": "vite build --config vite.config.main-world.ts",
11 "dev": "npm run build:gui -- --watch",12 "dev": "npm run build:gui -- --watch",
12 "dev:homepage": "npm run build:homepage -- --watch",13 "dev:homepage": "npm run build:homepage -- --watch",
13 "dev:color-scheme": "npm run build:color-scheme -- --watch",14 "dev:color-scheme": "npm run build:color-scheme -- --watch",
15+ "dev:main-world": "npm run build:main-world -- --watch",
14 "check": "svelte-check",16 "check": "svelte-check",
15 "lint": "eslint src --fix",17 "lint": "eslint src --fix",
16 "fmt": "prettier --write .",18 "fmt": "prettier --write .",
M gui/src/components/omnibox/Omnibox.svelte +9 -0
@@ -284,6 +284,15 @@
284284 } catch (error) {
285285 console.error('Error in findQuery:', error);
286286 }
287+ },
288+ setActive: (active) => {
289+ try {
290+ console.log('🎯 Omnibox setActive called with:', active);
291+ // For now, this is a no-op but maintains API compatibility
292+ // In the future, this could control focus management or visibility
293+ } catch (error) {
294+ console.error('Error in setActive:', error);
295+ }
287296 }
288297 };
289298
@@ -284,6 +284,15 @@
284 } catch (error) {284 } catch (error) {
285 console.error('Error in findQuery:', error);285 console.error('Error in findQuery:', error);
286 }286 }
287+ },
288+ setActive: (active) => {
289+ try {
290+ console.log('🎯 Omnibox setActive called with:', active);
291+ // For now, this is a no-op but maintains API compatibility
292+ // In the future, this could control focus management or visibility
293+ } catch (error) {
294+ console.error('Error in setActive:', error);
295+ }
287 }296 }
288 };297 };
289 298
M gui/src/components/omnibox/messaging.ts +1 -0
@@ -98,6 +98,7 @@ declare global {
9898 open: (mode?: string, query?: string) => void;
9999 close: () => void;
100100 findQuery: (query: string) => void;
101+ setActive: (active: boolean) => void;
101102 };
102103 }
103104 }⏎̸
@@ -98,6 +98,7 @@ declare global {
98 open: (mode?: string, query?: string) => void;98 open: (mode?: string, query?: string) => void;
99 close: () => void;99 close: () => void;
100 findQuery: (query: string) => void;100 findQuery: (query: string) => void;
101+ setActive: (active: boolean) => void;
101 };102 };
102 }103 }
103 }⏎̸104 }⏎̸
M gui/src/injected/gui.ts +25 -11
@@ -9,7 +9,9 @@
99 import { initializeToast, type ToastConfig } from './modules/toast';
1010 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';
1111 import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace';
12+import { initializeWindowOpenInterceptor } from './modules/window-open';
1213 import { keyboardService, type KeyboardService } from '$lib/keyboard';
14+import type { Suggestion } from '../components/omnibox/types';
1315 // Note: color-scheme module is loaded separately at document-start by WebKit
1416
1517 // Global interface for the unified GUI system
@@ -48,6 +50,16 @@ declare global {
4850 __dumber_keyboard?: KeyboardService;
4951 __dumber_workspace_config?: WorkspaceConfigPayload;
5052 __dumber_workspace?: WorkspaceRuntime;
53+ __dumber_pane?: { id: string; active: boolean };
54+ __dumber_omnibox?: {
55+ setSuggestions: (suggestions: Suggestion[]) => void;
56+ toggle: () => void;
57+ open: (mode?: string, query?: string) => void;
58+ close: () => void;
59+ findQuery: (query: string) => void;
60+ setActive: (active: boolean) => void;
61+ };
62+ __dumber_gui_bootstrap?: () => void;
5163 // Legacy compatibility functions for Go bridge
5264 __dumber_toggle?: () => void;
5365 __dumber_find_open?: (query?: string) => void;
@@ -96,10 +108,10 @@ if (!window.__dumber_gui_ready) {
96108 // Handle GUI visibility based on focus
97109 if (window.__dumber_omnibox) {
98110 if (workspaceHasFocus) {
99- window.__dumber_omnibox.setActive?.(true);
111+ window.__dumber_omnibox.setActive(true);
100112 } else {
101- window.__dumber_omnibox.setActive?.(false);
102- window.__dumber_omnibox.close?.();
113+ window.__dumber_omnibox.setActive(false);
114+ window.__dumber_omnibox.close();
103115 }
104116 }
105117 });
@@ -109,6 +121,9 @@ if (!window.__dumber_gui_ready) {
109121 try {
110122 // Page-world bridge is injected by WebKit; avoid duplicate injection here
111123
124+ // Initialize window.open interceptor first (must be early)
125+ initializeWindowOpenInterceptor();
126+
112127 // Initialize toast system first
113128 await initializeToast();
114129
@@ -331,22 +346,21 @@ if (!window.__dumber_gui_ready) {
331346 };
332347
333348 // Bootstrap function for lazy GUI initialization by workspace manager
334- window.__dumber_gui_bootstrap = function(paneId: string) {
335- console.log(`[gui-bootstrap] Initializing GUI for pane ${paneId}`);
349+ window.__dumber_gui_bootstrap = function() {
350+ console.log('[gui-bootstrap] Initializing GUI for workspace');
336351
337- // Update current pane ID
338- currentPaneId = paneId;
352+ // Pane ID will be updated via focus events
339353
340354 // Ensure omnibox is available for this pane
341355 if (!window.__dumber_omnibox) {
342- console.log(`[gui-bootstrap] Omnibox not yet available for pane ${paneId}, will be loaded when needed`);
356+ console.log(`[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`);
343357 } else {
344- console.log(`[gui-bootstrap] Omnibox already available for pane ${paneId}`);
358+ console.log(`[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`);
345359 // Ensure omnibox is aware of the current pane
346- window.__dumber_omnibox.setActive?.(workspaceHasFocus);
360+ window.__dumber_omnibox.setActive(workspaceHasFocus);
347361 }
348362
349- console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${paneId}`);
363+ console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`);
350364 };
351365
352366 }
@@ -9,7 +9,9 @@
9 import { initializeToast, type ToastConfig } from './modules/toast';9 import { initializeToast, type ToastConfig } from './modules/toast';
10 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';10 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';
11 import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace';11 import { initializeWorkspace, type WorkspaceConfigPayload, type WorkspaceRuntime } from './modules/workspace';
12+import { initializeWindowOpenInterceptor } from './modules/window-open';
12 import { keyboardService, type KeyboardService } from '$lib/keyboard';13 import { keyboardService, type KeyboardService } from '$lib/keyboard';
14+import type { Suggestion } from '../components/omnibox/types';
13 // Note: color-scheme module is loaded separately at document-start by WebKit15 // Note: color-scheme module is loaded separately at document-start by WebKit
14 16
15 // Global interface for the unified GUI system17 // Global interface for the unified GUI system
@@ -48,6 +50,16 @@ declare global {
48 __dumber_keyboard?: KeyboardService;50 __dumber_keyboard?: KeyboardService;
49 __dumber_workspace_config?: WorkspaceConfigPayload;51 __dumber_workspace_config?: WorkspaceConfigPayload;
50 __dumber_workspace?: WorkspaceRuntime;52 __dumber_workspace?: WorkspaceRuntime;
53+ __dumber_pane?: { id: string; active: boolean };
54+ __dumber_omnibox?: {
55+ setSuggestions: (suggestions: Suggestion[]) => void;
56+ toggle: () => void;
57+ open: (mode?: string, query?: string) => void;
58+ close: () => void;
59+ findQuery: (query: string) => void;
60+ setActive: (active: boolean) => void;
61+ };
62+ __dumber_gui_bootstrap?: () => void;
51 // Legacy compatibility functions for Go bridge63 // Legacy compatibility functions for Go bridge
52 __dumber_toggle?: () => void;64 __dumber_toggle?: () => void;
53 __dumber_find_open?: (query?: string) => void;65 __dumber_find_open?: (query?: string) => void;
@@ -96,10 +108,10 @@ if (!window.__dumber_gui_ready) {
96 // Handle GUI visibility based on focus108 // Handle GUI visibility based on focus
97 if (window.__dumber_omnibox) {109 if (window.__dumber_omnibox) {
98 if (workspaceHasFocus) {110 if (workspaceHasFocus) {
99- window.__dumber_omnibox.setActive?.(true);111+ window.__dumber_omnibox.setActive(true);
100 } else {112 } else {
101- window.__dumber_omnibox.setActive?.(false);113+ window.__dumber_omnibox.setActive(false);
102- window.__dumber_omnibox.close?.();114+ window.__dumber_omnibox.close();
103 }115 }
104 }116 }
105 });117 });
@@ -109,6 +121,9 @@ if (!window.__dumber_gui_ready) {
109 try {121 try {
110 // Page-world bridge is injected by WebKit; avoid duplicate injection here122 // Page-world bridge is injected by WebKit; avoid duplicate injection here
111 123
124+ // Initialize window.open interceptor first (must be early)
125+ initializeWindowOpenInterceptor();
126+
112 // Initialize toast system first127 // Initialize toast system first
113 await initializeToast();128 await initializeToast();
114 129
@@ -331,22 +346,21 @@ if (!window.__dumber_gui_ready) {
331 };346 };
332 347
333 // Bootstrap function for lazy GUI initialization by workspace manager348 // Bootstrap function for lazy GUI initialization by workspace manager
334- window.__dumber_gui_bootstrap = function(paneId: string) {349+ window.__dumber_gui_bootstrap = function() {
335- console.log(`[gui-bootstrap] Initializing GUI for pane ${paneId}`);350+ console.log('[gui-bootstrap] Initializing GUI for workspace');
336 351
337- // Update current pane ID352+ // Pane ID will be updated via focus events
338- currentPaneId = paneId;
339 353
340 // Ensure omnibox is available for this pane354 // Ensure omnibox is available for this pane
341 if (!window.__dumber_omnibox) {355 if (!window.__dumber_omnibox) {
342- console.log(`[gui-bootstrap] Omnibox not yet available for pane ${paneId}, will be loaded when needed`);356+ console.log(`[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`);
343 } else {357 } else {
344- console.log(`[gui-bootstrap] Omnibox already available for pane ${paneId}`);358+ console.log(`[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`);
345 // Ensure omnibox is aware of the current pane359 // Ensure omnibox is aware of the current pane
346- window.__dumber_omnibox.setActive?.(workspaceHasFocus);360+ window.__dumber_omnibox.setActive(workspaceHasFocus);
347 }361 }
348 362
349- console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${paneId}`);363+ console.log(`[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`);
350 };364 };
351 365
352 }366 }
A gui/src/injected/main-world.ts +402 -0
@@ -0,0 +1,402 @@
1+/**
2+ * Main-world bridge script for Dumber Browser
3+ *
4+ * This script is injected into the page-world context to provide:
5+ * - Window.open interceptor with debouncing
6+ * - Toast notifications bridge
7+ * - Theme management
8+ * - DOM zoom functionality
9+ * - Omnibox suggestions bridge
10+ */
11+
12+// Type definitions for window extensions
13+declare global {
14+ interface Window {
15+ __dumber: DumberAPI;
16+ __dumber_page_bridge_installed?: boolean;
17+ __dumber_window_open_intercepted?: boolean;
18+ __dumber_dom_zoom_seed?: number;
19+ __dumber_dom_zoom_level?: number;
20+ __dumber_initial_theme?: string;
21+ __dumber_setTheme?: (theme: 'light' | 'dark') => void;
22+ __dumber_applyDomZoom?: (level: number) => void;
23+ __dumber_showToast?: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => number | void;
24+ __dumber_showZoomToast?: (level: number) => void;
25+ __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void;
26+ webkit?: {
27+ messageHandlers?: {
28+ dumber?: {
29+ postMessage: (message: string) => void;
30+ };
31+ };
32+ };
33+ }
34+}
35+
36+interface WindowIntent {
37+ url: string;
38+ target: string;
39+ features: string;
40+ timestamp: number;
41+ userTriggered: boolean;
42+}
43+
44+interface Suggestion {
45+ // Define proper suggestion types based on your needs
46+ [key: string]: unknown;
47+}
48+
49+interface DumberAPI {
50+ toast: {
51+ show: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => void;
52+ zoom: (level: number) => void;
53+ };
54+ omnibox: {
55+ suggestions: (suggestions: Suggestion[]) => void;
56+ };
57+}
58+
59+(() => {
60+ try {
61+ // Prevent multiple installations
62+ if (window.__dumber_page_bridge_installed) {
63+ return;
64+ }
65+ window.__dumber_page_bridge_installed = true;
66+
67+ // Initialize zoom level placeholder (will be replaced by Go)
68+ const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go
69+ window.__dumber_dom_zoom_seed = initialZoom;
70+
71+ // Theme setter function for GTK theme integration
72+ window.__dumber_setTheme = (theme: 'light' | 'dark') => {
73+ window.__dumber_initial_theme = theme;
74+ console.log('[dumber] Setting theme to:', theme);
75+ if (theme === 'dark') {
76+ document.documentElement.classList.add('dark');
77+ } else {
78+ document.documentElement.classList.remove('dark');
79+ }
80+ };
81+
82+ // Initialize unified API object
83+ window.__dumber = window.__dumber || {} as DumberAPI;
84+
85+ // Toast bridge
86+ window.__dumber.toast = window.__dumber.toast || {
87+ show: (message: string, duration?: number, type?: string) => {
88+ try {
89+ document.dispatchEvent(new CustomEvent('dumber:toast:show', {
90+ detail: { message, duration, type }
91+ }));
92+ // Legacy compatibility
93+ document.dispatchEvent(new CustomEvent('dumber:showToast', {
94+ detail: { message, duration, type }
95+ }));
96+ } catch {
97+ // ignore
98+ }
99+ },
100+ zoom: (level: number) => {
101+ try {
102+ document.dispatchEvent(new CustomEvent('dumber:toast:zoom', {
103+ detail: { level }
104+ }));
105+ // Legacy compatibility
106+ document.dispatchEvent(new CustomEvent('dumber:showZoomToast', {
107+ detail: { level }
108+ }));
109+ } catch {
110+ // ignore
111+ }
112+ }
113+ };
114+
115+ // Legacy toast helpers
116+ window.__dumber_showToast = (message: string, duration?: number, type?: 'info' | 'success' | 'error') => {
117+ window.__dumber.toast.show(message, duration, type);
118+ };
119+ window.__dumber_showZoomToast = (level: number) => {
120+ window.__dumber.toast.zoom(level);
121+ };
122+
123+ // DOM zoom functionality
124+ if (typeof window.__dumber_dom_zoom_level !== 'number') {
125+ window.__dumber_dom_zoom_level = initialZoom;
126+ }
127+
128+ const applyZoomStyles = (node: HTMLElement, level: number): void => {
129+ if (!node) return;
130+
131+ if (Math.abs(level - 1.0) < 1e-6) {
132+ // Reset zoom
133+ node.style.removeProperty('zoom');
134+ node.style.removeProperty('transform');
135+ node.style.removeProperty('transform-origin');
136+ node.style.removeProperty('width');
137+ node.style.removeProperty('min-width');
138+ node.style.removeProperty('height');
139+ node.style.removeProperty('min-height');
140+ return;
141+ }
142+
143+ const scale = level;
144+ const inversePercent = 100 / scale;
145+ const widthValue = `${inversePercent}%`;
146+
147+ node.style.removeProperty('zoom');
148+ node.style.transform = `scale(${scale})`;
149+ node.style.transformOrigin = '0 0';
150+ node.style.width = widthValue;
151+ node.style.minWidth = widthValue;
152+ node.style.minHeight = '100%';
153+ };
154+
155+ window.__dumber_applyDomZoom = (level: number) => {
156+ try {
157+ window.__dumber_dom_zoom_level = level;
158+ window.__dumber_dom_zoom_seed = level;
159+ applyZoomStyles(document.documentElement, level);
160+ if (document.body) {
161+ applyZoomStyles(document.body, level);
162+ }
163+ } catch (e) {
164+ console.error('[dumber] DOM zoom error', e);
165+ }
166+ };
167+
168+ // Apply initial zoom
169+ window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
170+
171+ if (!document.body) {
172+ document.addEventListener('DOMContentLoaded', () => {
173+ if (typeof window.__dumber_dom_zoom_level === 'number') {
174+ window.__dumber_applyDomZoom!(window.__dumber_dom_zoom_level);
175+ }
176+ }, { once: true });
177+ }
178+
179+ // Omnibox suggestions bridge
180+ const omniboxQueue: Suggestion[] = [];
181+ let omniboxReady = false;
182+
183+ const omniboxDispatch = (suggestions: Suggestion[]) => {
184+ try {
185+ document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', {
186+ detail: { suggestions }
187+ }));
188+ // Legacy compatibility
189+ document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', {
190+ detail: { suggestions }
191+ }));
192+ } catch (e) {
193+ console.error('[dumber] Omnibox dispatch error', e);
194+ }
195+ };
196+
197+ window.__dumber_omnibox_suggestions = (suggestions: Suggestion[]) => {
198+ if (omniboxReady) {
199+ omniboxDispatch(suggestions);
200+ } else {
201+ try {
202+ omniboxQueue.push(...suggestions);
203+ } catch (e){
204+ console.error('[dumber] Omnibox queue error', e);
205+ }
206+ }
207+ };
208+
209+ document.addEventListener('dumber:omnibox-ready', () => {
210+ omniboxReady = true;
211+ if (omniboxQueue && omniboxQueue.length) {
212+ const items = omniboxQueue.slice();
213+ omniboxQueue.length = 0;
214+ items.forEach(s => omniboxDispatch([s]) );
215+ }
216+ });
217+
218+ // Unified omnibox API
219+ window.__dumber.omnibox = window.__dumber.omnibox || {
220+ suggestions: (suggestions: Suggestion[]) => {
221+ window.__dumber_omnibox_suggestions!(suggestions);
222+ }
223+ };
224+
225+ // User interaction tracking and window.open interceptor
226+ let lastUserInteraction = 0;
227+ const INTERACTION_TIMEOUT = 5000; // 5 seconds
228+ const WINDOW_OPEN_DEBOUNCE = 1000; // 1 second between calls
229+ const recentWindowOpenCalls = new Map<string, number>();
230+
231+ const trackUserInteraction = () => {
232+ lastUserInteraction = Date.now();
233+ };
234+
235+ const isUserInteraction = (): boolean => {
236+ const now = Date.now();
237+ const timeSinceInteraction = now - lastUserInteraction;
238+ return timeSinceInteraction <= INTERACTION_TIMEOUT;
239+ };
240+
241+ const isDuplicateWindowOpen = (url: string): boolean => {
242+ const now = Date.now();
243+ const lastCall = recentWindowOpenCalls.get(url);
244+
245+ if (lastCall && (now - lastCall) < WINDOW_OPEN_DEBOUNCE) {
246+ return true;
247+ }
248+
249+ recentWindowOpenCalls.set(url, now);
250+
251+ // Clean up old entries
252+ for (const [key, timestamp] of recentWindowOpenCalls.entries()) {
253+ if (now - timestamp > WINDOW_OPEN_DEBOUNCE * 2) {
254+ recentWindowOpenCalls.delete(key);
255+ }
256+ }
257+
258+ return false;
259+ };
260+
261+ // Track user interactions for popup validation
262+ ['click', 'mousedown', 'keydown', 'touchstart'].forEach(eventType => {
263+ document.addEventListener(eventType, trackUserInteraction, true);
264+ });
265+
266+ console.log('[window-open] User interaction tracking enabled');
267+
268+ // Window.open interceptor - only install once
269+ if (!window.__dumber_window_open_intercepted) {
270+ try {
271+
272+ window.open = function(
273+ url?: string | URL | null,
274+ target?: string | null,
275+ features?: string | null
276+ ): WindowProxy | null {
277+ console.log('[window-open] Bridge called with:', url, target, features);
278+ console.log('[window-open] Call stack:', new Error().stack);
279+
280+ const urlString = url ? (typeof url === 'string' ? url : url.toString()) : '';
281+
282+ // Check for duplicate calls to prevent multiple panes
283+ if (isDuplicateWindowOpen(urlString)) {
284+ console.log('[window-open] Blocked: duplicate call within debounce window');
285+ return {
286+ closed: true,
287+ location: { href: urlString },
288+ close: () => {},
289+ focus: () => {},
290+ blur: () => {},
291+ postMessage: () => {}
292+ } as unknown as WindowProxy;
293+ }
294+
295+ // Check for user interaction to prevent popup spam
296+ if (!isUserInteraction()) {
297+ console.log('[window-open] Blocked: no recent user interaction detected');
298+ return {
299+ closed: true,
300+ location: { href: '' },
301+ close: () => {},
302+ focus: () => {},
303+ blur: () => {},
304+ postMessage: () => {}
305+ } as unknown as WindowProxy;
306+ }
307+
308+ const originalFeatures = features || '';
309+ console.log('[window-open] Using original features:', originalFeatures);
310+
311+ const intent: WindowIntent = {
312+ url: urlString,
313+ target: target || '_blank',
314+ features: originalFeatures,
315+ timestamp: Date.now(),
316+ userTriggered: true
317+ };
318+
319+ // Send message to Go to handle popup creation
320+ try {
321+ if (window.webkit?.messageHandlers?.dumber) {
322+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
323+ type: 'handle-window-open',
324+ payload: intent
325+ }));
326+ console.log('[window-open] Sent window.open request to Go - bypassing WebKit completely');
327+ }
328+ } catch (e) {
329+ console.error('[window-open] Failed to send to Go:', e);
330+ }
331+
332+ // Return fake window object - don't call original window.open
333+ console.log('[window-open] Returning fake window object - bypassing WebKit create signal');
334+ return {
335+ closed: false,
336+ location: { href: urlString },
337+ close() { (this as { closed: boolean }).closed = true; },
338+ focus: () => {},
339+ blur: () => {},
340+ postMessage: () => {}
341+ } as unknown as WindowProxy;
342+ };
343+
344+ window.__dumber_window_open_intercepted = true;
345+ console.log('[window-open] ✅ Page-world bridge installed');
346+ } catch (error) {
347+ console.error('[window-open] ❌ Failed to install page-world bridge:', error);
348+ }
349+ }
350+
351+ // Console capture functionality
352+ const setupConsoleCapture = () => {
353+ const originalConsole = {
354+ log: console.log,
355+ warn: console.warn,
356+ error: console.error,
357+ info: console.info,
358+ debug: console.debug
359+ };
360+
361+ const sendConsoleMessage = (level: string, args: unknown[]) => {
362+ try {
363+ const message = args.map(arg =>
364+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
365+ ).join(' ');
366+
367+ const formattedMessage = `${window.location.href} ${message}`;
368+
369+ // Send to Go via existing message handler
370+ if (window.webkit?.messageHandlers?.dumber) {
371+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
372+ type: 'console-message',
373+ payload: { level, message: formattedMessage, url: window.location.href }
374+ }));
375+ }
376+ } catch {
377+ // Silently ignore errors in console capture to avoid infinite loops
378+ }
379+ };
380+
381+ const createConsoleWrapper = (originalMethod: (...args: unknown[]) => void, level: string) => {
382+ return function(...args: unknown[]) {
383+ originalMethod.apply(console, args);
384+ sendConsoleMessage(level, args);
385+ };
386+ };
387+
388+ // Override console methods to capture messages
389+ console.log = createConsoleWrapper(originalConsole.log, 'LOG');
390+ console.warn = createConsoleWrapper(originalConsole.warn, 'WARN');
391+ console.error = createConsoleWrapper(originalConsole.error, 'ERROR');
392+ console.info = createConsoleWrapper(originalConsole.info, 'INFO');
393+ console.debug = createConsoleWrapper(originalConsole.debug, 'DEBUG');
394+ };
395+
396+ // Initialize console capture
397+ setupConsoleCapture();
398+
399+ } catch (e) {
400+ console.warn('[dumber] unified bridge init failed', e);
401+ }
402+})();⏎̸
@@ -0,0 +1,402 @@
1+/**
2+ * Main-world bridge script for Dumber Browser
3+ *
4+ * This script is injected into the page-world context to provide:
5+ * - Window.open interceptor with debouncing
6+ * - Toast notifications bridge
7+ * - Theme management
8+ * - DOM zoom functionality
9+ * - Omnibox suggestions bridge
10+ */
11+
12+// Type definitions for window extensions
13+declare global {
14+ interface Window {
15+ __dumber: DumberAPI;
16+ __dumber_page_bridge_installed?: boolean;
17+ __dumber_window_open_intercepted?: boolean;
18+ __dumber_dom_zoom_seed?: number;
19+ __dumber_dom_zoom_level?: number;
20+ __dumber_initial_theme?: string;
21+ __dumber_setTheme?: (theme: 'light' | 'dark') => void;
22+ __dumber_applyDomZoom?: (level: number) => void;
23+ __dumber_showToast?: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => number | void;
24+ __dumber_showZoomToast?: (level: number) => void;
25+ __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void;
26+ webkit?: {
27+ messageHandlers?: {
28+ dumber?: {
29+ postMessage: (message: string) => void;
30+ };
31+ };
32+ };
33+ }
34+}
35+
36+interface WindowIntent {
37+ url: string;
38+ target: string;
39+ features: string;
40+ timestamp: number;
41+ userTriggered: boolean;
42+}
43+
44+interface Suggestion {
45+ // Define proper suggestion types based on your needs
46+ [key: string]: unknown;
47+}
48+
49+interface DumberAPI {
50+ toast: {
51+ show: (message: string, duration?: number, type?: 'info' | 'success' | 'error') => void;
52+ zoom: (level: number) => void;
53+ };
54+ omnibox: {
55+ suggestions: (suggestions: Suggestion[]) => void;
56+ };
57+}
58+
59+(() => {
60+ try {
61+ // Prevent multiple installations
62+ if (window.__dumber_page_bridge_installed) {
63+ return;
64+ }
65+ window.__dumber_page_bridge_installed = true;
66+
67+ // Initialize zoom level placeholder (will be replaced by Go)
68+ const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go
69+ window.__dumber_dom_zoom_seed = initialZoom;
70+
71+ // Theme setter function for GTK theme integration
72+ window.__dumber_setTheme = (theme: 'light' | 'dark') => {
73+ window.__dumber_initial_theme = theme;
74+ console.log('[dumber] Setting theme to:', theme);
75+ if (theme === 'dark') {
76+ document.documentElement.classList.add('dark');
77+ } else {
78+ document.documentElement.classList.remove('dark');
79+ }
80+ };
81+
82+ // Initialize unified API object
83+ window.__dumber = window.__dumber || {} as DumberAPI;
84+
85+ // Toast bridge
86+ window.__dumber.toast = window.__dumber.toast || {
87+ show: (message: string, duration?: number, type?: string) => {
88+ try {
89+ document.dispatchEvent(new CustomEvent('dumber:toast:show', {
90+ detail: { message, duration, type }
91+ }));
92+ // Legacy compatibility
93+ document.dispatchEvent(new CustomEvent('dumber:showToast', {
94+ detail: { message, duration, type }
95+ }));
96+ } catch {
97+ // ignore
98+ }
99+ },
100+ zoom: (level: number) => {
101+ try {
102+ document.dispatchEvent(new CustomEvent('dumber:toast:zoom', {
103+ detail: { level }
104+ }));
105+ // Legacy compatibility
106+ document.dispatchEvent(new CustomEvent('dumber:showZoomToast', {
107+ detail: { level }
108+ }));
109+ } catch {
110+ // ignore
111+ }
112+ }
113+ };
114+
115+ // Legacy toast helpers
116+ window.__dumber_showToast = (message: string, duration?: number, type?: 'info' | 'success' | 'error') => {
117+ window.__dumber.toast.show(message, duration, type);
118+ };
119+ window.__dumber_showZoomToast = (level: number) => {
120+ window.__dumber.toast.zoom(level);
121+ };
122+
123+ // DOM zoom functionality
124+ if (typeof window.__dumber_dom_zoom_level !== 'number') {
125+ window.__dumber_dom_zoom_level = initialZoom;
126+ }
127+
128+ const applyZoomStyles = (node: HTMLElement, level: number): void => {
129+ if (!node) return;
130+
131+ if (Math.abs(level - 1.0) < 1e-6) {
132+ // Reset zoom
133+ node.style.removeProperty('zoom');
134+ node.style.removeProperty('transform');
135+ node.style.removeProperty('transform-origin');
136+ node.style.removeProperty('width');
137+ node.style.removeProperty('min-width');
138+ node.style.removeProperty('height');
139+ node.style.removeProperty('min-height');
140+ return;
141+ }
142+
143+ const scale = level;
144+ const inversePercent = 100 / scale;
145+ const widthValue = `${inversePercent}%`;
146+
147+ node.style.removeProperty('zoom');
148+ node.style.transform = `scale(${scale})`;
149+ node.style.transformOrigin = '0 0';
150+ node.style.width = widthValue;
151+ node.style.minWidth = widthValue;
152+ node.style.minHeight = '100%';
153+ };
154+
155+ window.__dumber_applyDomZoom = (level: number) => {
156+ try {
157+ window.__dumber_dom_zoom_level = level;
158+ window.__dumber_dom_zoom_seed = level;
159+ applyZoomStyles(document.documentElement, level);
160+ if (document.body) {
161+ applyZoomStyles(document.body, level);
162+ }
163+ } catch (e) {
164+ console.error('[dumber] DOM zoom error', e);
165+ }
166+ };
167+
168+ // Apply initial zoom
169+ window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
170+
171+ if (!document.body) {
172+ document.addEventListener('DOMContentLoaded', () => {
173+ if (typeof window.__dumber_dom_zoom_level === 'number') {
174+ window.__dumber_applyDomZoom!(window.__dumber_dom_zoom_level);
175+ }
176+ }, { once: true });
177+ }
178+
179+ // Omnibox suggestions bridge
180+ const omniboxQueue: Suggestion[] = [];
181+ let omniboxReady = false;
182+
183+ const omniboxDispatch = (suggestions: Suggestion[]) => {
184+ try {
185+ document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', {
186+ detail: { suggestions }
187+ }));
188+ // Legacy compatibility
189+ document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', {
190+ detail: { suggestions }
191+ }));
192+ } catch (e) {
193+ console.error('[dumber] Omnibox dispatch error', e);
194+ }
195+ };
196+
197+ window.__dumber_omnibox_suggestions = (suggestions: Suggestion[]) => {
198+ if (omniboxReady) {
199+ omniboxDispatch(suggestions);
200+ } else {
201+ try {
202+ omniboxQueue.push(...suggestions);
203+ } catch (e){
204+ console.error('[dumber] Omnibox queue error', e);
205+ }
206+ }
207+ };
208+
209+ document.addEventListener('dumber:omnibox-ready', () => {
210+ omniboxReady = true;
211+ if (omniboxQueue && omniboxQueue.length) {
212+ const items = omniboxQueue.slice();
213+ omniboxQueue.length = 0;
214+ items.forEach(s => omniboxDispatch([s]) );
215+ }
216+ });
217+
218+ // Unified omnibox API
219+ window.__dumber.omnibox = window.__dumber.omnibox || {
220+ suggestions: (suggestions: Suggestion[]) => {
221+ window.__dumber_omnibox_suggestions!(suggestions);
222+ }
223+ };
224+
225+ // User interaction tracking and window.open interceptor
226+ let lastUserInteraction = 0;
227+ const INTERACTION_TIMEOUT = 5000; // 5 seconds
228+ const WINDOW_OPEN_DEBOUNCE = 1000; // 1 second between calls
229+ const recentWindowOpenCalls = new Map<string, number>();
230+
231+ const trackUserInteraction = () => {
232+ lastUserInteraction = Date.now();
233+ };
234+
235+ const isUserInteraction = (): boolean => {
236+ const now = Date.now();
237+ const timeSinceInteraction = now - lastUserInteraction;
238+ return timeSinceInteraction <= INTERACTION_TIMEOUT;
239+ };
240+
241+ const isDuplicateWindowOpen = (url: string): boolean => {
242+ const now = Date.now();
243+ const lastCall = recentWindowOpenCalls.get(url);
244+
245+ if (lastCall && (now - lastCall) < WINDOW_OPEN_DEBOUNCE) {
246+ return true;
247+ }
248+
249+ recentWindowOpenCalls.set(url, now);
250+
251+ // Clean up old entries
252+ for (const [key, timestamp] of recentWindowOpenCalls.entries()) {
253+ if (now - timestamp > WINDOW_OPEN_DEBOUNCE * 2) {
254+ recentWindowOpenCalls.delete(key);
255+ }
256+ }
257+
258+ return false;
259+ };
260+
261+ // Track user interactions for popup validation
262+ ['click', 'mousedown', 'keydown', 'touchstart'].forEach(eventType => {
263+ document.addEventListener(eventType, trackUserInteraction, true);
264+ });
265+
266+ console.log('[window-open] User interaction tracking enabled');
267+
268+ // Window.open interceptor - only install once
269+ if (!window.__dumber_window_open_intercepted) {
270+ try {
271+
272+ window.open = function(
273+ url?: string | URL | null,
274+ target?: string | null,
275+ features?: string | null
276+ ): WindowProxy | null {
277+ console.log('[window-open] Bridge called with:', url, target, features);
278+ console.log('[window-open] Call stack:', new Error().stack);
279+
280+ const urlString = url ? (typeof url === 'string' ? url : url.toString()) : '';
281+
282+ // Check for duplicate calls to prevent multiple panes
283+ if (isDuplicateWindowOpen(urlString)) {
284+ console.log('[window-open] Blocked: duplicate call within debounce window');
285+ return {
286+ closed: true,
287+ location: { href: urlString },
288+ close: () => {},
289+ focus: () => {},
290+ blur: () => {},
291+ postMessage: () => {}
292+ } as unknown as WindowProxy;
293+ }
294+
295+ // Check for user interaction to prevent popup spam
296+ if (!isUserInteraction()) {
297+ console.log('[window-open] Blocked: no recent user interaction detected');
298+ return {
299+ closed: true,
300+ location: { href: '' },
301+ close: () => {},
302+ focus: () => {},
303+ blur: () => {},
304+ postMessage: () => {}
305+ } as unknown as WindowProxy;
306+ }
307+
308+ const originalFeatures = features || '';
309+ console.log('[window-open] Using original features:', originalFeatures);
310+
311+ const intent: WindowIntent = {
312+ url: urlString,
313+ target: target || '_blank',
314+ features: originalFeatures,
315+ timestamp: Date.now(),
316+ userTriggered: true
317+ };
318+
319+ // Send message to Go to handle popup creation
320+ try {
321+ if (window.webkit?.messageHandlers?.dumber) {
322+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
323+ type: 'handle-window-open',
324+ payload: intent
325+ }));
326+ console.log('[window-open] Sent window.open request to Go - bypassing WebKit completely');
327+ }
328+ } catch (e) {
329+ console.error('[window-open] Failed to send to Go:', e);
330+ }
331+
332+ // Return fake window object - don't call original window.open
333+ console.log('[window-open] Returning fake window object - bypassing WebKit create signal');
334+ return {
335+ closed: false,
336+ location: { href: urlString },
337+ close() { (this as { closed: boolean }).closed = true; },
338+ focus: () => {},
339+ blur: () => {},
340+ postMessage: () => {}
341+ } as unknown as WindowProxy;
342+ };
343+
344+ window.__dumber_window_open_intercepted = true;
345+ console.log('[window-open] ✅ Page-world bridge installed');
346+ } catch (error) {
347+ console.error('[window-open] ❌ Failed to install page-world bridge:', error);
348+ }
349+ }
350+
351+ // Console capture functionality
352+ const setupConsoleCapture = () => {
353+ const originalConsole = {
354+ log: console.log,
355+ warn: console.warn,
356+ error: console.error,
357+ info: console.info,
358+ debug: console.debug
359+ };
360+
361+ const sendConsoleMessage = (level: string, args: unknown[]) => {
362+ try {
363+ const message = args.map(arg =>
364+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
365+ ).join(' ');
366+
367+ const formattedMessage = `${window.location.href} ${message}`;
368+
369+ // Send to Go via existing message handler
370+ if (window.webkit?.messageHandlers?.dumber) {
371+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
372+ type: 'console-message',
373+ payload: { level, message: formattedMessage, url: window.location.href }
374+ }));
375+ }
376+ } catch {
377+ // Silently ignore errors in console capture to avoid infinite loops
378+ }
379+ };
380+
381+ const createConsoleWrapper = (originalMethod: (...args: unknown[]) => void, level: string) => {
382+ return function(...args: unknown[]) {
383+ originalMethod.apply(console, args);
384+ sendConsoleMessage(level, args);
385+ };
386+ };
387+
388+ // Override console methods to capture messages
389+ console.log = createConsoleWrapper(originalConsole.log, 'LOG');
390+ console.warn = createConsoleWrapper(originalConsole.warn, 'WARN');
391+ console.error = createConsoleWrapper(originalConsole.error, 'ERROR');
392+ console.info = createConsoleWrapper(originalConsole.info, 'INFO');
393+ console.debug = createConsoleWrapper(originalConsole.debug, 'DEBUG');
394+ };
395+
396+ // Initialize console capture
397+ setupConsoleCapture();
398+
399+ } catch (e) {
400+ console.warn('[dumber] unified bridge init failed', e);
401+ }
402+})();⏎̸
A gui/src/injected/modules/window-open.ts +29 -0
@@ -0,0 +1,29 @@
1+/**
2+ * Window Open Interceptor Module
3+ *
4+ * OBSOLETE: window.open() is now handled directly via page-world bridge bypass.
5+ * This module is kept for compatibility but no longer performs active interception.
6+ */
7+
8+let isInitialized = false;
9+
10+/**
11+ * Initialize the window.open interceptor (no-op)
12+ * Window.open is now handled directly via JavaScript bypass in page-world
13+ */
14+export function initializeWindowOpenInterceptor(): void {
15+ if (isInitialized) {
16+ return;
17+ }
18+
19+ // No-op - window.open handled directly via page-world bridge
20+ isInitialized = true;
21+ console.log('[window-open] ✅ Interceptor initialized (bypass mode)');
22+}
23+
24+/**
25+ * Get initialization status
26+ */
27+export function isWindowOpenInterceptorInitialized(): boolean {
28+ return isInitialized;
29+}⏎̸
@@ -0,0 +1,29 @@
1+/**
2+ * Window Open Interceptor Module
3+ *
4+ * OBSOLETE: window.open() is now handled directly via page-world bridge bypass.
5+ * This module is kept for compatibility but no longer performs active interception.
6+ */
7+
8+let isInitialized = false;
9+
10+/**
11+ * Initialize the window.open interceptor (no-op)
12+ * Window.open is now handled directly via JavaScript bypass in page-world
13+ */
14+export function initializeWindowOpenInterceptor(): void {
15+ if (isInitialized) {
16+ return;
17+ }
18+
19+ // No-op - window.open handled directly via page-world bridge
20+ isInitialized = true;
21+ console.log('[window-open] ✅ Interceptor initialized (bypass mode)');
22+}
23+
24+/**
25+ * Get initialization status
26+ */
27+export function isWindowOpenInterceptorInitialized(): boolean {
28+ return isInitialized;
29+}⏎̸
A gui/vite.config.main-world.ts +28 -0
@@ -0,0 +1,28 @@
1+import { defineConfig } from 'vite';
2+import { resolve } from 'path';
3+
4+export default defineConfig({
5+ build: {
6+ rollupOptions: {
7+ input: resolve(__dirname, 'src/injected/main-world.ts'),
8+ output: {
9+ dir: '../assets/gui',
10+ format: 'iife',
11+ entryFileNames: 'main-world.min.js',
12+ name: '__dumberMainWorld',
13+ inlineDynamicImports: true,
14+ manualChunks: undefined,
15+ assetFileNames: '[name].[ext]',
16+ },
17+ },
18+ emptyOutDir: false,
19+ target: ['es2020', 'chrome91', 'firefox90'],
20+ minify: true,
21+ sourcemap: false,
22+ cssCodeSplit: false,
23+ assetsInlineLimit: 100000000,
24+ },
25+ define: {
26+ '__DOM_ZOOM_DEFAULT__': '1.0' // Default placeholder, will be replaced by Go
27+ }
28+});⏎̸
@@ -0,0 +1,28 @@
1+import { defineConfig } from 'vite';
2+import { resolve } from 'path';
3+
4+export default defineConfig({
5+ build: {
6+ rollupOptions: {
7+ input: resolve(__dirname, 'src/injected/main-world.ts'),
8+ output: {
9+ dir: '../assets/gui',
10+ format: 'iife',
11+ entryFileNames: 'main-world.min.js',
12+ name: '__dumberMainWorld',
13+ inlineDynamicImports: true,
14+ manualChunks: undefined,
15+ assetFileNames: '[name].[ext]',
16+ },
17+ },
18+ emptyOutDir: false,
19+ target: ['es2020', 'chrome91', 'firefox90'],
20+ minify: true,
21+ sourcemap: false,
22+ cssCodeSplit: false,
23+ assetsInlineLimit: 100000000,
24+ },
25+ define: {
26+ '__DOM_ZOOM_DEFAULT__': '1.0' // Default placeholder, will be replaced by Go
27+ }
28+});⏎̸
M internal/app/browser/shortcuts.go +24 -7
@@ -30,16 +30,33 @@ func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.Cl
3030
3131 func (s *ShortcutHandler) isActivePane() bool {
3232 if s == nil || s.webView == nil {
33- log.Printf("[shortcut] isActivePane: handler or webView is nil")
3433 return false
3534 }
36- if s.app == nil || s.app.activePane == nil {
37- log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true")
38- return true
35+ if s.app == nil || s.app.workspace == nil {
36+ return true // No workspace context; allow
3937 }
40- isActive := s.app.activePane.webView == s.webView
41- log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive)
42- return isActive
38+
39+ // Find node for this WebView
40+ node := s.app.workspace.viewToNode[s.webView]
41+ if node == nil {
42+ return false
43+ }
44+
45+ // For related popups, they're active if parent chain contains the active node
46+ if node.isRelated && node.parentPane != nil {
47+ activeNode := s.app.workspace.active
48+ cur := node
49+ for cur != nil {
50+ if cur == activeNode {
51+ return true
52+ }
53+ cur = cur.parentPane
54+ }
55+ return false
56+ }
57+
58+ // Independent panes: equal to active node
59+ return node == s.app.workspace.active
4360 }
4461
4562 // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards
@@ -30,16 +30,33 @@ func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.Cl
30 30
31 func (s *ShortcutHandler) isActivePane() bool {31 func (s *ShortcutHandler) isActivePane() bool {
32 if s == nil || s.webView == nil {32 if s == nil || s.webView == nil {
33- log.Printf("[shortcut] isActivePane: handler or webView is nil")
34 return false33 return false
35 }34 }
36- if s.app == nil || s.app.activePane == nil {35+ if s.app == nil || s.app.workspace == nil {
37- log.Printf("[shortcut] isActivePane: app or activePane is nil, returning true")36+ return true // No workspace context; allow
38- return true
39 }37 }
40- isActive := s.app.activePane.webView == s.webView38+
41- log.Printf("[shortcut] isActivePane: handler webView=%p activePane webView=%p isActive=%v", s.webView, s.app.activePane.webView, isActive)39+ // Find node for this WebView
42- return isActive40+ node := s.app.workspace.viewToNode[s.webView]
41+ if node == nil {
42+ return false
43+ }
44+
45+ // For related popups, they're active if parent chain contains the active node
46+ if node.isRelated && node.parentPane != nil {
47+ activeNode := s.app.workspace.active
48+ cur := node
49+ for cur != nil {
50+ if cur == activeNode {
51+ return true
52+ }
53+ cur = cur.parentPane
54+ }
55+ return false
56+ }
57+
58+ // Independent panes: equal to active node
59+ return node == s.app.workspace.active
43 }60 }
44 61
45 // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards62 // RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards
M internal/app/browser/workspace_manager.go +468 -6
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "log"
77 "math"
8+ neturl "net/url"
89 "strings"
910 "sync"
1011 "time"
@@ -22,8 +23,14 @@ type paneNode struct {
2223 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves
2324 orientation webkit.Orientation
2425 isLeaf bool
25- isPopup bool // Track if this is a popup pane for OAuth auto-close
26- hoverToken uintptr
26+ isPopup bool // Deprecated: use windowType instead
27+ // Window type tracking
28+ windowType webkit.WindowType // Tab or Popup
29+ windowFeatures *webkit.WindowFeatures // Features if popup
30+ isRelated bool // Shares context
31+ parentPane *paneNode // Parent for related views
32+ autoClose bool // Auto-close on OAuth success
33+ hoverToken uintptr
2734 }
2835
2936 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -241,6 +248,45 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
241248 }
242249 wm.clonePaneState(node, newNode)
243250 wm.splitting = false
251+ case "create-pane":
252+ log.Printf("[workspace] create-pane requested: url=%s action=%s", msg.URL, msg.Action)
253+
254+ if msg.URL == "" {
255+ log.Printf("[workspace] create-pane: empty URL, ignoring")
256+ break
257+ }
258+
259+ // Create a basic window intent from the message
260+ intent := &messaging.WindowIntent{
261+ URL: msg.URL,
262+ WindowType: msg.Action, // "tab" or "popup"
263+ }
264+
265+ // Use the existing methods to handle tab vs popup creation
266+ switch strings.ToLower(msg.Action) {
267+ case "tab":
268+ newView := wm.handleIntentAsTab(node, msg.URL, intent)
269+ if newView != nil {
270+ log.Printf("[workspace] create-pane: tab created successfully")
271+ } else {
272+ log.Printf("[workspace] create-pane: failed to create tab")
273+ }
274+ case "popup":
275+ newView := wm.handleIntentAsPopup(node, msg.URL, intent)
276+ if newView != nil {
277+ log.Printf("[workspace] create-pane: popup created successfully")
278+ } else {
279+ log.Printf("[workspace] create-pane: failed to create popup")
280+ }
281+ default:
282+ log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action)
283+ newView := wm.handleIntentAsTab(node, msg.URL, intent)
284+ if newView != nil {
285+ log.Printf("[workspace] create-pane: default tab created successfully")
286+ } else {
287+ log.Printf("[workspace] create-pane: failed to create default tab")
288+ }
289+ }
244290 default:
245291 log.Printf("[workspace] unhandled workspace event: %s", msg.Event)
246292 }
@@ -1267,13 +1313,14 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
12671313 return nil
12681314 }
12691315
1316+ // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass
1317+
12701318 cfg := wm.app.config
12711319 if cfg == nil {
12721320 log.Printf("[workspace] HandlePopup: nil config - allowing native popup")
12731321 return nil
12741322 }
12751323
1276-
12771324 popCfg := cfg.Workspace.Popups
12781325 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)
12791326
@@ -1282,15 +1329,50 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
12821329 return nil
12831330 }
12841331
1285- // Create a new WebView related to the source for process sharing and proper WindowFeatures
1332+ // Smart detection path: create temporary view and decide placement once type is known
1333+ if popCfg.EnableSmartDetection {
1334+ webkitCfg, err := wm.app.buildWebkitConfig()
1335+ if err != nil {
1336+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1337+ return nil
1338+ }
1339+ webkitCfg.CreateWindow = false
1340+ // Create as related to avoid WindowFeatures crash; we'll decide final placement later
1341+ newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
1342+ if err != nil {
1343+ log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err)
1344+ return nil
1345+ }
1346+
1347+ // Register detection callback
1348+ newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) {
1349+ wm.RunOnUI(func() {
1350+ wm.handleDetectedWindowType(node, newView, url, t, feat)
1351+ })
1352+ })
1353+
1354+ // Fallback: if detection never fires, treat as popup as before
1355+ go func() {
1356+ time.Sleep(1500 * time.Millisecond)
1357+ if newView != nil {
1358+ wm.RunOnUI(func() {
1359+ if wm.viewToNode[newView] == nil { // not yet placed
1360+ wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil)
1361+ }
1362+ })
1363+ }
1364+ }()
1365+
1366+ return newView
1367+ }
1368+
1369+ // Legacy path preserved
12861370 webkitCfg, err := wm.app.buildWebkitConfig()
12871371 if err != nil {
12881372 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
12891373 return nil
12901374 }
1291- // Don't create window initially - we'll embed directly in workspace
12921375 webkitCfg.CreateWindow = false
1293-
12941376 newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
12951377 if err != nil {
12961378 log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err)
@@ -1372,6 +1454,386 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
13721454 return newView
13731455 }
13741456
1457+// handleIntentAsTab creates an independent tab pane based on window.open intent
1458+func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) {
1459+ initial := strings.TrimSpace(strings.ToLower(url))
1460+ closed := false
1461+ view.RegisterURIChangedHandler(func(uri string) {
1462+ if closed {
1463+ return
1464+ }
1465+ if uri == "" {
1466+ return
1467+ }
1468+ u := strings.ToLower(uri)
1469+ if u == "about:blank" || u == initial {
1470+ return
1471+ }
1472+
1473+ should := false
1474+ if strings.Contains(u, "auth/callback") || strings.Contains(u, "callback") || strings.Contains(u, "redirect") {
1475+ should = true
1476+ }
1477+ if !should {
1478+ if parsed, err := neturl.Parse(uri); err == nil {
1479+ q := parsed.Query()
1480+ if q.Has("code") || q.Has("access_token") || q.Has("id_token") {
1481+ should = true
1482+ }
1483+ if !should && parsed.Fragment != "" {
1484+ frag := strings.ToLower(parsed.Fragment)
1485+ if strings.Contains(frag, "access_token=") || strings.Contains(frag, "id_token=") || strings.Contains(frag, "code=") {
1486+ should = true
1487+ }
1488+ }
1489+ }
1490+ }
1491+
1492+ if should {
1493+ log.Printf("[workspace] OAuth URL-based auto-close triggered for: %s", uri)
1494+ closed = true
1495+ time.AfterFunc(200*time.Millisecond, func() {
1496+ webkit.IdleAdd(func() bool {
1497+ if n := wm.viewToNode[view]; n != nil && n.isPopup {
1498+ if err := wm.closePane(n); err != nil {
1499+ log.Printf("[workspace] Failed to close OAuth popup pane: %v", err)
1500+ }
1501+ }
1502+ return false
1503+ })
1504+ })
1505+ }
1506+ })
1507+}
1508+
1509+func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) {
1510+ if intent == nil {
1511+ return
1512+ }
1513+
1514+ features := &webkit.WindowFeatures{}
1515+
1516+ // Apply dimensions if specified
1517+ if intent.Width != nil {
1518+ features.Width = *intent.Width
1519+ }
1520+ if intent.Height != nil {
1521+ features.Height = *intent.Height
1522+ }
1523+
1524+ // Apply visibility features based on window type
1525+ defaultToolbar := !isPopup
1526+ defaultLocation := !isPopup
1527+ defaultMenubar := !isPopup
1528+
1529+ if intent.Toolbar != nil {
1530+ features.ToolbarVisible = *intent.Toolbar
1531+ } else {
1532+ features.ToolbarVisible = defaultToolbar
1533+ }
1534+
1535+ if intent.Location != nil {
1536+ features.LocationbarVisible = *intent.Location
1537+ } else {
1538+ features.LocationbarVisible = defaultLocation
1539+ }
1540+
1541+ if intent.Menubar != nil {
1542+ features.MenubarVisible = *intent.Menubar
1543+ } else {
1544+ features.MenubarVisible = defaultMenubar
1545+ }
1546+
1547+ if intent.Resizable != nil {
1548+ features.Resizable = *intent.Resizable
1549+ } else {
1550+ features.Resizable = true // Usually resizable unless explicitly disabled
1551+ }
1552+
1553+ view.SetWindowFeatures(features)
1554+ windowTypeStr := "tab"
1555+ if isPopup {
1556+ windowTypeStr = "popup"
1557+ }
1558+ log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t",
1559+ windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable)
1560+}
1561+
1562+func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
1563+ log.Printf("[workspace] Handling intent as tab: %s", url)
1564+
1565+ webkitCfg, err := wm.app.buildWebkitConfig()
1566+ if err != nil {
1567+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1568+ return nil
1569+ }
1570+ webkitCfg.CreateWindow = false
1571+
1572+ newView, err := webkit.NewWebView(webkitCfg)
1573+ if err != nil {
1574+ log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err)
1575+ return nil
1576+ }
1577+
1578+ newPane, err := wm.createPane(newView)
1579+ if err != nil {
1580+ log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err)
1581+ return nil
1582+ }
1583+
1584+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1585+ if direction == "" {
1586+ direction = "right"
1587+ }
1588+
1589+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1590+ log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err)
1591+ return nil
1592+ }
1593+
1594+ node := wm.viewToNode[newView]
1595+ if node != nil {
1596+ node.windowType = webkit.WindowTypeTab
1597+ node.isRelated = false
1598+
1599+ // Apply window features from JavaScript intent
1600+ wm.applyWindowFeatures(newView, intent, false)
1601+ }
1602+
1603+ wm.ensureGUIInPane(newPane)
1604+
1605+ if url != "" {
1606+ if err := newView.LoadURL(url); err != nil {
1607+ log.Printf("[workspace] failed to load tab URL: %v", err)
1608+ }
1609+ if err := newView.Show(); err != nil {
1610+ log.Printf("[workspace] failed to show tab WebView: %v", err)
1611+ }
1612+ }
1613+
1614+ log.Printf("[workspace] Created tab pane for URL: %s", url)
1615+ return newView
1616+}
1617+
1618+// handleIntentAsPopup creates a related popup pane based on window.open intent
1619+func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
1620+ log.Printf("[workspace] Handling intent as popup: %s", url)
1621+
1622+ webkitCfg, err := wm.app.buildWebkitConfig()
1623+ if err != nil {
1624+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1625+ return nil
1626+ }
1627+ webkitCfg.CreateWindow = false
1628+
1629+ newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView)
1630+ if err != nil {
1631+ log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err)
1632+ return nil
1633+ }
1634+
1635+ newPane, err := wm.createPane(newView)
1636+ if err != nil {
1637+ log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err)
1638+ return nil
1639+ }
1640+
1641+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1642+ if direction == "" {
1643+ direction = "right"
1644+ }
1645+
1646+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1647+ log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err)
1648+ return nil
1649+ }
1650+
1651+ node := wm.viewToNode[newView]
1652+ if node != nil {
1653+ node.windowType = webkit.WindowTypePopup
1654+ node.isRelated = true
1655+ node.parentPane = sourceNode
1656+ node.isPopup = true
1657+ node.autoClose = wm.shouldAutoClose(url)
1658+
1659+ // Apply window features from JavaScript intent
1660+ wm.applyWindowFeatures(newView, intent, true)
1661+ }
1662+
1663+ // Register close handler for popup auto-close
1664+ newView.RegisterCloseHandler(func() {
1665+ log.Printf("[workspace] Popup requesting close via window.close()")
1666+ if n := wm.viewToNode[newView]; n != nil && n.isPopup {
1667+ time.AfterFunc(200*time.Millisecond, func() {
1668+ webkit.IdleAdd(func() bool {
1669+ if err := wm.closePane(n); err != nil {
1670+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1671+ }
1672+ return false
1673+ })
1674+ })
1675+ }
1676+ })
1677+
1678+ // URL-based auto-close for OAuth popups
1679+ if node != nil && node.isPopup && node.autoClose {
1680+ wm.registerOAuthAutoClose(newView, url)
1681+ }
1682+
1683+ wm.ensureGUIInPane(newPane)
1684+
1685+ if url != "" {
1686+ if err := newView.LoadURL(url); err != nil {
1687+ log.Printf("[workspace] failed to load popup URL: %v", err)
1688+ }
1689+ if err := newView.Show(); err != nil {
1690+ log.Printf("[workspace] failed to show popup WebView: %v", err)
1691+ }
1692+ }
1693+
1694+ log.Printf("[workspace] Created popup pane for URL: %s", url)
1695+ return newView
1696+}
1697+
1698+// insertIndependentPane inserts a new independent pane next to the source
1699+func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error {
1700+ newPane, err := wm.createPane(webView)
1701+ if err != nil {
1702+ return err
1703+ }
1704+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1705+ if direction == "" {
1706+ direction = "right"
1707+ }
1708+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { // reuse insertion primitive
1709+ return err
1710+ }
1711+ node := wm.viewToNode[webView]
1712+ if node != nil {
1713+ node.windowType = webkit.WindowTypeTab
1714+ node.isRelated = false
1715+ }
1716+ if url != "" {
1717+ _ = webView.LoadURL(url)
1718+ }
1719+ return nil
1720+}
1721+
1722+// configureRelatedPopup creates a related view and inserts it
1723+func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) {
1724+ // Use the WebView that was already created and returned to WebKit
1725+ related := webView
1726+ newPane, err := wm.createPane(related)
1727+ if err != nil {
1728+ log.Printf("[workspace] failed to create related popup pane: %v", err)
1729+ return
1730+ }
1731+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1732+ if direction == "" {
1733+ direction = "right"
1734+ }
1735+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1736+ log.Printf("[workspace] failed to insert related popup pane: %v", err)
1737+ return
1738+ }
1739+ node := wm.viewToNode[related]
1740+ if node != nil {
1741+ node.windowType = webkit.WindowTypePopup
1742+ node.windowFeatures = feat
1743+ node.isRelated = true
1744+ node.parentPane = sourceNode
1745+ node.isPopup = true
1746+ // Heuristic + config for auto-close intent
1747+ node.autoClose = wm.shouldAutoClose(url)
1748+ }
1749+ // Pipe into existing auto-close flow only for popups (confirmed by detection)
1750+ related.RegisterCloseHandler(func() {
1751+ log.Printf("[workspace] Popup requesting close via window.close()")
1752+ if n := wm.viewToNode[related]; n != nil && n.isPopup {
1753+ time.AfterFunc(200*time.Millisecond, func() {
1754+ webkit.IdleAdd(func() bool {
1755+ if err := wm.closePane(n); err != nil {
1756+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1757+ }
1758+ return false
1759+ })
1760+ })
1761+ }
1762+ })
1763+
1764+ // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs
1765+ if node != nil && node.isPopup && node.autoClose {
1766+ wm.registerOAuthAutoClose(related, url)
1767+ }
1768+ if url != "" {
1769+ _ = related.LoadURL(url)
1770+ }
1771+}
1772+
1773+// shouldAutoClose checks simple OAuth-like URL patterns and config flag
1774+func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
1775+ if wm == nil || wm.app == nil || wm.app.config == nil {
1776+ return true
1777+ }
1778+ if !wm.app.config.Workspace.Popups.OAuthAutoClose {
1779+ return false
1780+ }
1781+ u := strings.ToLower(url)
1782+ patterns := []string{"oauth", "authorize", "login/oauth", "auth/callback", "connect/authorize"}
1783+ for _, p := range patterns {
1784+ if strings.Contains(u, p) {
1785+ return true
1786+ }
1787+ }
1788+ return false
1789+}
1790+
1791+// RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded
1792+func (wm *WorkspaceManager) RunOnUI(fn func()) {
1793+ if fn != nil {
1794+ fn()
1795+ }
1796+}
1797+
1798+// handleDetectedWindowType handles window type detection from smart detection path
1799+func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) {
1800+ if wm.viewToNode[webView] != nil {
1801+ return // Already placed
1802+ }
1803+
1804+ log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url)
1805+
1806+ switch windowType {
1807+ case webkit.WindowTypeTab:
1808+ // For tabs, create a NEW independent WebView (can't use the related one)
1809+ webkitCfg, err := wm.app.buildWebkitConfig()
1810+ if err != nil {
1811+ log.Printf("[workspace] failed to build webkit config for tab: %v", err)
1812+ return
1813+ }
1814+ webkitCfg.CreateWindow = false
1815+
1816+ // Create independent WebView like handleIntentAsTab does
1817+ independentView, err := webkit.NewWebView(webkitCfg)
1818+ if err != nil {
1819+ log.Printf("[workspace] failed to create independent tab WebView: %v", err)
1820+ return
1821+ }
1822+
1823+ // The related webView was just for detection - we don't use it for tabs
1824+ // Insert the new independent view as a tab
1825+ if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil {
1826+ log.Printf("[workspace] Failed to insert independent pane: %v", err)
1827+ }
1828+
1829+ case webkit.WindowTypePopup:
1830+ // For popups, use the related WebView we already created
1831+ wm.configureRelatedPopup(sourceNode, webView, url, features)
1832+ default:
1833+ // Fallback to popup behavior
1834+ wm.configureRelatedPopup(sourceNode, webView, url, features)
1835+ }
1836+}
13751837
13761838 // insertPopupPane inserts a pre-created popup pane into the workspace
13771839 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "log"6 "log"
7 "math"7 "math"
8+ neturl "net/url"
8 "strings"9 "strings"
9 "sync"10 "sync"
10 "time"11 "time"
@@ -22,8 +23,14 @@ type paneNode struct {
22 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves23 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves
23 orientation webkit.Orientation24 orientation webkit.Orientation
24 isLeaf bool25 isLeaf bool
25- isPopup bool // Track if this is a popup pane for OAuth auto-close26+ isPopup bool // Deprecated: use windowType instead
26- hoverToken uintptr27+ // Window type tracking
28+ windowType webkit.WindowType // Tab or Popup
29+ windowFeatures *webkit.WindowFeatures // Features if popup
30+ isRelated bool // Shares context
31+ parentPane *paneNode // Parent for related views
32+ autoClose bool // Auto-close on OAuth success
33+ hoverToken uintptr
27 }34 }
28 35
29 // WorkspaceManager coordinates Zellij-style pane operations.36 // WorkspaceManager coordinates Zellij-style pane operations.
@@ -241,6 +248,45 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
241 }248 }
242 wm.clonePaneState(node, newNode)249 wm.clonePaneState(node, newNode)
243 wm.splitting = false250 wm.splitting = false
251+ case "create-pane":
252+ log.Printf("[workspace] create-pane requested: url=%s action=%s", msg.URL, msg.Action)
253+
254+ if msg.URL == "" {
255+ log.Printf("[workspace] create-pane: empty URL, ignoring")
256+ break
257+ }
258+
259+ // Create a basic window intent from the message
260+ intent := &messaging.WindowIntent{
261+ URL: msg.URL,
262+ WindowType: msg.Action, // "tab" or "popup"
263+ }
264+
265+ // Use the existing methods to handle tab vs popup creation
266+ switch strings.ToLower(msg.Action) {
267+ case "tab":
268+ newView := wm.handleIntentAsTab(node, msg.URL, intent)
269+ if newView != nil {
270+ log.Printf("[workspace] create-pane: tab created successfully")
271+ } else {
272+ log.Printf("[workspace] create-pane: failed to create tab")
273+ }
274+ case "popup":
275+ newView := wm.handleIntentAsPopup(node, msg.URL, intent)
276+ if newView != nil {
277+ log.Printf("[workspace] create-pane: popup created successfully")
278+ } else {
279+ log.Printf("[workspace] create-pane: failed to create popup")
280+ }
281+ default:
282+ log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action)
283+ newView := wm.handleIntentAsTab(node, msg.URL, intent)
284+ if newView != nil {
285+ log.Printf("[workspace] create-pane: default tab created successfully")
286+ } else {
287+ log.Printf("[workspace] create-pane: failed to create default tab")
288+ }
289+ }
244 default:290 default:
245 log.Printf("[workspace] unhandled workspace event: %s", msg.Event)291 log.Printf("[workspace] unhandled workspace event: %s", msg.Event)
246 }292 }
@@ -1267,13 +1313,14 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1267 return nil1313 return nil
1268 }1314 }
1269 1315
1316+ // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass
1317+
1270 cfg := wm.app.config1318 cfg := wm.app.config
1271 if cfg == nil {1319 if cfg == nil {
1272 log.Printf("[workspace] HandlePopup: nil config - allowing native popup")1320 log.Printf("[workspace] HandlePopup: nil config - allowing native popup")
1273 return nil1321 return nil
1274 }1322 }
1275 1323
1276-
1277 popCfg := cfg.Workspace.Popups1324 popCfg := cfg.Workspace.Popups
1278 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)1325 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)
1279 1326
@@ -1282,15 +1329,50 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1282 return nil1329 return nil
1283 }1330 }
1284 1331
1285- // Create a new WebView related to the source for process sharing and proper WindowFeatures1332+ // Smart detection path: create temporary view and decide placement once type is known
1333+ if popCfg.EnableSmartDetection {
1334+ webkitCfg, err := wm.app.buildWebkitConfig()
1335+ if err != nil {
1336+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1337+ return nil
1338+ }
1339+ webkitCfg.CreateWindow = false
1340+ // Create as related to avoid WindowFeatures crash; we'll decide final placement later
1341+ newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
1342+ if err != nil {
1343+ log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err)
1344+ return nil
1345+ }
1346+
1347+ // Register detection callback
1348+ newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) {
1349+ wm.RunOnUI(func() {
1350+ wm.handleDetectedWindowType(node, newView, url, t, feat)
1351+ })
1352+ })
1353+
1354+ // Fallback: if detection never fires, treat as popup as before
1355+ go func() {
1356+ time.Sleep(1500 * time.Millisecond)
1357+ if newView != nil {
1358+ wm.RunOnUI(func() {
1359+ if wm.viewToNode[newView] == nil { // not yet placed
1360+ wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil)
1361+ }
1362+ })
1363+ }
1364+ }()
1365+
1366+ return newView
1367+ }
1368+
1369+ // Legacy path preserved
1286 webkitCfg, err := wm.app.buildWebkitConfig()1370 webkitCfg, err := wm.app.buildWebkitConfig()
1287 if err != nil {1371 if err != nil {
1288 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)1372 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1289 return nil1373 return nil
1290 }1374 }
1291- // Don't create window initially - we'll embed directly in workspace
1292 webkitCfg.CreateWindow = false1375 webkitCfg.CreateWindow = false
1293-
1294 newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)1376 newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
1295 if err != nil {1377 if err != nil {
1296 log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err)1378 log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err)
@@ -1372,6 +1454,386 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1372 return newView1454 return newView
1373 }1455 }
1374 1456
1457+// handleIntentAsTab creates an independent tab pane based on window.open intent
1458+func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) {
1459+ initial := strings.TrimSpace(strings.ToLower(url))
1460+ closed := false
1461+ view.RegisterURIChangedHandler(func(uri string) {
1462+ if closed {
1463+ return
1464+ }
1465+ if uri == "" {
1466+ return
1467+ }
1468+ u := strings.ToLower(uri)
1469+ if u == "about:blank" || u == initial {
1470+ return
1471+ }
1472+
1473+ should := false
1474+ if strings.Contains(u, "auth/callback") || strings.Contains(u, "callback") || strings.Contains(u, "redirect") {
1475+ should = true
1476+ }
1477+ if !should {
1478+ if parsed, err := neturl.Parse(uri); err == nil {
1479+ q := parsed.Query()
1480+ if q.Has("code") || q.Has("access_token") || q.Has("id_token") {
1481+ should = true
1482+ }
1483+ if !should && parsed.Fragment != "" {
1484+ frag := strings.ToLower(parsed.Fragment)
1485+ if strings.Contains(frag, "access_token=") || strings.Contains(frag, "id_token=") || strings.Contains(frag, "code=") {
1486+ should = true
1487+ }
1488+ }
1489+ }
1490+ }
1491+
1492+ if should {
1493+ log.Printf("[workspace] OAuth URL-based auto-close triggered for: %s", uri)
1494+ closed = true
1495+ time.AfterFunc(200*time.Millisecond, func() {
1496+ webkit.IdleAdd(func() bool {
1497+ if n := wm.viewToNode[view]; n != nil && n.isPopup {
1498+ if err := wm.closePane(n); err != nil {
1499+ log.Printf("[workspace] Failed to close OAuth popup pane: %v", err)
1500+ }
1501+ }
1502+ return false
1503+ })
1504+ })
1505+ }
1506+ })
1507+}
1508+
1509+func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) {
1510+ if intent == nil {
1511+ return
1512+ }
1513+
1514+ features := &webkit.WindowFeatures{}
1515+
1516+ // Apply dimensions if specified
1517+ if intent.Width != nil {
1518+ features.Width = *intent.Width
1519+ }
1520+ if intent.Height != nil {
1521+ features.Height = *intent.Height
1522+ }
1523+
1524+ // Apply visibility features based on window type
1525+ defaultToolbar := !isPopup
1526+ defaultLocation := !isPopup
1527+ defaultMenubar := !isPopup
1528+
1529+ if intent.Toolbar != nil {
1530+ features.ToolbarVisible = *intent.Toolbar
1531+ } else {
1532+ features.ToolbarVisible = defaultToolbar
1533+ }
1534+
1535+ if intent.Location != nil {
1536+ features.LocationbarVisible = *intent.Location
1537+ } else {
1538+ features.LocationbarVisible = defaultLocation
1539+ }
1540+
1541+ if intent.Menubar != nil {
1542+ features.MenubarVisible = *intent.Menubar
1543+ } else {
1544+ features.MenubarVisible = defaultMenubar
1545+ }
1546+
1547+ if intent.Resizable != nil {
1548+ features.Resizable = *intent.Resizable
1549+ } else {
1550+ features.Resizable = true // Usually resizable unless explicitly disabled
1551+ }
1552+
1553+ view.SetWindowFeatures(features)
1554+ windowTypeStr := "tab"
1555+ if isPopup {
1556+ windowTypeStr = "popup"
1557+ }
1558+ log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t",
1559+ windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable)
1560+}
1561+
1562+func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
1563+ log.Printf("[workspace] Handling intent as tab: %s", url)
1564+
1565+ webkitCfg, err := wm.app.buildWebkitConfig()
1566+ if err != nil {
1567+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1568+ return nil
1569+ }
1570+ webkitCfg.CreateWindow = false
1571+
1572+ newView, err := webkit.NewWebView(webkitCfg)
1573+ if err != nil {
1574+ log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err)
1575+ return nil
1576+ }
1577+
1578+ newPane, err := wm.createPane(newView)
1579+ if err != nil {
1580+ log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err)
1581+ return nil
1582+ }
1583+
1584+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1585+ if direction == "" {
1586+ direction = "right"
1587+ }
1588+
1589+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1590+ log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err)
1591+ return nil
1592+ }
1593+
1594+ node := wm.viewToNode[newView]
1595+ if node != nil {
1596+ node.windowType = webkit.WindowTypeTab
1597+ node.isRelated = false
1598+
1599+ // Apply window features from JavaScript intent
1600+ wm.applyWindowFeatures(newView, intent, false)
1601+ }
1602+
1603+ wm.ensureGUIInPane(newPane)
1604+
1605+ if url != "" {
1606+ if err := newView.LoadURL(url); err != nil {
1607+ log.Printf("[workspace] failed to load tab URL: %v", err)
1608+ }
1609+ if err := newView.Show(); err != nil {
1610+ log.Printf("[workspace] failed to show tab WebView: %v", err)
1611+ }
1612+ }
1613+
1614+ log.Printf("[workspace] Created tab pane for URL: %s", url)
1615+ return newView
1616+}
1617+
1618+// handleIntentAsPopup creates a related popup pane based on window.open intent
1619+func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
1620+ log.Printf("[workspace] Handling intent as popup: %s", url)
1621+
1622+ webkitCfg, err := wm.app.buildWebkitConfig()
1623+ if err != nil {
1624+ log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
1625+ return nil
1626+ }
1627+ webkitCfg.CreateWindow = false
1628+
1629+ newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView)
1630+ if err != nil {
1631+ log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err)
1632+ return nil
1633+ }
1634+
1635+ newPane, err := wm.createPane(newView)
1636+ if err != nil {
1637+ log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err)
1638+ return nil
1639+ }
1640+
1641+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1642+ if direction == "" {
1643+ direction = "right"
1644+ }
1645+
1646+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1647+ log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err)
1648+ return nil
1649+ }
1650+
1651+ node := wm.viewToNode[newView]
1652+ if node != nil {
1653+ node.windowType = webkit.WindowTypePopup
1654+ node.isRelated = true
1655+ node.parentPane = sourceNode
1656+ node.isPopup = true
1657+ node.autoClose = wm.shouldAutoClose(url)
1658+
1659+ // Apply window features from JavaScript intent
1660+ wm.applyWindowFeatures(newView, intent, true)
1661+ }
1662+
1663+ // Register close handler for popup auto-close
1664+ newView.RegisterCloseHandler(func() {
1665+ log.Printf("[workspace] Popup requesting close via window.close()")
1666+ if n := wm.viewToNode[newView]; n != nil && n.isPopup {
1667+ time.AfterFunc(200*time.Millisecond, func() {
1668+ webkit.IdleAdd(func() bool {
1669+ if err := wm.closePane(n); err != nil {
1670+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1671+ }
1672+ return false
1673+ })
1674+ })
1675+ }
1676+ })
1677+
1678+ // URL-based auto-close for OAuth popups
1679+ if node != nil && node.isPopup && node.autoClose {
1680+ wm.registerOAuthAutoClose(newView, url)
1681+ }
1682+
1683+ wm.ensureGUIInPane(newPane)
1684+
1685+ if url != "" {
1686+ if err := newView.LoadURL(url); err != nil {
1687+ log.Printf("[workspace] failed to load popup URL: %v", err)
1688+ }
1689+ if err := newView.Show(); err != nil {
1690+ log.Printf("[workspace] failed to show popup WebView: %v", err)
1691+ }
1692+ }
1693+
1694+ log.Printf("[workspace] Created popup pane for URL: %s", url)
1695+ return newView
1696+}
1697+
1698+// insertIndependentPane inserts a new independent pane next to the source
1699+func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error {
1700+ newPane, err := wm.createPane(webView)
1701+ if err != nil {
1702+ return err
1703+ }
1704+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1705+ if direction == "" {
1706+ direction = "right"
1707+ }
1708+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { // reuse insertion primitive
1709+ return err
1710+ }
1711+ node := wm.viewToNode[webView]
1712+ if node != nil {
1713+ node.windowType = webkit.WindowTypeTab
1714+ node.isRelated = false
1715+ }
1716+ if url != "" {
1717+ _ = webView.LoadURL(url)
1718+ }
1719+ return nil
1720+}
1721+
1722+// configureRelatedPopup creates a related view and inserts it
1723+func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) {
1724+ // Use the WebView that was already created and returned to WebKit
1725+ related := webView
1726+ newPane, err := wm.createPane(related)
1727+ if err != nil {
1728+ log.Printf("[workspace] failed to create related popup pane: %v", err)
1729+ return
1730+ }
1731+ direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
1732+ if direction == "" {
1733+ direction = "right"
1734+ }
1735+ if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
1736+ log.Printf("[workspace] failed to insert related popup pane: %v", err)
1737+ return
1738+ }
1739+ node := wm.viewToNode[related]
1740+ if node != nil {
1741+ node.windowType = webkit.WindowTypePopup
1742+ node.windowFeatures = feat
1743+ node.isRelated = true
1744+ node.parentPane = sourceNode
1745+ node.isPopup = true
1746+ // Heuristic + config for auto-close intent
1747+ node.autoClose = wm.shouldAutoClose(url)
1748+ }
1749+ // Pipe into existing auto-close flow only for popups (confirmed by detection)
1750+ related.RegisterCloseHandler(func() {
1751+ log.Printf("[workspace] Popup requesting close via window.close()")
1752+ if n := wm.viewToNode[related]; n != nil && n.isPopup {
1753+ time.AfterFunc(200*time.Millisecond, func() {
1754+ webkit.IdleAdd(func() bool {
1755+ if err := wm.closePane(n); err != nil {
1756+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1757+ }
1758+ return false
1759+ })
1760+ })
1761+ }
1762+ })
1763+
1764+ // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs
1765+ if node != nil && node.isPopup && node.autoClose {
1766+ wm.registerOAuthAutoClose(related, url)
1767+ }
1768+ if url != "" {
1769+ _ = related.LoadURL(url)
1770+ }
1771+}
1772+
1773+// shouldAutoClose checks simple OAuth-like URL patterns and config flag
1774+func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
1775+ if wm == nil || wm.app == nil || wm.app.config == nil {
1776+ return true
1777+ }
1778+ if !wm.app.config.Workspace.Popups.OAuthAutoClose {
1779+ return false
1780+ }
1781+ u := strings.ToLower(url)
1782+ patterns := []string{"oauth", "authorize", "login/oauth", "auth/callback", "connect/authorize"}
1783+ for _, p := range patterns {
1784+ if strings.Contains(u, p) {
1785+ return true
1786+ }
1787+ }
1788+ return false
1789+}
1790+
1791+// RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded
1792+func (wm *WorkspaceManager) RunOnUI(fn func()) {
1793+ if fn != nil {
1794+ fn()
1795+ }
1796+}
1797+
1798+// handleDetectedWindowType handles window type detection from smart detection path
1799+func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) {
1800+ if wm.viewToNode[webView] != nil {
1801+ return // Already placed
1802+ }
1803+
1804+ log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url)
1805+
1806+ switch windowType {
1807+ case webkit.WindowTypeTab:
1808+ // For tabs, create a NEW independent WebView (can't use the related one)
1809+ webkitCfg, err := wm.app.buildWebkitConfig()
1810+ if err != nil {
1811+ log.Printf("[workspace] failed to build webkit config for tab: %v", err)
1812+ return
1813+ }
1814+ webkitCfg.CreateWindow = false
1815+
1816+ // Create independent WebView like handleIntentAsTab does
1817+ independentView, err := webkit.NewWebView(webkitCfg)
1818+ if err != nil {
1819+ log.Printf("[workspace] failed to create independent tab WebView: %v", err)
1820+ return
1821+ }
1822+
1823+ // The related webView was just for detection - we don't use it for tabs
1824+ // Insert the new independent view as a tab
1825+ if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil {
1826+ log.Printf("[workspace] Failed to insert independent pane: %v", err)
1827+ }
1828+
1829+ case webkit.WindowTypePopup:
1830+ // For popups, use the related WebView we already created
1831+ wm.configureRelatedPopup(sourceNode, webView, url, features)
1832+ default:
1833+ // Fallback to popup behavior
1834+ wm.configureRelatedPopup(sourceNode, webView, url, features)
1835+ }
1836+}
1375 1837
1376 // insertPopupPane inserts a pre-created popup pane into the workspace1838 // insertPopupPane inserts a pre-created popup pane into the workspace
1377 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {1839 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {
M internal/app/messaging/handler.go +75 -1
@@ -11,6 +11,8 @@ import (
1111
1212 "github.com/bnema/dumber/internal/app/constants"
1313 "github.com/bnema/dumber/internal/app/control"
14+ "github.com/bnema/dumber/internal/config"
15+ "github.com/bnema/dumber/internal/logging"
1416 "github.com/bnema/dumber/internal/services"
1517 "github.com/bnema/dumber/pkg/webkit"
1618 )
@@ -50,6 +52,24 @@ type Message struct {
5052 Payload json.RawMessage `json:"payload"`
5153 }
5254
55+// WindowIntent represents an intercepted window.open call from JavaScript
56+type WindowIntent struct {
57+ URL string `json:"url"`
58+ Target string `json:"target"`
59+ Features string `json:"features"`
60+ Timestamp int64 `json:"timestamp"`
61+ WindowType string `json:"windowType"` // "tab", "popup", "unknown"
62+ IsPopup bool `json:"isPopup"`
63+ IsTab bool `json:"isTab"`
64+ // Parsed features
65+ Width *int `json:"width,omitempty"`
66+ Height *int `json:"height,omitempty"`
67+ Toolbar *bool `json:"toolbar,omitempty"`
68+ Location *bool `json:"location,omitempty"`
69+ Menubar *bool `json:"menubar,omitempty"`
70+ Resizable *bool `json:"resizable,omitempty"`
71+}
72+
5373 // NewHandler creates a new message handler
5474 func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
5575 return &Handler{
@@ -98,6 +118,10 @@ func (h *Handler) Handle(payload string) {
98118 h.handleHistoryDelete(msg)
99119 case "workspace":
100120 h.handleWorkspace(msg)
121+ case "handle-window-open":
122+ h.handleWindowOpen(msg)
123+ case "console-message":
124+ h.handleConsoleMessage(msg)
101125 }
102126 }
103127
@@ -422,7 +446,7 @@ func (h *Handler) handleHistoryRecent(msg Message) {
422446 }
423447
424448 // handleHistoryStats processes history stats requests
425-func (h *Handler) handleHistoryStats(msg Message) {
449+func (h *Handler) handleHistoryStats(_ Message) {
426450 if h.browserService == nil || h.webView == nil {
427451 return
428452 }
@@ -496,3 +520,53 @@ func (h *Handler) handleHistoryDelete(msg Message) {
496520 b, _ := json.Marshal(successData)
497521 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")
498522 }
523+
524+// handleWindowOpen processes handle-window-open messages to create panes directly
525+func (h *Handler) handleWindowOpen(msg Message) {
526+ var intent WindowIntent
527+ if err := json.Unmarshal(msg.Payload, &intent); err != nil {
528+ log.Printf("[messaging] Failed to unmarshal handle-window-open: %v", err)
529+ return
530+ }
531+
532+ log.Printf("[messaging] Handling direct window.open request: url=%s type=%s", intent.URL, intent.WindowType)
533+
534+ // Forward to workspace observer if available
535+ if h.workspaceObserver != nil && h.webView != nil {
536+ // Create a workspace message to trigger pane creation
537+ workspaceMsg := Message{
538+ Type: "workspace",
539+ Event: "create-pane",
540+ URL: intent.URL,
541+ Action: intent.WindowType, // "tab" or "popup"
542+ }
543+ log.Printf("[messaging] Forwarding to workspace: %+v", workspaceMsg)
544+ h.workspaceObserver.OnWorkspaceMessage(h.webView, workspaceMsg)
545+ } else {
546+ log.Printf("[messaging] No workspace observer available for direct window.open")
547+ }
548+}
549+
550+// handleConsoleMessage processes console-message from JavaScript
551+func (h *Handler) handleConsoleMessage(msg Message) {
552+ // Check if console capture is enabled
553+ cfg := config.Get()
554+ if !cfg.Logging.CaptureConsole {
555+ return
556+ }
557+
558+ // Parse the console message payload
559+ var consolePayload struct {
560+ Level string `json:"level"`
561+ Message string `json:"message"`
562+ URL string `json:"url"`
563+ }
564+
565+ if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil {
566+ log.Printf("[messaging] Failed to unmarshal console-message: %v", err)
567+ return
568+ }
569+
570+ // Send to logging system with [CONSOLE] tag
571+ logging.CaptureWebKitLog(consolePayload.Message)
572+}
@@ -11,6 +11,8 @@ import (
11 11
12 "github.com/bnema/dumber/internal/app/constants"12 "github.com/bnema/dumber/internal/app/constants"
13 "github.com/bnema/dumber/internal/app/control"13 "github.com/bnema/dumber/internal/app/control"
14+ "github.com/bnema/dumber/internal/config"
15+ "github.com/bnema/dumber/internal/logging"
14 "github.com/bnema/dumber/internal/services"16 "github.com/bnema/dumber/internal/services"
15 "github.com/bnema/dumber/pkg/webkit"17 "github.com/bnema/dumber/pkg/webkit"
16 )18 )
@@ -50,6 +52,24 @@ type Message struct {
50 Payload json.RawMessage `json:"payload"`52 Payload json.RawMessage `json:"payload"`
51 }53 }
52 54
55+// WindowIntent represents an intercepted window.open call from JavaScript
56+type WindowIntent struct {
57+ URL string `json:"url"`
58+ Target string `json:"target"`
59+ Features string `json:"features"`
60+ Timestamp int64 `json:"timestamp"`
61+ WindowType string `json:"windowType"` // "tab", "popup", "unknown"
62+ IsPopup bool `json:"isPopup"`
63+ IsTab bool `json:"isTab"`
64+ // Parsed features
65+ Width *int `json:"width,omitempty"`
66+ Height *int `json:"height,omitempty"`
67+ Toolbar *bool `json:"toolbar,omitempty"`
68+ Location *bool `json:"location,omitempty"`
69+ Menubar *bool `json:"menubar,omitempty"`
70+ Resizable *bool `json:"resizable,omitempty"`
71+}
72+
53 // NewHandler creates a new message handler73 // NewHandler creates a new message handler
54 func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {74 func NewHandler(parserService *services.ParserService, browserService *services.BrowserService) *Handler {
55 return &Handler{75 return &Handler{
@@ -98,6 +118,10 @@ func (h *Handler) Handle(payload string) {
98 h.handleHistoryDelete(msg)118 h.handleHistoryDelete(msg)
99 case "workspace":119 case "workspace":
100 h.handleWorkspace(msg)120 h.handleWorkspace(msg)
121+ case "handle-window-open":
122+ h.handleWindowOpen(msg)
123+ case "console-message":
124+ h.handleConsoleMessage(msg)
101 }125 }
102 }126 }
103 127
@@ -422,7 +446,7 @@ func (h *Handler) handleHistoryRecent(msg Message) {
422 }446 }
423 447
424 // handleHistoryStats processes history stats requests448 // handleHistoryStats processes history stats requests
425-func (h *Handler) handleHistoryStats(msg Message) {449+func (h *Handler) handleHistoryStats(_ Message) {
426 if h.browserService == nil || h.webView == nil {450 if h.browserService == nil || h.webView == nil {
427 return451 return
428 }452 }
@@ -496,3 +520,53 @@ func (h *Handler) handleHistoryDelete(msg Message) {
496 b, _ := json.Marshal(successData)520 b, _ := json.Marshal(successData)
497 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")521 _ = h.webView.InjectScript("window.__dumber_history_deleted && window.__dumber_history_deleted(" + string(b) + ")")
498 }522 }
523+
524+// handleWindowOpen processes handle-window-open messages to create panes directly
525+func (h *Handler) handleWindowOpen(msg Message) {
526+ var intent WindowIntent
527+ if err := json.Unmarshal(msg.Payload, &intent); err != nil {
528+ log.Printf("[messaging] Failed to unmarshal handle-window-open: %v", err)
529+ return
530+ }
531+
532+ log.Printf("[messaging] Handling direct window.open request: url=%s type=%s", intent.URL, intent.WindowType)
533+
534+ // Forward to workspace observer if available
535+ if h.workspaceObserver != nil && h.webView != nil {
536+ // Create a workspace message to trigger pane creation
537+ workspaceMsg := Message{
538+ Type: "workspace",
539+ Event: "create-pane",
540+ URL: intent.URL,
541+ Action: intent.WindowType, // "tab" or "popup"
542+ }
543+ log.Printf("[messaging] Forwarding to workspace: %+v", workspaceMsg)
544+ h.workspaceObserver.OnWorkspaceMessage(h.webView, workspaceMsg)
545+ } else {
546+ log.Printf("[messaging] No workspace observer available for direct window.open")
547+ }
548+}
549+
550+// handleConsoleMessage processes console-message from JavaScript
551+func (h *Handler) handleConsoleMessage(msg Message) {
552+ // Check if console capture is enabled
553+ cfg := config.Get()
554+ if !cfg.Logging.CaptureConsole {
555+ return
556+ }
557+
558+ // Parse the console message payload
559+ var consolePayload struct {
560+ Level string `json:"level"`
561+ Message string `json:"message"`
562+ URL string `json:"url"`
563+ }
564+
565+ if err := json.Unmarshal(msg.Payload, &consolePayload); err != nil {
566+ log.Printf("[messaging] Failed to unmarshal console-message: %v", err)
567+ return
568+ }
569+
570+ // Send to logging system with [CONSOLE] tag
571+ logging.CaptureWebKitLog(consolePayload.Message)
572+}
M internal/cache/favicon.go +18 -4
@@ -135,7 +135,11 @@ func (fc *FaviconCache) downloadFavicon(faviconURL string) error {
135135 log.Printf("[favicon] failed to close download file: %v", err)
136136 }
137137 }()
138- defer os.Remove(tempDownloadFile)
138+ defer func() {
139+ if err := os.Remove(tempDownloadFile); err != nil {
140+ log.Printf("[favicon] failed to remove temp download file: %v", err)
141+ }
142+ }()
139143
140144 // Save raw favicon data
141145 if _, err := io.Copy(downloadFile, limitedBody); err != nil {
@@ -177,7 +181,11 @@ func (fc *FaviconCache) tryStandardImageDecode(filePath string) (image.Image, er
177181 if err != nil {
178182 return nil, err
179183 }
180- defer file.Close()
184+ defer func() {
185+ if err := file.Close(); err != nil {
186+ log.Printf("[favicon] failed to close file: %v", err)
187+ }
188+ }()
181189
182190 img, _, err := image.Decode(file)
183191 return img, err
@@ -190,10 +198,16 @@ func (fc *FaviconCache) savePNG(img image.Image, outputPath string) error {
190198 if err != nil {
191199 return fmt.Errorf("failed to create temp file: %w", err)
192200 }
193- defer file.Close()
201+ defer func() {
202+ if err := file.Close(); err != nil {
203+ log.Printf("[favicon] failed to close file: %v", err)
204+ }
205+ }()
194206
195207 if err := png.Encode(file, img); err != nil {
196- os.Remove(tempFile)
208+ if removeErr := os.Remove(tempFile); removeErr != nil {
209+ log.Printf("[favicon] failed to remove temp file: %v", removeErr)
210+ }
197211 return fmt.Errorf("failed to encode PNG: %w", err)
198212 }
199213
@@ -135,7 +135,11 @@ func (fc *FaviconCache) downloadFavicon(faviconURL string) error {
135 log.Printf("[favicon] failed to close download file: %v", err)135 log.Printf("[favicon] failed to close download file: %v", err)
136 }136 }
137 }()137 }()
138- defer os.Remove(tempDownloadFile)138+ defer func() {
139+ if err := os.Remove(tempDownloadFile); err != nil {
140+ log.Printf("[favicon] failed to remove temp download file: %v", err)
141+ }
142+ }()
139 143
140 // Save raw favicon data144 // Save raw favicon data
141 if _, err := io.Copy(downloadFile, limitedBody); err != nil {145 if _, err := io.Copy(downloadFile, limitedBody); err != nil {
@@ -177,7 +181,11 @@ func (fc *FaviconCache) tryStandardImageDecode(filePath string) (image.Image, er
177 if err != nil {181 if err != nil {
178 return nil, err182 return nil, err
179 }183 }
180- defer file.Close()184+ defer func() {
185+ if err := file.Close(); err != nil {
186+ log.Printf("[favicon] failed to close file: %v", err)
187+ }
188+ }()
181 189
182 img, _, err := image.Decode(file)190 img, _, err := image.Decode(file)
183 return img, err191 return img, err
@@ -190,10 +198,16 @@ func (fc *FaviconCache) savePNG(img image.Image, outputPath string) error {
190 if err != nil {198 if err != nil {
191 return fmt.Errorf("failed to create temp file: %w", err)199 return fmt.Errorf("failed to create temp file: %w", err)
192 }200 }
193- defer file.Close()201+ defer func() {
202+ if err := file.Close(); err != nil {
203+ log.Printf("[favicon] failed to close file: %v", err)
204+ }
205+ }()
194 206
195 if err := png.Encode(file, img); err != nil {207 if err := png.Encode(file, img); err != nil {
196- os.Remove(tempFile)208+ if removeErr := os.Remove(tempFile); removeErr != nil {
209+ log.Printf("[favicon] failed to remove temp file: %v", removeErr)
210+ }
197 return fmt.Errorf("failed to encode PNG: %w", err)211 return fmt.Errorf("failed to encode PNG: %w", err)
198 }212 }
199 213
M internal/cache/filter_cache.go +10 -3
@@ -4,6 +4,7 @@ import (
44 "encoding/binary"
55 "fmt"
66 "io"
7+ "log"
78 "os"
89 "syscall"
910 "time"
@@ -110,18 +111,24 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) {
110111 // Parse the header
111112 header, err := fc.parseHeader()
112113 if err != nil {
113- fc.Unmap()
114+ if unmapErr := fc.Unmap(); unmapErr != nil {
115+ log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr)
116+ }
114117 return nil, fmt.Errorf("invalid cache header: %w", err)
115118 }
116119
117120 // Validate magic and version
118121 if header.Magic != cacheMagic {
119- fc.Unmap()
122+ if unmapErr := fc.Unmap(); unmapErr != nil {
123+ log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr)
124+ }
120125 return nil, fmt.Errorf("invalid cache magic bytes")
121126 }
122127
123128 if header.Version != cacheFormatVersion {
124- fc.Unmap()
129+ if unmapErr := fc.Unmap(); unmapErr != nil {
130+ log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr)
131+ }
125132 return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
126133 }
127134
@@ -4,6 +4,7 @@ import (
4 "encoding/binary"4 "encoding/binary"
5 "fmt"5 "fmt"
6 "io"6 "io"
7+ "log"
7 "os"8 "os"
8 "syscall"9 "syscall"
9 "time"10 "time"
@@ -110,18 +111,24 @@ func (fc *FilterCache) LoadMapped() (*FilterData, error) {
110 // Parse the header111 // Parse the header
111 header, err := fc.parseHeader()112 header, err := fc.parseHeader()
112 if err != nil {113 if err != nil {
113- fc.Unmap()114+ if unmapErr := fc.Unmap(); unmapErr != nil {
115+ log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr)
116+ }
114 return nil, fmt.Errorf("invalid cache header: %w", err)117 return nil, fmt.Errorf("invalid cache header: %w", err)
115 }118 }
116 119
117 // Validate magic and version120 // Validate magic and version
118 if header.Magic != cacheMagic {121 if header.Magic != cacheMagic {
119- fc.Unmap()122+ if unmapErr := fc.Unmap(); unmapErr != nil {
123+ log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr)
124+ }
120 return nil, fmt.Errorf("invalid cache magic bytes")125 return nil, fmt.Errorf("invalid cache magic bytes")
121 }126 }
122 127
123 if header.Version != cacheFormatVersion {128 if header.Version != cacheFormatVersion {
124- fc.Unmap()129+ if unmapErr := fc.Unmap(); unmapErr != nil {
130+ log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr)
131+ }
125 return nil, fmt.Errorf("unsupported cache version: %d", header.Version)132 return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
126 }133 }
127 134
M internal/cli/logs.go +87 -0
@@ -26,6 +26,7 @@ func NewLogsCmd() *cobra.Command {
2626 cmd.AddCommand(newLogsListCmd())
2727 cmd.AddCommand(newLogsCleanCmd())
2828 cmd.AddCommand(newLogsPathCmd())
29+ cmd.AddCommand(newLogsConsoleCmd())
2930
3031 return cmd
3132 }
@@ -126,6 +127,32 @@ func newLogsPathCmd() *cobra.Command {
126127 return cmd
127128 }
128129
130+// newLogsConsoleCmd creates the console subcommand for filtering console logs
131+func newLogsConsoleCmd() *cobra.Command {
132+ var lines int
133+ var level string
134+
135+ cmd := &cobra.Command{
136+ Use: "console",
137+ Short: "View console logs from WebKit",
138+ Long: "Display console.log, console.error, and other console messages from web pages.",
139+ RunE: func(cmd *cobra.Command, args []string) error {
140+ logDir, err := config.GetLogDir()
141+ if err != nil {
142+ return fmt.Errorf("failed to get log directory: %w", err)
143+ }
144+
145+ currentLog := filepath.Join(logDir, "dumber.log")
146+ return tailConsoleMessages(currentLog, lines, level)
147+ },
148+ }
149+
150+ cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show")
151+ cmd.Flags().StringVarP(&level, "level", "l", "", "Filter by console level (error, warn, info, log, debug)")
152+
153+ return cmd
154+}
155+
129156 // tailFile displays the last N lines of a file
130157 func tailFile(filePath string, lines int) error {
131158 file, err := os.Open(filePath)
@@ -297,3 +324,63 @@ func formatSize(size int64) string {
297324
298325 return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
299326 }
327+
328+// tailConsoleMessages displays console messages from the log file
329+func tailConsoleMessages(filePath string, lines int, level string) error {
330+ file, err := os.Open(filePath)
331+ if err != nil {
332+ if os.IsNotExist(err) {
333+ fmt.Printf("Log file does not exist: %s\n", filePath)
334+ return nil
335+ }
336+ return fmt.Errorf("failed to open log file: %w", err)
337+ }
338+ defer file.Close()
339+
340+ // Read all lines and filter for console messages
341+ var consoleLines []string
342+ scanner := bufio.NewScanner(file)
343+ for scanner.Scan() {
344+ line := scanner.Text()
345+
346+ // Filter for [CONSOLE] tagged messages
347+ if !strings.Contains(line, "[CONSOLE]") {
348+ continue
349+ }
350+
351+ // Filter by level if specified
352+ if level != "" {
353+ levelFilter := fmt.Sprintf("[%s]", strings.ToUpper(level))
354+ if !strings.Contains(line, levelFilter) {
355+ continue
356+ }
357+ }
358+
359+ consoleLines = append(consoleLines, line)
360+ }
361+
362+ if err := scanner.Err(); err != nil {
363+ return fmt.Errorf("error reading log file: %w", err)
364+ }
365+
366+ if len(consoleLines) == 0 {
367+ if level != "" {
368+ fmt.Printf("No console messages found with level '%s'\n", level)
369+ } else {
370+ fmt.Println("No console messages found")
371+ }
372+ return nil
373+ }
374+
375+ // Display last N lines
376+ start := len(consoleLines) - lines
377+ if start < 0 {
378+ start = 0
379+ }
380+
381+ for i := start; i < len(consoleLines); i++ {
382+ fmt.Println(consoleLines[i])
383+ }
384+
385+ return nil
386+}
@@ -26,6 +26,7 @@ func NewLogsCmd() *cobra.Command {
26 cmd.AddCommand(newLogsListCmd())26 cmd.AddCommand(newLogsListCmd())
27 cmd.AddCommand(newLogsCleanCmd())27 cmd.AddCommand(newLogsCleanCmd())
28 cmd.AddCommand(newLogsPathCmd())28 cmd.AddCommand(newLogsPathCmd())
29+ cmd.AddCommand(newLogsConsoleCmd())
29 30
30 return cmd31 return cmd
31 }32 }
@@ -126,6 +127,32 @@ func newLogsPathCmd() *cobra.Command {
126 return cmd127 return cmd
127 }128 }
128 129
130+// newLogsConsoleCmd creates the console subcommand for filtering console logs
131+func newLogsConsoleCmd() *cobra.Command {
132+ var lines int
133+ var level string
134+
135+ cmd := &cobra.Command{
136+ Use: "console",
137+ Short: "View console logs from WebKit",
138+ Long: "Display console.log, console.error, and other console messages from web pages.",
139+ RunE: func(cmd *cobra.Command, args []string) error {
140+ logDir, err := config.GetLogDir()
141+ if err != nil {
142+ return fmt.Errorf("failed to get log directory: %w", err)
143+ }
144+
145+ currentLog := filepath.Join(logDir, "dumber.log")
146+ return tailConsoleMessages(currentLog, lines, level)
147+ },
148+ }
149+
150+ cmd.Flags().IntVarP(&lines, "lines", "n", 50, "Number of lines to show")
151+ cmd.Flags().StringVarP(&level, "level", "l", "", "Filter by console level (error, warn, info, log, debug)")
152+
153+ return cmd
154+}
155+
129 // tailFile displays the last N lines of a file156 // tailFile displays the last N lines of a file
130 func tailFile(filePath string, lines int) error {157 func tailFile(filePath string, lines int) error {
131 file, err := os.Open(filePath)158 file, err := os.Open(filePath)
@@ -297,3 +324,63 @@ func formatSize(size int64) string {
297 324
298 return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])325 return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
299 }326 }
327+
328+// tailConsoleMessages displays console messages from the log file
329+func tailConsoleMessages(filePath string, lines int, level string) error {
330+ file, err := os.Open(filePath)
331+ if err != nil {
332+ if os.IsNotExist(err) {
333+ fmt.Printf("Log file does not exist: %s\n", filePath)
334+ return nil
335+ }
336+ return fmt.Errorf("failed to open log file: %w", err)
337+ }
338+ defer file.Close()
339+
340+ // Read all lines and filter for console messages
341+ var consoleLines []string
342+ scanner := bufio.NewScanner(file)
343+ for scanner.Scan() {
344+ line := scanner.Text()
345+
346+ // Filter for [CONSOLE] tagged messages
347+ if !strings.Contains(line, "[CONSOLE]") {
348+ continue
349+ }
350+
351+ // Filter by level if specified
352+ if level != "" {
353+ levelFilter := fmt.Sprintf("[%s]", strings.ToUpper(level))
354+ if !strings.Contains(line, levelFilter) {
355+ continue
356+ }
357+ }
358+
359+ consoleLines = append(consoleLines, line)
360+ }
361+
362+ if err := scanner.Err(); err != nil {
363+ return fmt.Errorf("error reading log file: %w", err)
364+ }
365+
366+ if len(consoleLines) == 0 {
367+ if level != "" {
368+ fmt.Printf("No console messages found with level '%s'\n", level)
369+ } else {
370+ fmt.Println("No console messages found")
371+ }
372+ return nil
373+ }
374+
375+ // Display last N lines
376+ start := len(consoleLines) - lines
377+ if start < 0 {
378+ start = 0
379+ }
380+
381+ for i := start; i < len(consoleLines); i++ {
382+ fmt.Println(consoleLines[i])
383+ }
384+
385+ return nil
386+}
M internal/config/config.go +17 -3
@@ -101,9 +101,10 @@ type LoggingConfig struct {
101101 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`
102102
103103 // Capture settings
104- CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"`
105- CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"`
106- CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"`
104+ CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout" json:"capture_stdout"`
105+ CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr" json:"capture_stderr"`
106+ CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output" json:"capture_c_output"`
107+ CaptureConsole bool `mapstructure:"capture_console" yaml:"capture_console" json:"capture_console"`
107108
108109 // Debug output
109110 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`
@@ -258,6 +259,15 @@ type PopupBehaviorConfig struct {
258259 Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
259260 OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
260261 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
262+
263+ // New fields for distinguishing window types and behaviors
264+ // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents
265+ // Accepted values: "pane" (default) or "tab" (future support)
266+ BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`
267+ // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents
268+ EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`
269+ // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects
270+ OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`
261271 }
262272
263273 // WorkspaceStylingConfig defines visual styling for workspace panes.
@@ -678,6 +688,10 @@ func (m *Manager) setDefaults() {
678688 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
679689 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
680690 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
691+ // New popup behaviour defaults
692+ m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
693+ m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
694+ m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
681695 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
682696 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
683697 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
@@ -101,9 +101,10 @@ type LoggingConfig struct {
101 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`101 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`
102 102
103 // Capture settings103 // Capture settings
104- CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"`104+ CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout" json:"capture_stdout"`
105- CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"`105+ CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr" json:"capture_stderr"`
106- CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"`106+ CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output" json:"capture_c_output"`
107+ CaptureConsole bool `mapstructure:"capture_console" yaml:"capture_console" json:"capture_console"`
107 108
108 // Debug output109 // Debug output
109 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`110 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`
@@ -258,6 +259,15 @@ type PopupBehaviorConfig struct {
258 Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`259 Placement string `mapstructure:"placement" yaml:"placement" json:"placement"`
259 OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`260 OpenInNewPane bool `mapstructure:"open_in_new_pane" yaml:"open_in_new_pane" json:"open_in_new_pane"`
260 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`261 FollowPaneContext bool `mapstructure:"follow_pane_context" yaml:"follow_pane_context" json:"follow_pane_context"`
262+
263+ // New fields for distinguishing window types and behaviors
264+ // BlankTargetBehavior determines how to handle window.open(url, "_blank") intents
265+ // Accepted values: "pane" (default) or "tab" (future support)
266+ BlankTargetBehavior string `mapstructure:"blank_target_behavior" yaml:"blank_target_behavior" json:"blank_target_behavior"`
267+ // EnableSmartDetection uses WebKitWindowProperties to detect popup vs tab intents
268+ EnableSmartDetection bool `mapstructure:"enable_smart_detection" yaml:"enable_smart_detection" json:"enable_smart_detection"`
269+ // OAuthAutoClose enables auto-closing OAuth popups after successful auth redirects
270+ OAuthAutoClose bool `mapstructure:"oauth_auto_close" yaml:"oauth_auto_close" json:"oauth_auto_close"`
261 }271 }
262 272
263 // WorkspaceStylingConfig defines visual styling for workspace panes.273 // WorkspaceStylingConfig defines visual styling for workspace panes.
@@ -678,6 +688,10 @@ func (m *Manager) setDefaults() {
678 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)688 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
679 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)689 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
680 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)690 m.viper.SetDefault("workspace.popups.follow_pane_context", defaults.Workspace.Popups.FollowPaneContext)
691+ // New popup behaviour defaults
692+ m.viper.SetDefault("workspace.popups.blank_target_behavior", defaults.Workspace.Popups.BlankTargetBehavior)
693+ m.viper.SetDefault("workspace.popups.enable_smart_detection", defaults.Workspace.Popups.EnableSmartDetection)
694+ m.viper.SetDefault("workspace.popups.oauth_auto_close", defaults.Workspace.Popups.OAuthAutoClose)
681 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)695 m.viper.SetDefault("workspace.styling.border_width", defaults.Workspace.Styling.BorderWidth)
682 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)696 m.viper.SetDefault("workspace.styling.border_color", defaults.Workspace.Styling.BorderColor)
683 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)697 m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration)
M internal/config/defaults.go +7 -3
@@ -101,6 +101,7 @@ func DefaultConfig() *Config {
101101 CaptureStdout: false,
102102 CaptureStderr: false,
103103 CaptureCOutput: false, // Disabled by default to avoid performance impact
104+ CaptureConsole: false, // Disabled by default
104105 DebugFile: "debug.log",
105106 VerboseWebKit: false,
106107 },
@@ -183,9 +184,12 @@ func DefaultConfig() *Config {
183184 PreviousTab: "cmdorctrl+shift+tab",
184185 },
185186 Popups: PopupBehaviorConfig{
186- Placement: defaultPopupPlacement,
187- OpenInNewPane: true,
188- FollowPaneContext: true,
187+ Placement: defaultPopupPlacement,
188+ OpenInNewPane: true,
189+ FollowPaneContext: true,
190+ BlankTargetBehavior: "pane", // Default to pane, future: "tab"
191+ EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab
192+ OAuthAutoClose: true, // Auto-close OAuth popups on success
189193 },
190194 Styling: WorkspaceStylingConfig{
191195 BorderWidth: defaultBorderWidth,
@@ -101,6 +101,7 @@ func DefaultConfig() *Config {
101 CaptureStdout: false,101 CaptureStdout: false,
102 CaptureStderr: false,102 CaptureStderr: false,
103 CaptureCOutput: false, // Disabled by default to avoid performance impact103 CaptureCOutput: false, // Disabled by default to avoid performance impact
104+ CaptureConsole: false, // Disabled by default
104 DebugFile: "debug.log",105 DebugFile: "debug.log",
105 VerboseWebKit: false,106 VerboseWebKit: false,
106 },107 },
@@ -183,9 +184,12 @@ func DefaultConfig() *Config {
183 PreviousTab: "cmdorctrl+shift+tab",184 PreviousTab: "cmdorctrl+shift+tab",
184 },185 },
185 Popups: PopupBehaviorConfig{186 Popups: PopupBehaviorConfig{
186- Placement: defaultPopupPlacement,187+ Placement: defaultPopupPlacement,
187- OpenInNewPane: true,188+ OpenInNewPane: true,
188- FollowPaneContext: true,189+ FollowPaneContext: true,
190+ BlankTargetBehavior: "pane", // Default to pane, future: "tab"
191+ EnableSmartDetection: true, // Use WindowProperties to detect popup vs tab
192+ OAuthAutoClose: true, // Auto-close OAuth popups on success
189 },193 },
190 Styling: WorkspaceStylingConfig{194 Styling: WorkspaceStylingConfig{
191 BorderWidth: defaultBorderWidth,195 BorderWidth: defaultBorderWidth,
M internal/logging/logger.go +8 -1
@@ -218,10 +218,17 @@ func Fatal(message string) {
218218
219219 func CaptureWebKitLog(message string) {
220220 if l := GetLogger(); l != nil {
221- l.WriteTagged("WEBKIT", message)
221+ l.WriteTagged("CONSOLE", message)
222222 }
223223 }
224224
225+// StartConsoleCapture starts capturing stdout for console messages
226+// StartConsoleCapture starts capturing stdout for console messages and writing to console.log
227+// StartConsoleCapture - placeholder for console message capture
228+func StartConsoleCapture() {
229+ // Placeholder - console message capture will be implemented differently
230+}
231+
225232 // LogLevelInfo returns the INFO log level constant for external packages
226233 func LogLevelInfo() LogLevel {
227234 return INFO
@@ -218,10 +218,17 @@ func Fatal(message string) {
218 218
219 func CaptureWebKitLog(message string) {219 func CaptureWebKitLog(message string) {
220 if l := GetLogger(); l != nil {220 if l := GetLogger(); l != nil {
221- l.WriteTagged("WEBKIT", message)221+ l.WriteTagged("CONSOLE", message)
222 }222 }
223 }223 }
224 224
225+// StartConsoleCapture starts capturing stdout for console messages
226+// StartConsoleCapture starts capturing stdout for console messages and writing to console.log
227+// StartConsoleCapture - placeholder for console message capture
228+func StartConsoleCapture() {
229+ // Placeholder - console message capture will be implemented differently
230+}
231+
225 // LogLevelInfo returns the INFO log level constant for external packages232 // LogLevelInfo returns the INFO log level constant for external packages
226 func LogLevelInfo() LogLevel {233 func LogLevelInfo() LogLevel {
227 return INFO234 return INFO
M pkg/webkit/keyboard_cgo.go +2 -41
@@ -3,9 +3,10 @@
33 package webkit
44
55 /*
6-#cgo pkg-config: gtk4
6+#cgo pkg-config: gtk4 webkitgtk-6.0
77 #include <gtk/gtk.h>
88 #include <gdk/gdk.h>
9+#include <webkit/webkit.h>
910
1011 // Declarations for Go callbacks
1112 extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
@@ -84,7 +85,6 @@ import (
8485 "sync"
8586 "sync/atomic"
8687 "time"
87- "unsafe"
8888 )
8989
9090 // Registry of accelerators per view id.
@@ -489,45 +489,6 @@ func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean
489489 }
490490 }
491491
492-//export goHandleCreateWebView
493-func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget {
494- uid := uintptr(id)
495- regMu.RLock()
496- vw := viewByID[uid]
497- regMu.RUnlock()
498-
499- if vw == nil {
500- log.Printf("[webkit] Create signal: no WebView found for ID %d", uid)
501- return nil // Fallback to native popup
502- }
503-
504- uri := ""
505- if curi != nil {
506- uri = C.GoString(curi)
507- }
508-
509- log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri)
510-
511- // Check if we have a popup handler
512- if vw.popupHandler == nil {
513- log.Printf("[webkit] Create signal: no popup handler registered")
514- return nil // Fallback to native popup
515- }
516-
517- // Always use the popup handler to create workspace pane (tiling WM design)
518- newView := vw.popupHandler(uri)
519- if newView != nil {
520- widget := newView.Widget()
521- if widget != 0 {
522- log.Printf("[webkit] Create signal: created new workspace pane for popup")
523- return (*C.GtkWidget)(unsafe.Pointer(widget))
524- }
525- }
526-
527- log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native")
528- return nil // Fallback to native popup if workspace creation fails
529-}
530-
531492 //export goHandleLoadChanged
532493 func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
533494 uid := uintptr(id)
@@ -3,9 +3,10 @@
3 package webkit3 package webkit
4 4
5 /*5 /*
6-#cgo pkg-config: gtk46+#cgo pkg-config: gtk4 webkitgtk-6.0
7 #include <gtk/gtk.h>7 #include <gtk/gtk.h>
8 #include <gdk/gdk.h>8 #include <gdk/gdk.h>
9+#include <webkit/webkit.h>
9 10
10 // Declarations for Go callbacks11 // Declarations for Go callbacks
11 extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);12 extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
@@ -84,7 +85,6 @@ import (
84 "sync"85 "sync"
85 "sync/atomic"86 "sync/atomic"
86 "time"87 "time"
87- "unsafe"
88 )88 )
89 89
90 // Registry of accelerators per view id.90 // Registry of accelerators per view id.
@@ -489,45 +489,6 @@ func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean
489 }489 }
490 }490 }
491 491
492-//export goHandleCreateWebView
493-func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget {
494- uid := uintptr(id)
495- regMu.RLock()
496- vw := viewByID[uid]
497- regMu.RUnlock()
498-
499- if vw == nil {
500- log.Printf("[webkit] Create signal: no WebView found for ID %d", uid)
501- return nil // Fallback to native popup
502- }
503-
504- uri := ""
505- if curi != nil {
506- uri = C.GoString(curi)
507- }
508-
509- log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri)
510-
511- // Check if we have a popup handler
512- if vw.popupHandler == nil {
513- log.Printf("[webkit] Create signal: no popup handler registered")
514- return nil // Fallback to native popup
515- }
516-
517- // Always use the popup handler to create workspace pane (tiling WM design)
518- newView := vw.popupHandler(uri)
519- if newView != nil {
520- widget := newView.Widget()
521- if widget != 0 {
522- log.Printf("[webkit] Create signal: created new workspace pane for popup")
523- return (*C.GtkWidget)(unsafe.Pointer(widget))
524- }
525- }
526-
527- log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native")
528- return nil // Fallback to native popup if workspace creation fails
529-}
530-
531 //export goHandleLoadChanged492 //export goHandleLoadChanged
532 func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {493 func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
533 uid := uintptr(id)494 uid := uintptr(id)
M pkg/webkit/webview.go +23 -0
@@ -26,6 +26,11 @@ type WebView struct {
2626 useDomZoom bool
2727 domZoomSeed float64
2828 container uintptr
29+
30+ // Window type fields (no-op in stub)
31+ windowType WindowType
32+ windowFeatures *WindowFeatures
33+ windowTypeCallback func(WindowType, *WindowFeatures)
2934 }
3035
3136 // NewWebView constructs a new WebView instance.
@@ -211,6 +216,19 @@ func (w *WebView) SeedDomZoom(level float64) {
211216 w.domZoomSeed = level
212217 }
213218
219+// CreateRelatedView returns a new WebView (stub creates independent)
220+func (w *WebView) CreateRelatedView() *WebView {
221+ nw, _ := NewWebView(w.config)
222+ return nw
223+}
224+
225+// OnWindowTypeDetected registers a callback (stored but never invoked in stub)
226+func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
227+ if w != nil {
228+ w.windowTypeCallback = callback
229+ }
230+}
231+
214232 // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub)
215233 func (w *WebView) InitializeContentBlocking(filterManager interface{}) error {
216234 return ErrNotImplemented
@@ -225,3 +243,8 @@ func (w *WebView) OnNavigate(url string, filterManager interface{}) {
225243 func (w *WebView) UpdateContentFilters(filterManager interface{}) error {
226244 return ErrNotImplemented
227245 }
246+
247+// SetWindowFeatures sets the window features for this WebView (stub)
248+func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
249+ w.windowFeatures = features
250+}
@@ -26,6 +26,11 @@ type WebView struct {
26 useDomZoom bool26 useDomZoom bool
27 domZoomSeed float6427 domZoomSeed float64
28 container uintptr28 container uintptr
29+
30+ // Window type fields (no-op in stub)
31+ windowType WindowType
32+ windowFeatures *WindowFeatures
33+ windowTypeCallback func(WindowType, *WindowFeatures)
29 }34 }
30 35
31 // NewWebView constructs a new WebView instance.36 // NewWebView constructs a new WebView instance.
@@ -211,6 +216,19 @@ func (w *WebView) SeedDomZoom(level float64) {
211 w.domZoomSeed = level216 w.domZoomSeed = level
212 }217 }
213 218
219+// CreateRelatedView returns a new WebView (stub creates independent)
220+func (w *WebView) CreateRelatedView() *WebView {
221+ nw, _ := NewWebView(w.config)
222+ return nw
223+}
224+
225+// OnWindowTypeDetected registers a callback (stored but never invoked in stub)
226+func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
227+ if w != nil {
228+ w.windowTypeCallback = callback
229+ }
230+}
231+
214 // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub)232 // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub)
215 func (w *WebView) InitializeContentBlocking(filterManager interface{}) error {233 func (w *WebView) InitializeContentBlocking(filterManager interface{}) error {
216 return ErrNotImplemented234 return ErrNotImplemented
@@ -225,3 +243,8 @@ func (w *WebView) OnNavigate(url string, filterManager interface{}) {
225 func (w *WebView) UpdateContentFilters(filterManager interface{}) error {243 func (w *WebView) UpdateContentFilters(filterManager interface{}) error {
226 return ErrNotImplemented244 return ErrNotImplemented
227 }245 }
246+
247+// SetWindowFeatures sets the window features for this WebView (stub)
248+func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
249+ w.windowFeatures = features
250+}
M pkg/webkit/webview_cgo.go +211 -176
@@ -24,7 +24,6 @@ extern void goQuitMainLoop();
2424 extern void goInvokeHandle(uintptr_t handle);
2525 extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type);
2626 extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height);
27-extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri);
2827 extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event);
2928 extern void goHandleWebViewClose(unsigned long id);
3029 static gboolean invoke_handle_cb(gpointer data) {
@@ -49,27 +48,17 @@ static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
4948
5049 // TLS error handling forward declarations
5150 extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
51+
52+// Console message handling forward declarations
53+extern void goHandleConsoleMessage(char* message);
54+
5255 static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
5356 const char *failing_uri,
5457 GTlsCertificate *certificate,
5558 GTlsCertificateFlags errors,
5659 gpointer user_data);
5760
58-// Handle ready-to-show signal for popup windows to properly initialize window properties
59-static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) {
60- // Get window properties to ensure they're properly initialized
61- WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view);
62- if (props) {
63- // Access properties to ensure WindowFeatures are initialized
64- GdkRectangle geometry;
65- webkit_window_properties_get_geometry(props, &geometry);
6661
67- // Pass geometry to Go for workspace positioning
68- unsigned long view_id = (unsigned long)user_data;
69- goHandlePopupGeometry(view_id, geometry.x, geometry.y,
70- geometry.width, geometry.height);
71- }
72-}
7362
7463 // Handle load_changed signal for OAuth completion detection
7564 static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) {
@@ -90,38 +79,6 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) {
9079 goHandleWebViewClose(view_id);
9180 }
9281
93-// Handle create signal for popup windows - essential for window.open() support
94-static GtkWidget* on_create_web_view(WebKitWebView *web_view,
95- WebKitNavigationAction *navigation_action,
96- gpointer user_data) {
97- WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action);
98- const char* uri = webkit_uri_request_get_uri(request);
99-
100- printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL");
101-
102- unsigned long view_id = (unsigned long)user_data;
103- GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri);
104-
105- if (new_webview) {
106- printf("[webkit-create] Go handler provided WebView - using workspace pane\n");
107-
108- // Connect load_changed signal to the new popup WebView for OAuth detection
109- WebKitWebView* popup_webview = WEBKIT_WEB_VIEW(new_webview);
110- g_signal_connect_data(G_OBJECT(popup_webview), "load-changed",
111- G_CALLBACK(on_load_changed), user_data, NULL, 0);
112- printf("[webkit-create] Connected load-changed signal to popup for OAuth detection\n");
113-
114- // Connect close signal to handle window.close() from OAuth providers
115- g_signal_connect_data(G_OBJECT(popup_webview), "close",
116- G_CALLBACK(on_webview_close), user_data, NULL, 0);
117- printf("[webkit-create] Connected close signal to popup for OAuth auto-close\n");
118-
119- return new_webview;
120- } else {
121- printf("[webkit-create] Go handler declined - allowing native popup window\n");
122- return NULL; // Let WebKit create native popup window
123- }
124-}
12582
12683 // Handle decide-policy signal for new window requests - more robust than create signal
12784 static gboolean on_decide_policy(WebKitWebView *web_view,
@@ -282,9 +239,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon
282239
283240 static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) {
284241 if (!web_view) return;
285- g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0);
286- // Connect create signal for popup window support
287- g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0);
242+ // Create signal is now handled in window_handler_cgo.go
243+ // Use ConnectCreateSignal from Go to connect the signal
244+ printf("[webkit-debug] Policy handler connected (create signal handled separately)\n");
288245 }
289246
290247 // Connect close signal to a WebView for popup auto-close functionality
@@ -780,6 +737,7 @@ import (
780737 "context"
781738 "crypto/sha256"
782739 "database/sql"
740+ "encoding/json"
783741 "errors"
784742 "fmt"
785743 "log"
@@ -807,125 +765,7 @@ type FilterManager interface {
807765 GetCosmeticScriptForDomain(domain string) string
808766 }
809767
810-const domBridgeTemplate = `(() => { try {
811- if (window.__dumber_page_bridge_installed) return;
812- window.__dumber_page_bridge_installed = true;
813-
814- const __dumberInitialZoom = __DOM_ZOOM_DEFAULT__;
815- window.__dumber_dom_zoom_seed = __dumberInitialZoom;
816-
817- // Theme setter function for GTK theme integration
818- window.__dumber_setTheme = (theme) => {
819- window.__dumber_initial_theme = theme;
820- console.log('[dumber] Setting theme to:', theme);
821- if (theme === 'dark') {
822- document.documentElement.classList.add('dark');
823- } else {
824- document.documentElement.classList.remove('dark');
825- }
826- };
827-
828- // Ensure unified API object
829- window.__dumber = window.__dumber || {};
830- // Toast namespace
831- window.__dumber.toast = window.__dumber.toast || {
832- show: function(message, duration, type) {
833- try {
834- document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } }));
835- // Legacy compatibility
836- document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } }));
837- } catch(e) { /* ignore */ }
838- },
839- zoom: function(level) {
840- try {
841- document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } }));
842- // Legacy compatibility
843- document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } }));
844- } catch(e) { /* ignore */ }
845- }
846- };
847- // Back-compat global helpers
848- window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); };
849- window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); };
850-
851- // DOM zoom helpers (optional native override)
852- if (typeof window.__dumber_dom_zoom_level !== 'number') {
853- window.__dumber_dom_zoom_level = __dumberInitialZoom;
854- }
855- const __dumberApplyZoomStyles = function(node, level) {
856- if (!node) return;
857- if (Math.abs(level - 1.0) < 1e-6) {
858- node.style.removeProperty('zoom');
859- node.style.removeProperty('transform');
860- node.style.removeProperty('transform-origin');
861- node.style.removeProperty('width');
862- node.style.removeProperty('min-width');
863- node.style.removeProperty('height');
864- node.style.removeProperty('min-height');
865- return;
866- }
867- const scale = level;
868- const inversePercent = 100 / scale;
869- const widthValue = inversePercent + '%';
870- node.style.removeProperty('zoom');
871- node.style.transform = 'scale(' + scale + ')';
872- node.style.transformOrigin = '0 0';
873- node.style.width = widthValue;
874- node.style.minWidth = widthValue;
875- node.style.minHeight = '100%';
876- };
877- window.__dumber_applyDomZoom = function(level) {
878- try {
879- window.__dumber_dom_zoom_level = level;
880- window.__dumber_dom_zoom_seed = level;
881- __dumberApplyZoomStyles(document.documentElement, level);
882- if (document.body) {
883- __dumberApplyZoomStyles(document.body, level);
884- }
885- } catch (e) {
886- console.error('[dumber] DOM zoom error', e);
887- }
888- };
889- // Apply immediately so first paint uses the desired zoom, then reapply when body exists.
890- window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
891- if (!document.body) {
892- document.addEventListener('DOMContentLoaded', function() {
893- if (typeof window.__dumber_dom_zoom_level === 'number') {
894- window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
895- }
896- }, { once: true });
897- }
898-
899- // Omnibox suggestions bridge (with queue before ready)
900- let __omniboxQueue = [];
901- let __omniboxReady = false;
902- function __omniboxDispatch(suggestions) {
903- try {
904- document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } }));
905- // Legacy compatibility event name
906- document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } }));
907- } catch (e) { /* ignore */ }
908- }
909- window.__dumber_omnibox_suggestions = function(suggestions) {
910- if (__omniboxReady) {
911- __omniboxDispatch(suggestions);
912- } else {
913- try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ }
914- }
915- };
916- document.addEventListener('dumber:omnibox-ready', function() {
917- __omniboxReady = true;
918- if (__omniboxQueue && __omniboxQueue.length) {
919- const items = __omniboxQueue.slice();
920- __omniboxQueue.length = 0;
921- for (const s of items) __omniboxDispatch(s);
922- }
923- });
924- // Unified omnibox API for page-world callers
925- window.__dumber.omnibox = window.__dumber.omnibox || {
926- suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); }
927- };
928- } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();`
768+// domBridgeTemplate removed - now loading compiled main-world.min.js
929769
930770 var registerSchemeOnce sync.Once
931771
@@ -1071,9 +911,9 @@ type WebView struct {
1071911 titleHandler func(title string)
1072912 uriHandler func(uri string)
1073913 zoomHandler func(level float64)
1074- popupHandler func(string) *WebView
1075- closeHandler func()
1076- memStats *memoryStats
914+ popupHandler func(string) *WebView
915+ closeHandler func()
916+ memStats *memoryStats
1077917 gcTicker *time.Ticker
1078918 gcDone chan struct{}
1079919 tlsExceptions map[string]bool // host -> whether user allowed certificate exception
@@ -1081,6 +921,13 @@ type WebView struct {
1081921 domZoomSeed float64
1082922 domBridgeScript *C.WebKitUserScript
1083923 wasReparented bool // Track if this WebView has been reparented
924+
925+ // Window type detection and relationships
926+ windowType WindowType
927+ windowFeatures *WindowFeatures
928+ windowTypeCallback func(WindowType, *WindowFeatures)
929+ parentWebView *WebView
930+ isRelated bool
1084931 }
1085932
1086933 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -1237,6 +1084,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
12371084 v.id = nextViewID()
12381085 registerView(v.id, v)
12391086 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1087+ ConnectCreateSignal(v) // Connect create signal for window.open support
12401088
12411089 // Register with global memory manager if monitoring is enabled
12421090 if cfg.Memory.EnableMemoryMonitoring {
@@ -1446,9 +1294,124 @@ func NewWebView(cfg *Config) (*WebView, error) {
14461294 }
14471295 }
14481296
1297+ // Setup console message logging
1298+ v.setupConsoleLogging()
1299+
14491300 return v, nil
14501301 }
14511302
1303+// setupConsoleLogging enables console message capture for the WebView
1304+func (v *WebView) setupConsoleLogging() {
1305+ if v.native == nil || v.native.wv == nil {
1306+ return
1307+ }
1308+
1309+ // Check if console capture is enabled in config
1310+ cfg := config.Get()
1311+ if !cfg.Logging.CaptureConsole {
1312+ // Disable console messages to stdout (always, to keep backend clean)
1313+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1314+ C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1315+ }
1316+ return
1317+ }
1318+
1319+ // If console capture is enabled, disable stdout and enable our capture
1320+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1321+ C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1322+ log.Printf("[webkit] Console message capture enabled - redirecting to console.log file")
1323+ }
1324+
1325+ // TODO: Implement JavaScript injection for console capture
1326+ // This will inject code to intercept console.log/error/warn and send to Go
1327+}
1328+
1329+// setupGlobalConsoleCapture sets up a global stdout filter to capture console messages
1330+func setupGlobalConsoleCapture() {
1331+ // This should be called once globally, not per WebView
1332+ logging.StartConsoleCapture()
1333+}
1334+
1335+// injectConsoleCapture injects JavaScript to capture console messages
1336+func (v *WebView) injectConsoleCapture() {
1337+ consoleScript := `
1338+(function() {
1339+ const originalLog = console.log;
1340+ const originalWarn = console.warn;
1341+ const originalError = console.error;
1342+ const originalInfo = console.info;
1343+ const originalDebug = console.debug;
1344+
1345+ function sendToGo(level, args) {
1346+ const message = args.map(arg =>
1347+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1348+ ).join(' ');
1349+
1350+ const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message;
1351+
1352+ // Send to Go via user script message
1353+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) {
1354+ window.webkit.messageHandlers.dumberBridge.postMessage({
1355+ type: 'console',
1356+ data: formattedMessage
1357+ });
1358+ }
1359+ }
1360+
1361+ console.log = function(...args) {
1362+ originalLog.apply(console, args);
1363+ sendToGo('LOG', args);
1364+ };
1365+
1366+ console.warn = function(...args) {
1367+ originalWarn.apply(console, args);
1368+ sendToGo('WARN', args);
1369+ };
1370+
1371+ console.error = function(...args) {
1372+ originalError.apply(console, args);
1373+ sendToGo('ERROR', args);
1374+ };
1375+
1376+ console.info = function(...args) {
1377+ originalInfo.apply(console, args);
1378+ sendToGo('INFO', args);
1379+ };
1380+
1381+ console.debug = function(...args) {
1382+ originalDebug.apply(console, args);
1383+ sendToGo('DEBUG', args);
1384+ };
1385+})();
1386+`
1387+ _ = v.InjectScript(consoleScript)
1388+}
1389+
1390+// handleConsoleMessage processes console messages from JavaScript
1391+func (v *WebView) handleConsoleMessage(payload string) {
1392+ // Parse the message payload to check if it's a console message
1393+ var msg struct {
1394+ Type string `json:"type"`
1395+ Data string `json:"data"`
1396+ }
1397+
1398+ if err := json.Unmarshal([]byte(payload), &msg); err != nil {
1399+ return
1400+ }
1401+
1402+ if msg.Type == "console" {
1403+ // Send to logging system with [CONSOLE] tag
1404+ logging.CaptureWebKitLog(msg.Data)
1405+ }
1406+}
1407+
1408+// startConsoleCapture starts capturing stdout to filter console messages
1409+func (v *WebView) startConsoleCapture() {
1410+ // This is a simplified approach - we'll rely on the existing OutputCapture
1411+ // mechanism in internal/logging/capture.go to handle stdout capture
1412+ // and filter for console messages in the log processing
1413+}
1414+
14521415 // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing
14531416 func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) {
14541417 if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil {
@@ -1532,6 +1495,7 @@ func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error)
15321495 v.id = nextViewID()
15331496 registerView(v.id, v)
15341497 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1498+ ConnectCreateSignal(v) // Connect create signal for window.open support
15351499
15361500 // Register with memory manager if enabled
15371501 if cfg.Memory.EnableMemoryMonitoring {
@@ -1744,6 +1708,46 @@ func (w *WebView) dispatchPopupRequest(uri string) *WebView {
17441708 return nil
17451709 }
17461710
1711+// CreateRelatedView creates a WebView that shares process/context with this view
1712+func (w *WebView) CreateRelatedView() *WebView {
1713+ if w == nil || w.native == nil || w.native.wv == nil {
1714+ return nil
1715+ }
1716+ // Build a minimal config inheriting from current view
1717+ cfg := &Config{
1718+ Assets: w.config.Assets,
1719+ ZoomDefault: w.config.ZoomDefault,
1720+ EnableDeveloperExtras: w.config.EnableDeveloperExtras,
1721+ DataDir: w.config.DataDir,
1722+ CacheDir: w.config.CacheDir,
1723+ APIToken: w.config.APIToken,
1724+ DefaultSansFont: w.config.DefaultSansFont,
1725+ DefaultSerifFont: w.config.DefaultSerifFont,
1726+ DefaultMonospaceFont: w.config.DefaultMonospaceFont,
1727+ DefaultFontSize: w.config.DefaultFontSize,
1728+ Rendering: w.config.Rendering,
1729+ UseDomZoom: w.config.UseDomZoom,
1730+ VideoAcceleration: w.config.VideoAcceleration,
1731+ Memory: w.config.Memory,
1732+ CodecPreferences: w.config.CodecPreferences,
1733+ CreateWindow: false,
1734+ }
1735+ related, err := NewWebViewWithRelated(cfg, w)
1736+ if err != nil {
1737+ log.Printf("[webkit] CreateRelatedView failed: %v", err)
1738+ return nil
1739+ }
1740+ return related
1741+}
1742+
1743+// OnWindowTypeDetected registers a callback invoked when window type is detected
1744+func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
1745+ if w == nil {
1746+ return
1747+ }
1748+ w.windowTypeCallback = callback
1749+}
1750+
17471751 // GetNativePointer returns the native WebView pointer for CGO callbacks
17481752 func (w *WebView) GetNativePointer() unsafe.Pointer {
17491753 if w != nil && w.native != nil && w.native.wv != nil {
@@ -2027,10 +2031,26 @@ func clampDomZoom(level float64) float64 {
20272031 return level
20282032 }
20292033
2030-func buildDomBridgeScript(initialZoom float64) string {
2034+func buildDomBridgeScript(cfg *Config, initialZoom float64) string {
2035+ if cfg == nil || cfg.Assets == nil {
2036+ log.Printf("[webkit] Config or assets not available for main-world script")
2037+ return ""
2038+ }
2039+
2040+ // Load the compiled main-world script
2041+ scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js")
2042+ if err != nil {
2043+ log.Printf("[webkit] Failed to load main-world script: %v", err)
2044+ return ""
2045+ }
2046+
2047+ // Replace the zoom placeholder with the actual value
20312048 zoom := clampDomZoom(initialZoom)
20322049 zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64)
2033- return strings.ReplaceAll(domBridgeTemplate, "__DOM_ZOOM_DEFAULT__", zoomStr)
2050+ script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr)
2051+
2052+ log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2053+ return script
20342054 }
20352055
20362056 func (w *WebView) UsesDomZoom() bool {
@@ -2050,7 +2070,7 @@ func (w *WebView) installDomBridgeScript() {
20502070 w.domBridgeScript = nil
20512071 }
20522072
2053- source := buildDomBridgeScript(w.domZoomSeed)
2073+ source := buildDomBridgeScript(w.config, w.domZoomSeed)
20542074 cBridge := C.CString(source)
20552075 defer C.free(unsafe.Pointer(cBridge))
20562076
@@ -2076,8 +2096,11 @@ func (w *WebView) SeedDomZoom(level float64) {
20762096 return
20772097 }
20782098 w.domZoomSeed = seed
2099+ // Instead of re-injecting the entire script, just update the zoom level
20792100 w.RunOnMainThread(func() {
2080- w.installDomBridgeScript()
2101+ zoomStr := strconv.FormatFloat(seed, 'f', -1, 64)
2102+ script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr)
2103+ w.InjectScript(script)
20812104 })
20822105 }
20832106
@@ -2212,6 +2235,13 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn
22122235 return C.FALSE // Don't proceed
22132236 }
22142237
2238+//export goHandleConsoleMessage
2239+//export goHandleConsoleMessage
2240+func goHandleConsoleMessage(message *C.char) {
2241+ messageText := C.GoString(message)
2242+ logging.CaptureWebKitLog(messageText)
2243+}
2244+
22152245 // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
22162246 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
22172247 if w == nil || w.native == nil || w.native.win == nil {
@@ -2530,3 +2560,8 @@ func (w *WebView) detectAndLogRenderingBackend() {
25302560 w.InjectScript(detectionScript)
25312561 }()
25322562 }
2563+
2564+// SetWindowFeatures sets the window features for this WebView
2565+func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
2566+ w.windowFeatures = features
2567+}
@@ -24,7 +24,6 @@ extern void goQuitMainLoop();
24 extern void goInvokeHandle(uintptr_t handle);24 extern void goInvokeHandle(uintptr_t handle);
25 extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type);25 extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type);
26 extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height);26 extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height);
27-extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri);
28 extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event);27 extern void goHandleLoadChanged(unsigned long id, char* uri, int load_event);
29 extern void goHandleWebViewClose(unsigned long id);28 extern void goHandleWebViewClose(unsigned long id);
30 static gboolean invoke_handle_cb(gpointer data) {29 static gboolean invoke_handle_cb(gpointer data) {
@@ -49,27 +48,17 @@ static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
49 48
50 // TLS error handling forward declarations49 // TLS error handling forward declarations
51 extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);50 extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
51+
52+// Console message handling forward declarations
53+extern void goHandleConsoleMessage(char* message);
54+
52 static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,55 static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
53 const char *failing_uri,56 const char *failing_uri,
54 GTlsCertificate *certificate,57 GTlsCertificate *certificate,
55 GTlsCertificateFlags errors,58 GTlsCertificateFlags errors,
56 gpointer user_data);59 gpointer user_data);
57 60
58-// Handle ready-to-show signal for popup windows to properly initialize window properties
59-static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) {
60- // Get window properties to ensure they're properly initialized
61- WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view);
62- if (props) {
63- // Access properties to ensure WindowFeatures are initialized
64- GdkRectangle geometry;
65- webkit_window_properties_get_geometry(props, &geometry);
66 61
67- // Pass geometry to Go for workspace positioning
68- unsigned long view_id = (unsigned long)user_data;
69- goHandlePopupGeometry(view_id, geometry.x, geometry.y,
70- geometry.width, geometry.height);
71- }
72-}
73 62
74 // Handle load_changed signal for OAuth completion detection63 // Handle load_changed signal for OAuth completion detection
75 static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) {64 static void on_load_changed(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) {
@@ -90,38 +79,6 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) {
90 goHandleWebViewClose(view_id);79 goHandleWebViewClose(view_id);
91 }80 }
92 81
93-// Handle create signal for popup windows - essential for window.open() support
94-static GtkWidget* on_create_web_view(WebKitWebView *web_view,
95- WebKitNavigationAction *navigation_action,
96- gpointer user_data) {
97- WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action);
98- const char* uri = webkit_uri_request_get_uri(request);
99-
100- printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL");
101-
102- unsigned long view_id = (unsigned long)user_data;
103- GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri);
104-
105- if (new_webview) {
106- printf("[webkit-create] Go handler provided WebView - using workspace pane\n");
107-
108- // Connect load_changed signal to the new popup WebView for OAuth detection
109- WebKitWebView* popup_webview = WEBKIT_WEB_VIEW(new_webview);
110- g_signal_connect_data(G_OBJECT(popup_webview), "load-changed",
111- G_CALLBACK(on_load_changed), user_data, NULL, 0);
112- printf("[webkit-create] Connected load-changed signal to popup for OAuth detection\n");
113-
114- // Connect close signal to handle window.close() from OAuth providers
115- g_signal_connect_data(G_OBJECT(popup_webview), "close",
116- G_CALLBACK(on_webview_close), user_data, NULL, 0);
117- printf("[webkit-create] Connected close signal to popup for OAuth auto-close\n");
118-
119- return new_webview;
120- } else {
121- printf("[webkit-create] Go handler declined - allowing native popup window\n");
122- return NULL; // Let WebKit create native popup window
123- }
124-}
125 82
126 // Handle decide-policy signal for new window requests - more robust than create signal83 // Handle decide-policy signal for new window requests - more robust than create signal
127 static gboolean on_decide_policy(WebKitWebView *web_view,84 static gboolean on_decide_policy(WebKitWebView *web_view,
@@ -282,9 +239,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon
282 239
283 static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) {240 static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) {
284 if (!web_view) return;241 if (!web_view) return;
285- g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0);242+ // Create signal is now handled in window_handler_cgo.go
286- // Connect create signal for popup window support243+ // Use ConnectCreateSignal from Go to connect the signal
287- g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0);244+ printf("[webkit-debug] Policy handler connected (create signal handled separately)\n");
288 }245 }
289 246
290 // Connect close signal to a WebView for popup auto-close functionality247 // Connect close signal to a WebView for popup auto-close functionality
@@ -780,6 +737,7 @@ import (
780 "context"737 "context"
781 "crypto/sha256"738 "crypto/sha256"
782 "database/sql"739 "database/sql"
740+ "encoding/json"
783 "errors"741 "errors"
784 "fmt"742 "fmt"
785 "log"743 "log"
@@ -807,125 +765,7 @@ type FilterManager interface {
807 GetCosmeticScriptForDomain(domain string) string765 GetCosmeticScriptForDomain(domain string) string
808 }766 }
809 767
810-const domBridgeTemplate = `(() => { try {768+// domBridgeTemplate removed - now loading compiled main-world.min.js
811- if (window.__dumber_page_bridge_installed) return;
812- window.__dumber_page_bridge_installed = true;
813-
814- const __dumberInitialZoom = __DOM_ZOOM_DEFAULT__;
815- window.__dumber_dom_zoom_seed = __dumberInitialZoom;
816-
817- // Theme setter function for GTK theme integration
818- window.__dumber_setTheme = (theme) => {
819- window.__dumber_initial_theme = theme;
820- console.log('[dumber] Setting theme to:', theme);
821- if (theme === 'dark') {
822- document.documentElement.classList.add('dark');
823- } else {
824- document.documentElement.classList.remove('dark');
825- }
826- };
827-
828- // Ensure unified API object
829- window.__dumber = window.__dumber || {};
830- // Toast namespace
831- window.__dumber.toast = window.__dumber.toast || {
832- show: function(message, duration, type) {
833- try {
834- document.dispatchEvent(new CustomEvent('dumber:toast:show', { detail: { message, duration, type } }));
835- // Legacy compatibility
836- document.dispatchEvent(new CustomEvent('dumber:showToast', { detail: { message, duration, type } }));
837- } catch(e) { /* ignore */ }
838- },
839- zoom: function(level) {
840- try {
841- document.dispatchEvent(new CustomEvent('dumber:toast:zoom', { detail: { level } }));
842- // Legacy compatibility
843- document.dispatchEvent(new CustomEvent('dumber:showZoomToast', { detail: { level } }));
844- } catch(e) { /* ignore */ }
845- }
846- };
847- // Back-compat global helpers
848- window.__dumber_showToast = function(message, duration, type) { window.__dumber.toast.show(message, duration, type); };
849- window.__dumber_showZoomToast = function(level) { window.__dumber.toast.zoom(level); };
850-
851- // DOM zoom helpers (optional native override)
852- if (typeof window.__dumber_dom_zoom_level !== 'number') {
853- window.__dumber_dom_zoom_level = __dumberInitialZoom;
854- }
855- const __dumberApplyZoomStyles = function(node, level) {
856- if (!node) return;
857- if (Math.abs(level - 1.0) < 1e-6) {
858- node.style.removeProperty('zoom');
859- node.style.removeProperty('transform');
860- node.style.removeProperty('transform-origin');
861- node.style.removeProperty('width');
862- node.style.removeProperty('min-width');
863- node.style.removeProperty('height');
864- node.style.removeProperty('min-height');
865- return;
866- }
867- const scale = level;
868- const inversePercent = 100 / scale;
869- const widthValue = inversePercent + '%';
870- node.style.removeProperty('zoom');
871- node.style.transform = 'scale(' + scale + ')';
872- node.style.transformOrigin = '0 0';
873- node.style.width = widthValue;
874- node.style.minWidth = widthValue;
875- node.style.minHeight = '100%';
876- };
877- window.__dumber_applyDomZoom = function(level) {
878- try {
879- window.__dumber_dom_zoom_level = level;
880- window.__dumber_dom_zoom_seed = level;
881- __dumberApplyZoomStyles(document.documentElement, level);
882- if (document.body) {
883- __dumberApplyZoomStyles(document.body, level);
884- }
885- } catch (e) {
886- console.error('[dumber] DOM zoom error', e);
887- }
888- };
889- // Apply immediately so first paint uses the desired zoom, then reapply when body exists.
890- window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
891- if (!document.body) {
892- document.addEventListener('DOMContentLoaded', function() {
893- if (typeof window.__dumber_dom_zoom_level === 'number') {
894- window.__dumber_applyDomZoom(window.__dumber_dom_zoom_level);
895- }
896- }, { once: true });
897- }
898-
899- // Omnibox suggestions bridge (with queue before ready)
900- let __omniboxQueue = [];
901- let __omniboxReady = false;
902- function __omniboxDispatch(suggestions) {
903- try {
904- document.dispatchEvent(new CustomEvent('dumber:omnibox:suggestions', { detail: { suggestions } }));
905- // Legacy compatibility event name
906- document.dispatchEvent(new CustomEvent('dumber:omnibox-suggestions', { detail: { suggestions } }));
907- } catch (e) { /* ignore */ }
908- }
909- window.__dumber_omnibox_suggestions = function(suggestions) {
910- if (__omniboxReady) {
911- __omniboxDispatch(suggestions);
912- } else {
913- try { __omniboxQueue.push(suggestions); } catch (_) { /* ignore */ }
914- }
915- };
916- document.addEventListener('dumber:omnibox-ready', function() {
917- __omniboxReady = true;
918- if (__omniboxQueue && __omniboxQueue.length) {
919- const items = __omniboxQueue.slice();
920- __omniboxQueue.length = 0;
921- for (const s of items) __omniboxDispatch(s);
922- }
923- });
924- // Unified omnibox API for page-world callers
925- window.__dumber.omnibox = window.__dumber.omnibox || {
926- suggestions: function(suggestions) { window.__dumber_omnibox_suggestions(suggestions); }
927- };
928- } catch (e) { console.warn('[dumber] unified bridge init failed', e); } })();`
929 769
930 var registerSchemeOnce sync.Once770 var registerSchemeOnce sync.Once
931 771
@@ -1071,9 +911,9 @@ type WebView struct {
1071 titleHandler func(title string)911 titleHandler func(title string)
1072 uriHandler func(uri string)912 uriHandler func(uri string)
1073 zoomHandler func(level float64)913 zoomHandler func(level float64)
1074- popupHandler func(string) *WebView914+ popupHandler func(string) *WebView
1075- closeHandler func()915+ closeHandler func()
1076- memStats *memoryStats916+ memStats *memoryStats
1077 gcTicker *time.Ticker917 gcTicker *time.Ticker
1078 gcDone chan struct{}918 gcDone chan struct{}
1079 tlsExceptions map[string]bool // host -> whether user allowed certificate exception919 tlsExceptions map[string]bool // host -> whether user allowed certificate exception
@@ -1081,6 +921,13 @@ type WebView struct {
1081 domZoomSeed float64921 domZoomSeed float64
1082 domBridgeScript *C.WebKitUserScript922 domBridgeScript *C.WebKitUserScript
1083 wasReparented bool // Track if this WebView has been reparented923 wasReparented bool // Track if this WebView has been reparented
924+
925+ // Window type detection and relationships
926+ windowType WindowType
927+ windowFeatures *WindowFeatures
928+ windowTypeCallback func(WindowType, *WindowFeatures)
929+ parentWebView *WebView
930+ isRelated bool
1084 }931 }
1085 932
1086 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.933 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -1237,6 +1084,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
1237 v.id = nextViewID()1084 v.id = nextViewID()
1238 registerView(v.id, v)1085 registerView(v.id, v)
1239 C.connect_policy_handler(v.native.wv, C.ulong(v.id))1086 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1087+ ConnectCreateSignal(v) // Connect create signal for window.open support
1240 1088
1241 // Register with global memory manager if monitoring is enabled1089 // Register with global memory manager if monitoring is enabled
1242 if cfg.Memory.EnableMemoryMonitoring {1090 if cfg.Memory.EnableMemoryMonitoring {
@@ -1446,9 +1294,124 @@ func NewWebView(cfg *Config) (*WebView, error) {
1446 }1294 }
1447 }1295 }
1448 1296
1297+ // Setup console message logging
1298+ v.setupConsoleLogging()
1299+
1449 return v, nil1300 return v, nil
1450 }1301 }
1451 1302
1303+// setupConsoleLogging enables console message capture for the WebView
1304+func (v *WebView) setupConsoleLogging() {
1305+ if v.native == nil || v.native.wv == nil {
1306+ return
1307+ }
1308+
1309+ // Check if console capture is enabled in config
1310+ cfg := config.Get()
1311+ if !cfg.Logging.CaptureConsole {
1312+ // Disable console messages to stdout (always, to keep backend clean)
1313+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1314+ C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1315+ }
1316+ return
1317+ }
1318+
1319+ // If console capture is enabled, disable stdout and enable our capture
1320+ if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
1321+ C.webkit_settings_set_enable_write_console_messages_to_stdout(settings, C.gboolean(0))
1322+ log.Printf("[webkit] Console message capture enabled - redirecting to console.log file")
1323+ }
1324+
1325+ // TODO: Implement JavaScript injection for console capture
1326+ // This will inject code to intercept console.log/error/warn and send to Go
1327+}
1328+
1329+// setupGlobalConsoleCapture sets up a global stdout filter to capture console messages
1330+func setupGlobalConsoleCapture() {
1331+ // This should be called once globally, not per WebView
1332+ logging.StartConsoleCapture()
1333+}
1334+
1335+// injectConsoleCapture injects JavaScript to capture console messages
1336+func (v *WebView) injectConsoleCapture() {
1337+ consoleScript := `
1338+(function() {
1339+ const originalLog = console.log;
1340+ const originalWarn = console.warn;
1341+ const originalError = console.error;
1342+ const originalInfo = console.info;
1343+ const originalDebug = console.debug;
1344+
1345+ function sendToGo(level, args) {
1346+ const message = args.map(arg =>
1347+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1348+ ).join(' ');
1349+
1350+ const formattedMessage = '[' + level + '] ' + window.location.href + ' ' + message;
1351+
1352+ // Send to Go via user script message
1353+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumberBridge) {
1354+ window.webkit.messageHandlers.dumberBridge.postMessage({
1355+ type: 'console',
1356+ data: formattedMessage
1357+ });
1358+ }
1359+ }
1360+
1361+ console.log = function(...args) {
1362+ originalLog.apply(console, args);
1363+ sendToGo('LOG', args);
1364+ };
1365+
1366+ console.warn = function(...args) {
1367+ originalWarn.apply(console, args);
1368+ sendToGo('WARN', args);
1369+ };
1370+
1371+ console.error = function(...args) {
1372+ originalError.apply(console, args);
1373+ sendToGo('ERROR', args);
1374+ };
1375+
1376+ console.info = function(...args) {
1377+ originalInfo.apply(console, args);
1378+ sendToGo('INFO', args);
1379+ };
1380+
1381+ console.debug = function(...args) {
1382+ originalDebug.apply(console, args);
1383+ sendToGo('DEBUG', args);
1384+ };
1385+})();
1386+`
1387+ _ = v.InjectScript(consoleScript)
1388+}
1389+
1390+// handleConsoleMessage processes console messages from JavaScript
1391+func (v *WebView) handleConsoleMessage(payload string) {
1392+ // Parse the message payload to check if it's a console message
1393+ var msg struct {
1394+ Type string `json:"type"`
1395+ Data string `json:"data"`
1396+ }
1397+
1398+ if err := json.Unmarshal([]byte(payload), &msg); err != nil {
1399+ return
1400+ }
1401+
1402+ if msg.Type == "console" {
1403+ // Send to logging system with [CONSOLE] tag
1404+ logging.CaptureWebKitLog(msg.Data)
1405+ }
1406+}
1407+
1408+// startConsoleCapture starts capturing stdout to filter console messages
1409+func (v *WebView) startConsoleCapture() {
1410+ // This is a simplified approach - we'll rely on the existing OutputCapture
1411+ // mechanism in internal/logging/capture.go to handle stdout capture
1412+ // and filter for console messages in the log processing
1413+}
1414+
1452 // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing1415 // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing
1453 func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) {1416 func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) {
1454 if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil {1417 if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil {
@@ -1532,6 +1495,7 @@ func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error)
1532 v.id = nextViewID()1495 v.id = nextViewID()
1533 registerView(v.id, v)1496 registerView(v.id, v)
1534 C.connect_policy_handler(v.native.wv, C.ulong(v.id))1497 C.connect_policy_handler(v.native.wv, C.ulong(v.id))
1498+ ConnectCreateSignal(v) // Connect create signal for window.open support
1535 1499
1536 // Register with memory manager if enabled1500 // Register with memory manager if enabled
1537 if cfg.Memory.EnableMemoryMonitoring {1501 if cfg.Memory.EnableMemoryMonitoring {
@@ -1744,6 +1708,46 @@ func (w *WebView) dispatchPopupRequest(uri string) *WebView {
1744 return nil1708 return nil
1745 }1709 }
1746 1710
1711+// CreateRelatedView creates a WebView that shares process/context with this view
1712+func (w *WebView) CreateRelatedView() *WebView {
1713+ if w == nil || w.native == nil || w.native.wv == nil {
1714+ return nil
1715+ }
1716+ // Build a minimal config inheriting from current view
1717+ cfg := &Config{
1718+ Assets: w.config.Assets,
1719+ ZoomDefault: w.config.ZoomDefault,
1720+ EnableDeveloperExtras: w.config.EnableDeveloperExtras,
1721+ DataDir: w.config.DataDir,
1722+ CacheDir: w.config.CacheDir,
1723+ APIToken: w.config.APIToken,
1724+ DefaultSansFont: w.config.DefaultSansFont,
1725+ DefaultSerifFont: w.config.DefaultSerifFont,
1726+ DefaultMonospaceFont: w.config.DefaultMonospaceFont,
1727+ DefaultFontSize: w.config.DefaultFontSize,
1728+ Rendering: w.config.Rendering,
1729+ UseDomZoom: w.config.UseDomZoom,
1730+ VideoAcceleration: w.config.VideoAcceleration,
1731+ Memory: w.config.Memory,
1732+ CodecPreferences: w.config.CodecPreferences,
1733+ CreateWindow: false,
1734+ }
1735+ related, err := NewWebViewWithRelated(cfg, w)
1736+ if err != nil {
1737+ log.Printf("[webkit] CreateRelatedView failed: %v", err)
1738+ return nil
1739+ }
1740+ return related
1741+}
1742+
1743+// OnWindowTypeDetected registers a callback invoked when window type is detected
1744+func (w *WebView) OnWindowTypeDetected(callback func(WindowType, *WindowFeatures)) {
1745+ if w == nil {
1746+ return
1747+ }
1748+ w.windowTypeCallback = callback
1749+}
1750+
1747 // GetNativePointer returns the native WebView pointer for CGO callbacks1751 // GetNativePointer returns the native WebView pointer for CGO callbacks
1748 func (w *WebView) GetNativePointer() unsafe.Pointer {1752 func (w *WebView) GetNativePointer() unsafe.Pointer {
1749 if w != nil && w.native != nil && w.native.wv != nil {1753 if w != nil && w.native != nil && w.native.wv != nil {
@@ -2027,10 +2031,26 @@ func clampDomZoom(level float64) float64 {
2027 return level2031 return level
2028 }2032 }
2029 2033
2030-func buildDomBridgeScript(initialZoom float64) string {2034+func buildDomBridgeScript(cfg *Config, initialZoom float64) string {
2035+ if cfg == nil || cfg.Assets == nil {
2036+ log.Printf("[webkit] Config or assets not available for main-world script")
2037+ return ""
2038+ }
2039+
2040+ // Load the compiled main-world script
2041+ scriptBytes, err := cfg.Assets.ReadFile("assets/gui/main-world.min.js")
2042+ if err != nil {
2043+ log.Printf("[webkit] Failed to load main-world script: %v", err)
2044+ return ""
2045+ }
2046+
2047+ // Replace the zoom placeholder with the actual value
2031 zoom := clampDomZoom(initialZoom)2048 zoom := clampDomZoom(initialZoom)
2032 zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64)2049 zoomStr := strconv.FormatFloat(zoom, 'f', -1, 64)
2033- return strings.ReplaceAll(domBridgeTemplate, "__DOM_ZOOM_DEFAULT__", zoomStr)2050+ script := strings.ReplaceAll(string(scriptBytes), "__DOM_ZOOM_DEFAULT__", zoomStr)
2051+
2052+ log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2053+ return script
2034 }2054 }
2035 2055
2036 func (w *WebView) UsesDomZoom() bool {2056 func (w *WebView) UsesDomZoom() bool {
@@ -2050,7 +2070,7 @@ func (w *WebView) installDomBridgeScript() {
2050 w.domBridgeScript = nil2070 w.domBridgeScript = nil
2051 }2071 }
2052 2072
2053- source := buildDomBridgeScript(w.domZoomSeed)2073+ source := buildDomBridgeScript(w.config, w.domZoomSeed)
2054 cBridge := C.CString(source)2074 cBridge := C.CString(source)
2055 defer C.free(unsafe.Pointer(cBridge))2075 defer C.free(unsafe.Pointer(cBridge))
2056 2076
@@ -2076,8 +2096,11 @@ func (w *WebView) SeedDomZoom(level float64) {
2076 return2096 return
2077 }2097 }
2078 w.domZoomSeed = seed2098 w.domZoomSeed = seed
2099+ // Instead of re-injecting the entire script, just update the zoom level
2079 w.RunOnMainThread(func() {2100 w.RunOnMainThread(func() {
2080- w.installDomBridgeScript()2101+ zoomStr := strconv.FormatFloat(seed, 'f', -1, 64)
2102+ script := fmt.Sprintf("if (window.__dumber_applyDomZoom) { window.__dumber_applyDomZoom(%s); }", zoomStr)
2103+ w.InjectScript(script)
2081 })2104 })
2082 }2105 }
2083 2106
@@ -2212,6 +2235,13 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn
2212 return C.FALSE // Don't proceed2235 return C.FALSE // Don't proceed
2213 }2236 }
2214 2237
2238+//export goHandleConsoleMessage
2239+//export goHandleConsoleMessage
2240+func goHandleConsoleMessage(message *C.char) {
2241+ messageText := C.GoString(message)
2242+ logging.CaptureWebKitLog(messageText)
2243+}
2244+
2215 // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability2245 // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
2216 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {2246 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
2217 if w == nil || w.native == nil || w.native.win == nil {2247 if w == nil || w.native == nil || w.native.win == nil {
@@ -2530,3 +2560,8 @@ func (w *WebView) detectAndLogRenderingBackend() {
2530 w.InjectScript(detectionScript)2560 w.InjectScript(detectionScript)
2531 }()2561 }()
2532 }2562 }
2563+
2564+// SetWindowFeatures sets the window features for this WebView
2565+func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
2566+ w.windowFeatures = features
2567+}
A pkg/webkit/window_handler_cgo.go +72 -0
@@ -0,0 +1,72 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+/*
6+#cgo pkg-config: gtk4 webkitgtk-6.0
7+#include <gtk/gtk.h>
8+#include <gdk/gdk.h>
9+#include <webkit/webkit.h>
10+
11+// Declarations for Go callbacks
12+extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri);
13+
14+// Handle create signal for popup windows - essential for window.open() support
15+static GtkWidget* on_create_web_view(WebKitWebView *web_view,
16+ WebKitNavigationAction *navigation_action,
17+ gpointer user_data) {
18+ WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action);
19+ const char* uri = webkit_uri_request_get_uri(request);
20+
21+ printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL");
22+
23+ unsigned long parent_view_id = (unsigned long)user_data;
24+
25+ // Let Go handler create WebView and return it like Epiphany does
26+ GtkWidget* new_webview = goHandleCreateWebView(parent_view_id, (char*)uri);
27+
28+ if (new_webview) {
29+ printf("[webkit-create] Go handler provided WebView - using workspace pane\n");
30+ return new_webview;
31+ } else {
32+ printf("[webkit-create] Go handler declined - allowing native popup window\n");
33+ return NULL; // Let WebKit create native popup window
34+ }
35+}
36+
37+static void connect_create_signal(WebKitWebView* web_view, unsigned long id) {
38+ if (!web_view) return;
39+ g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0);
40+ printf("[webkit-debug] create-web-view callback ENABLED with WebKit-created WebViews\n");
41+}
42+*/
43+import "C"
44+import (
45+ "log"
46+ "unsafe"
47+)
48+
49+//export goHandleCreateWebView
50+func goHandleCreateWebView(parentID C.ulong, curi *C.char) *C.GtkWidget {
51+ uri := ""
52+ if curi != nil {
53+ uri = C.GoString(curi)
54+ }
55+
56+ log.Printf("[webkit] Create signal bypassed: URI=%s (handled by JavaScript bypass)", uri)
57+
58+ // Always return NULL - window.open is now handled entirely by JavaScript bypass
59+ // This prevents duplicate pane creation from WebKit's create signal
60+ return nil
61+}
62+
63+// ConnectCreateSignal connects the create signal to a WebView
64+func ConnectCreateSignal(view *WebView) {
65+ if view == nil || view.native == nil || view.native.view == nil {
66+ log.Printf("[webkit] ConnectCreateSignal: invalid WebView")
67+ return
68+ }
69+
70+ webkitView := (*C.WebKitWebView)(unsafe.Pointer(view.native.view))
71+ C.connect_create_signal(webkitView, C.ulong(view.id))
72+}
@@ -0,0 +1,72 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+/*
6+#cgo pkg-config: gtk4 webkitgtk-6.0
7+#include <gtk/gtk.h>
8+#include <gdk/gdk.h>
9+#include <webkit/webkit.h>
10+
11+// Declarations for Go callbacks
12+extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri);
13+
14+// Handle create signal for popup windows - essential for window.open() support
15+static GtkWidget* on_create_web_view(WebKitWebView *web_view,
16+ WebKitNavigationAction *navigation_action,
17+ gpointer user_data) {
18+ WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action);
19+ const char* uri = webkit_uri_request_get_uri(request);
20+
21+ printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL");
22+
23+ unsigned long parent_view_id = (unsigned long)user_data;
24+
25+ // Let Go handler create WebView and return it like Epiphany does
26+ GtkWidget* new_webview = goHandleCreateWebView(parent_view_id, (char*)uri);
27+
28+ if (new_webview) {
29+ printf("[webkit-create] Go handler provided WebView - using workspace pane\n");
30+ return new_webview;
31+ } else {
32+ printf("[webkit-create] Go handler declined - allowing native popup window\n");
33+ return NULL; // Let WebKit create native popup window
34+ }
35+}
36+
37+static void connect_create_signal(WebKitWebView* web_view, unsigned long id) {
38+ if (!web_view) return;
39+ g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0);
40+ printf("[webkit-debug] create-web-view callback ENABLED with WebKit-created WebViews\n");
41+}
42+*/
43+import "C"
44+import (
45+ "log"
46+ "unsafe"
47+)
48+
49+//export goHandleCreateWebView
50+func goHandleCreateWebView(parentID C.ulong, curi *C.char) *C.GtkWidget {
51+ uri := ""
52+ if curi != nil {
53+ uri = C.GoString(curi)
54+ }
55+
56+ log.Printf("[webkit] Create signal bypassed: URI=%s (handled by JavaScript bypass)", uri)
57+
58+ // Always return NULL - window.open is now handled entirely by JavaScript bypass
59+ // This prevents duplicate pane creation from WebKit's create signal
60+ return nil
61+}
62+
63+// ConnectCreateSignal connects the create signal to a WebView
64+func ConnectCreateSignal(view *WebView) {
65+ if view == nil || view.native == nil || view.native.view == nil {
66+ log.Printf("[webkit] ConnectCreateSignal: invalid WebView")
67+ return
68+ }
69+
70+ webkitView := (*C.WebKitWebView)(unsafe.Pointer(view.native.view))
71+ C.connect_create_signal(webkitView, C.ulong(view.id))
72+}
A pkg/webkit/window_types.go +23 -0
@@ -0,0 +1,23 @@
1+package webkit
2+
3+// WindowType indicates how a new WebView should be treated
4+type WindowType int
5+
6+const (
7+ // WindowTypeTab represents an independent WebView (future: tab)
8+ WindowTypeTab WindowType = iota
9+ // WindowTypePopup represents a related WebView (shares process/context)
10+ WindowTypePopup
11+ // WindowTypeUnknown indicates type not detected yet
12+ WindowTypeUnknown
13+)
14+
15+// WindowFeatures describes the window features detected from WebKitWindowProperties
16+type WindowFeatures struct {
17+ Width int
18+ Height int
19+ ToolbarVisible bool
20+ LocationbarVisible bool
21+ MenubarVisible bool
22+ Resizable bool
23+}
@@ -0,0 +1,23 @@
1+package webkit
2+
3+// WindowType indicates how a new WebView should be treated
4+type WindowType int
5+
6+const (
7+ // WindowTypeTab represents an independent WebView (future: tab)
8+ WindowTypeTab WindowType = iota
9+ // WindowTypePopup represents a related WebView (shares process/context)
10+ WindowTypePopup
11+ // WindowTypeUnknown indicates type not detected yet
12+ WindowTypeUnknown
13+)
14+
15+// WindowFeatures describes the window features detected from WebKitWindowProperties
16+type WindowFeatures struct {
17+ Width int
18+ Height int
19+ ToolbarVisible bool
20+ LocationbarVisible bool
21+ MenubarVisible bool
22+ Resizable bool
23+}
M pkg/webkit/workspace_widgets_cgo.go +8 -0
@@ -306,6 +306,14 @@ func PanedSetResizeEnd(paned uintptr, resize bool) {
306306 C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
307307 }
308308
309+// PanedSetPosition sets the pixel position of the divider.
310+func PanedSetPosition(paned uintptr, pos int) {
311+ if paned == 0 {
312+ return
313+ }
314+ C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos))
315+}
316+
309317 // WidgetUnparent detaches a widget from its current parent.
310318 func WidgetUnparent(widget uintptr) {
311319 if widget == 0 {
@@ -306,6 +306,14 @@ func PanedSetResizeEnd(paned uintptr, resize bool) {
306 C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))306 C.gtk_paned_set_resize_end_child((*C.GtkPaned)(unsafe.Pointer(paned)), goBool(resize))
307 }307 }
308 308
309+// PanedSetPosition sets the pixel position of the divider.
310+func PanedSetPosition(paned uintptr, pos int) {
311+ if paned == 0 {
312+ return
313+ }
314+ C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos))
315+}
316+
309 // WidgetUnparent detaches a widget from its current parent.317 // WidgetUnparent detaches a widget from its current parent.
310 func WidgetUnparent(widget uintptr) {318 func WidgetUnparent(widget uintptr) {
311 if widget == 0 {319 if widget == 0 {
M pkg/webkit/workspace_widgets_stub.go +3 -0
@@ -93,6 +93,9 @@ func PanedSetResizeStart(paned uintptr, resize bool) {}
9393
9494 func PanedSetResizeEnd(paned uintptr, resize bool) {}
9595
96+// PanedSetPosition is a no-op in stub builds.
97+func PanedSetPosition(paned uintptr, pos int) {}
98+
9699 func WidgetUnparent(widget uintptr) {}
97100
98101 func WidgetSetHExpand(widget uintptr, expand bool) {}
@@ -93,6 +93,9 @@ func PanedSetResizeStart(paned uintptr, resize bool) {}
93 93
94 func PanedSetResizeEnd(paned uintptr, resize bool) {}94 func PanedSetResizeEnd(paned uintptr, resize bool) {}
95 95
96+// PanedSetPosition is a no-op in stub builds.
97+func PanedSetPosition(paned uintptr, pos int) {}
98+
96 func WidgetUnparent(widget uintptr) {}99 func WidgetUnparent(widget uintptr) {}
97 100
98 func WidgetSetHExpand(widget uintptr, expand bool) {}101 func WidgetSetHExpand(widget uintptr, expand bool) {}

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