Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import { WebView } from "./web_view.js";
4import { LayoutManager } from "./layout_manager.js";
5import { MobileLayoutManager } from "./mobile_layout_manager.js";
6import "./system_menu.js";
7import "./mobile_action_bar.js";
8import "./mobile_notification_sheet.js";
9import "./mobile_overview.js";
10import "./mobile_radial_menu.js";
11
12let NEW_FRAME_DEFAULT_URL = navigator.servo.getStringPreference(
13 "browserhtml.start_url",
14);
15
16// Unique ID for this browser window (used for cross-window communication)
17const windowId = `browser-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18
19navigator.embedder.addEventListener("servoerror", (event) => {
20 console.error(`[Servo] ${event.type} ${event.detail}`);
21});
22
23// ============================================================================
24// Global State and Event Handlers
25// ============================================================================
26
27let layoutManager = null;
28let systemMenu = null;
29let notificationPanel = null;
30let mobileActionBar = null;
31let mobileNotificationSheet = null;
32let mobileOverview = null;
33let mobileRadialMenu = null;
34let isMobileMode = false;
35
36// Detect if we should use mobile mode
37function detectMobileMode() {
38 // Check if mobile simulation is forced via preference
39 const mobileSimulation = navigator.servo.getBoolPreference(
40 "browserhtml.mobile_simulation",
41 );
42 if (mobileSimulation) {
43 return true;
44 }
45
46 // maxTouchPoints is not yet supported on Servo
47 // const isTouchDevice = navigator.maxTouchPoints > 0;
48 const isNarrowScreen = window.matchMedia("(max-width: 768px)").matches;
49 return isNarrowScreen;
50}
51
52// Create the appropriate layout manager based on device type
53function createLayoutManager(rootElement, webViewBuilder) {
54 isMobileMode = detectMobileMode();
55
56 if (isMobileMode) {
57 document.body.classList.add("mobile-mode");
58 return new MobileLayoutManager(rootElement, webViewBuilder);
59 } else {
60 document.body.classList.remove("mobile-mode");
61 return new LayoutManager(rootElement, webViewBuilder);
62 }
63}
64
65navigator.embedder.addEventListener("preferencechanged", (event) => {
66 console.log(
67 `[System PrefChanged] ${event.detail.name} : ${event.detail.value}`,
68 );
69 if (event.detail.name == "browserhtml.start_url") {
70 NEW_FRAME_DEFAULT_URL = event.detail.value;
71 }
72});
73
74// Notification state
75const MAX_NOTIFICATIONS = 50;
76let notifications = [];
77
78function updateNotificationBadge() {
79 const badge = document.getElementById("notifications-badge");
80 if (!badge) {
81 return;
82 }
83
84 const count = notifications.length;
85 if (count > 0) {
86 badge.textContent =
87 count > MAX_NOTIFICATIONS ? `${MAX_NOTIFICATIONS}+` : count;
88 badge.classList.remove("hidden");
89 } else {
90 badge.classList.add("hidden");
91 }
92}
93
94function addNotification(notification) {
95 // Add timestamp if not present
96 if (!notification.timestamp) {
97 notification.timestamp = Date.now();
98 }
99
100 // Generate a unique ID if not present
101 if (!notification.id) {
102 notification.id = `notif-${Date.now()}-${Math.random()
103 .toString(36)
104 .slice(2)}`;
105 }
106
107 // Check for same-tag replacement
108 if (notification.tag) {
109 const existingIndex = notifications.findIndex(
110 (n) => n.tag === notification.tag,
111 );
112 if (existingIndex !== -1) {
113 // Replace existing notification with same tag
114 notifications[existingIndex] = notification;
115 } else {
116 // Add to front
117 notifications.unshift(notification);
118 }
119 } else {
120 // No tag, just add to front
121 notifications.unshift(notification);
122 }
123
124 // Enforce max limit (FIFO)
125 if (notifications.length > MAX_NOTIFICATIONS) {
126 notifications = notifications.slice(0, MAX_NOTIFICATIONS);
127 }
128
129 // Update panel and badge
130 if (notificationPanel) {
131 notificationPanel.notifications = [...notifications];
132 }
133 updateNotificationBadge();
134}
135
136function dismissNotification(notification) {
137 const index = notifications.findIndex((n) => n.id === notification.id);
138 if (index !== -1) {
139 notifications.splice(index, 1);
140 if (notificationPanel) {
141 notificationPanel.notifications = [...notifications];
142 }
143 updateNotificationBadge();
144 }
145}
146
147function clearAllNotifications() {
148 notifications = [];
149 if (notificationPanel) {
150 notificationPanel.notifications = [];
151 }
152 updateNotificationBadge();
153}
154
155window.servo = {
156 /**
157 * Called by Servo when an embedded webview has opened a new embedded webview
158 * via window.open() or target="_blank" link, and the constellation has already
159 * created the webview. The iframe should adopt the pre-created webview.
160 */
161 adoptNewWebView: function (url, webviewId, browsingContextId, pipelineId) {
162 console.log(
163 "servo.adoptNewWebView:",
164 url,
165 webviewId,
166 browsingContextId,
167 pipelineId,
168 );
169
170 const attrs = {
171 "adopt-webview-id": webviewId,
172 "adopt-browsing-context-id": browsingContextId,
173 "adopt-pipeline-id": pipelineId,
174 };
175 try {
176 const webView = new WebView(url, "", attrs);
177 layoutManager.addWebView(webView);
178 } catch (e) {
179 console.error(e);
180 }
181 },
182
183 openKeyboard: function (inputType, currentValue, position) {
184 console.log(`servo.openKeyboard: ${inputType} ${currentValue} ${position}`);
185 openVirtualKeyboard({
186 inputType,
187 currentValue,
188 placeholder: "",
189 position,
190 });
191 },
192
193 closeEmbedderControl: function (controlId) {
194 console.log(`servo.closeEmbedderControl ${controlId}`);
195 // TODO: proper control tracking (add controlId to openKeyboard).
196 closeVirtualKeyboard();
197 },
198};
199
200function createNewView() {
201 const webView = new WebView(NEW_FRAME_DEFAULT_URL, "", {});
202 layoutManager.addWebView(webView);
203}
204
205function switchToHomescreen() {
206 if (layoutManager.homescreenWebviewId) {
207 layoutManager.setActiveWebView(layoutManager.homescreenWebviewId);
208 }
209}
210
211function openSettingsView() {
212 const settingsView = new WebView(
213 "http://settings.localhost:8888/index.html",
214 "Settings",
215 {},
216 );
217 layoutManager.addWebView(settingsView);
218}
219
220// Update the mobile tab overview with current tabs
221function updateTabOverviewData() {
222 if (!mobileOverview || !layoutManager) {
223 return;
224 }
225
226 // Use getOverviewTabs() if available (excludes homescreen in mobile mode)
227 let tabs;
228 if (layoutManager.getOverviewTabs) {
229 tabs = layoutManager.getOverviewTabs();
230 } else {
231 tabs = [];
232 for (const [id, entry] of layoutManager.webviews) {
233 tabs.push({
234 id: id,
235 title: entry.webview.title || "Untitled",
236 favicon: entry.webview.favicon || "",
237 screenshotUrl: entry.webview.screenshotUrl || null,
238 });
239 }
240 }
241
242 mobileOverview.tabs = tabs;
243 mobileOverview.activeTabId = layoutManager.activeWebviewId;
244
245 // Also update notification sheet tab count
246 if (mobileNotificationSheet) {
247 mobileNotificationSheet.tabCount = tabs.length;
248 }
249}
250
251// Handle radial menu actions
252function handleRadialMenuAction(detail) {
253 const { action, originX, originY } = detail;
254
255 switch (action) {
256 case "new-view":
257 createNewView();
258 break;
259 case "home":
260 switchToHomescreen();
261 break;
262 case "back":
263 layoutManager.goBack();
264 break;
265 case "forward":
266 layoutManager.goForward();
267 break;
268 case "reload":
269 layoutManager.reload();
270 break;
271 case "close-view":
272 if (layoutManager.activeWebviewId) {
273 layoutManager.removeWebView(layoutManager.activeWebviewId);
274 }
275 break;
276 case "overview":
277 layoutManager.showOverview();
278 break;
279 case "settings":
280 openSettingsView();
281 break;
282 case "context-menu":
283 // Show the full context menu from the radial menu
284 const entry = layoutManager.getActiveEntry();
285 if (entry?.webview?.showPendingContextMenu) {
286 entry.webview.showPendingContextMenu();
287 }
288 break;
289 default:
290 console.log(`[RadialMenu] Unsupported action: ${action}`);
291 }
292}
293
294// Global keyboard shortcuts
295
296// New tab
297Mousetrap.bindGlobal("mod+t", (e) => {
298 createNewView();
299 return false;
300});
301
302// New OS Window
303Mousetrap.bindGlobal("mod+n", (e) => {
304 console.log("Requesting new OS window");
305 navigator.embedder.openNewOSWindow("http://system.localhost:8888/index.html");
306 return false;
307});
308
309Mousetrap.bindGlobal("mod+f", (e) => {
310 console.log("Requesting new floating search window");
311 navigator.embedder.openNewOSWindow(
312 "http://system.localhost:8888/search.html",
313 "notitle,ontop",
314 );
315 return false;
316});
317
318// Close current tab
319Mousetrap.bindGlobal("mod+w", (e) => {
320 if (layoutManager.activeWebviewId) {
321 layoutManager.removeWebView(layoutManager.activeWebviewId);
322 }
323 return false;
324});
325
326// Close current OS Window
327Mousetrap.bindGlobal("mod+shift+w", (e) => {
328 console.log("Closing current OS window");
329 navigator.embedder.closeCurrentOSWindow();
330 return false;
331});
332
333// Exit application
334Mousetrap.bindGlobal("mod+q", (e) => {
335 console.log("Bye bye");
336 navigator.embedder.exit();
337 return false;
338});
339
340// Whole system UI reload
341Mousetrap.bindGlobal("mod+shift+r", (e) => {
342 window.location.reload();
343 return false;
344});
345
346// Navigate to next panel (Cmd+] or Ctrl+Tab)
347Mousetrap.bindGlobal(["mod+]", "ctrl+tab"], (e) => {
348 layoutManager.nextPanel();
349 return false;
350});
351
352// Navigate to previous panel (Cmd+[ or Ctrl+Shift+Tab)
353Mousetrap.bindGlobal(["mod+[", "ctrl+shift+tab"], (e) => {
354 layoutManager.prevPanel();
355 return false;
356});
357
358// Toggle overview mode (Cmd+E or Ctrl+E)
359Mousetrap.bindGlobal("mod+e", (e) => {
360 layoutManager.toggleOverview();
361 return false;
362});
363
364// Exit overview mode or close system menu with Escape
365Mousetrap.bindGlobal("escape", (e) => {
366 if (mobileRadialMenu && mobileRadialMenu.open) {
367 mobileRadialMenu.hide();
368 return false;
369 }
370 if (mobileOverview && mobileOverview.open) {
371 mobileOverview.open = false;
372 return false;
373 }
374 if (mobileNotificationSheet && mobileNotificationSheet.open) {
375 mobileNotificationSheet.open = false;
376 return false;
377 }
378 if (mobileActionBar && mobileActionBar.open) {
379 mobileActionBar.hide();
380 return false;
381 }
382 if (notificationPanel && notificationPanel.open) {
383 notificationPanel.open = false;
384 return false;
385 }
386 if (systemMenu && systemMenu.open) {
387 systemMenu.open = false;
388 return false;
389 }
390 if (layoutManager.overviewMode) {
391 layoutManager.hideOverview();
392 return false;
393 }
394});
395
396// Open URL bar on active webview (Cmd+L or Ctrl+L)
397Mousetrap.bindGlobal("mod+l", (e) => {
398 if (layoutManager.activeWebviewId) {
399 const entry = layoutManager.webviews.get(layoutManager.activeWebviewId);
400 if (entry) {
401 entry.webview.openUrlBar(e);
402 }
403 }
404 return false;
405});
406
407async function openVirtualKeyboard(detail) {
408 // Check if virtual keyboard is enabled.
409 if (
410 !navigator.servo.getBoolPreference(
411 "browserhtml.ime_virtual_keyboard_enabled",
412 )
413 ) {
414 return;
415 }
416
417 // Load the keyboard if needed.
418 let keyboardFrame = document.getElementById("keyboard-frame");
419 if (!keyboardFrame) {
420 keyboardFrame = document.createElement("iframe");
421 keyboardFrame.setAttribute("embed", true);
422 keyboardFrame.setAttribute("hidefocus", true);
423 keyboardFrame.setAttribute("id", "keyboard-frame");
424 keyboardFrame.setAttribute("src", "//keyboard.localhost:8888/index.html");
425 document.getElementById("footer").append(keyboardFrame);
426 // Wait for the frame to be loaded.
427 let loaded = new Promise((resolve) => {
428 keyboardFrame.addEventListener("embedloadstatuschange", (event) => {
429 if (event.detail == "complete") {
430 resolve();
431 }
432 });
433 });
434 await loaded;
435 }
436
437 console.log("[VirtualKeyboard] Show event received:", detail);
438 const footer = document.getElementById("footer");
439 footer.classList.add("visible");
440 document.body.classList.add("keyboard-open");
441
442 // Send input context to keyboard iframe
443
444 if (keyboardFrame.contentWindow) {
445 keyboardFrame.contentWindow.postMessage(
446 {
447 type: "show",
448 inputType: detail.inputType,
449 currentValue: detail.currentValue,
450 placeholder: detail.placeholder,
451 },
452 "*",
453 );
454 }
455}
456
457function closeVirtualKeyboard() {
458 if (
459 !navigator.servo.getBoolPreference(
460 "browserhtml.ime_virtual_keyboard_enabled",
461 )
462 ) {
463 return;
464 }
465
466 const footer = document.getElementById("footer");
467 footer.classList.remove("visible");
468 document.body.classList.remove("keyboard-open");
469
470 // Notify keyboard iframe
471 const keyboardFrame = document.getElementById("keyboard-frame");
472 if (keyboardFrame.contentWindow) {
473 keyboardFrame.contentWindow.postMessage({ type: "hide" }, "*");
474 }
475}
476
477// Initialize on DOM ready
478document.addEventListener("DOMContentLoaded", () => {
479 layoutManager = createLayoutManager(document.getElementById("root"), () => {
480 return new WebView(NEW_FRAME_DEFAULT_URL, "", {});
481 });
482
483 // Setup mobile action bar if in mobile mode
484 if (isMobileMode) {
485 // Create homescreen webview first
486 const homescreenUrl = navigator.servo.getStringPreference(
487 "browserhtml.homescreen_url",
488 );
489 const homescreen = new WebView(homescreenUrl, "Home", {});
490 layoutManager.addWebView(homescreen);
491 layoutManager.setHomescreen(homescreen.webviewId);
492
493 // Create mobile action bar
494 mobileActionBar = document.createElement("mobile-action-bar");
495 document.body.appendChild(mobileActionBar);
496 mobileActionBar.setLayoutManager(layoutManager);
497 layoutManager.setActionBar(mobileActionBar);
498
499 // Handle new tab action from action bar
500 mobileActionBar.addEventListener("action-new-tab", () => {
501 createNewView();
502 });
503
504 // Handle home action from action bar
505 mobileActionBar.addEventListener("action-home", () => {
506 switchToHomescreen();
507 });
508
509 // Create mobile notification sheet
510 mobileNotificationSheet = document.createElement(
511 "mobile-notification-sheet",
512 );
513 document.body.appendChild(mobileNotificationSheet);
514 mobileNotificationSheet.tabCount = layoutManager.getTabCount();
515
516 mobileNotificationSheet.addEventListener("notification-click", (e) => {
517 const notification = e.detail.notification;
518 if (notification.webviewId && layoutManager.webviews) {
519 for (const [id, entry] of layoutManager.webviews) {
520 if (id.toString() === notification.webviewId) {
521 layoutManager.setActiveWebView(id);
522 break;
523 }
524 }
525 }
526 dismissNotification(notification);
527 mobileNotificationSheet.open = false;
528 });
529
530 mobileNotificationSheet.addEventListener("notification-dismiss", (e) => {
531 dismissNotification(e.detail.notification);
532 mobileNotificationSheet.notifications = [...notifications];
533 });
534
535 mobileNotificationSheet.addEventListener("notification-clear-all", () => {
536 clearAllNotifications();
537 mobileNotificationSheet.notifications = [];
538 });
539
540 mobileNotificationSheet.addEventListener("sheet-closed", () => {
541 mobileNotificationSheet.open = false;
542 });
543
544 // Create mobile tab overview
545 mobileOverview = document.createElement("mobile-overview");
546 document.body.appendChild(mobileOverview);
547
548 mobileOverview.addEventListener("tab-select", (e) => {
549 layoutManager.setActiveWebView(e.detail.tabId);
550 });
551
552 mobileOverview.addEventListener("tab-close", (e) => {
553 layoutManager.removeWebView(e.detail.tabId);
554 updateTabOverviewData();
555 });
556
557 mobileOverview.addEventListener("tab-new", () => {
558 createNewView();
559 mobileOverview.open = false;
560 });
561
562 mobileOverview.addEventListener("tab-home", () => {
563 switchToHomescreen();
564 mobileOverview.open = false;
565 });
566
567 mobileOverview.addEventListener("overview-close", () => {
568 mobileOverview.open = false;
569 });
570
571 // Create mobile radial menu
572 mobileRadialMenu = document.createElement("mobile-radial-menu");
573 document.body.appendChild(mobileRadialMenu);
574
575 // Radial menu is now triggered via contextmenu event from webviews,
576 // not via the gesture handler's long-press detection.
577
578 mobileRadialMenu.addEventListener("radial-action", (e) => {
579 handleRadialMenuAction(e.detail);
580 });
581
582 // Handle radial menu dismiss (close without action)
583 mobileRadialMenu.addEventListener("radial-dismiss", () => {
584 // Dismiss pending context menu on active webview
585 const entry = layoutManager.getActiveEntry();
586 if (entry?.webview?.dismissPendingContextMenu) {
587 entry.webview.dismissPendingContextMenu();
588 }
589 });
590
591 // Override layout manager's showOverview to use mobile tab overview
592 layoutManager.showOverview = function () {
593 updateTabOverviewData();
594 mobileOverview.open = true;
595 };
596
597 layoutManager.hideOverview = function () {
598 mobileOverview.open = false;
599 };
600 }
601
602 // Desktop-only header handlers
603 const headerSpan = document.querySelector("header span");
604 if (headerSpan) {
605 headerSpan.onmousedown = (e) => {
606 e.preventDefault();
607 navigator.embedder.startWindowDrag();
608 };
609 }
610
611 const headerResize = document.querySelector("header .resize");
612 if (headerResize) {
613 headerResize.onmousedown = (e) => {
614 e.preventDefault();
615 navigator.embedder.startWindowResize();
616 };
617 }
618
619 const headerClose = document.querySelector("header .close");
620 if (headerClose) {
621 headerClose.onmousedown = (e) => {
622 e.preventDefault();
623 navigator.embedder.closeCurrentOSWindow();
624 };
625 }
626
627 const plusIcon = document.getElementById("plus-icon");
628 if (plusIcon) {
629 plusIcon.onclick = () => {
630 createNewView();
631 };
632 }
633
634 // Setup system menu (desktop only)
635 if (!isMobileMode) {
636 systemMenu = document.createElement("system-menu");
637 document.body.appendChild(systemMenu);
638
639 const menuIcon = document.getElementById("menu-icon");
640 if (menuIcon) {
641 menuIcon.onclick = () => {
642 systemMenu.open = !systemMenu.open;
643 };
644 }
645
646 systemMenu.addEventListener("menu-action", (e) => {
647 switch (e.detail.action) {
648 case "new-tab":
649 createNewView();
650 break;
651 case "new-window":
652 navigator.embedder.openNewOSWindow(
653 "http://system.localhost:8888/index.html",
654 );
655 break;
656 case "new-search":
657 navigator.embedder.openNewOSWindow(
658 "http://system.localhost:8888/search.html",
659 "notitle,ontop",
660 );
661 break;
662 case "overview":
663 layoutManager.toggleOverview();
664 break;
665 case "settings":
666 openSettingsView();
667 break;
668 case "reload-ui":
669 window.location.reload();
670 break;
671 case "quit":
672 navigator.embedder.exit();
673 break;
674 }
675 });
676 }
677
678 // Setup notification panel
679 notificationPanel = document.getElementById("notification-panel");
680
681 const notificationsIconContainer = document.getElementById(
682 "notifications-icon-container",
683 );
684 if (notificationsIconContainer) {
685 notificationsIconContainer.onclick = () => {
686 notificationPanel.open = !notificationPanel.open;
687 };
688 }
689
690 // Listen for mobile notification show event (from edge gesture)
691 document.addEventListener("mobile-show-notifications", () => {
692 if (isMobileMode && mobileNotificationSheet) {
693 mobileNotificationSheet.notifications = [...notifications];
694 mobileNotificationSheet.tabCount = layoutManager.getTabCount();
695 mobileNotificationSheet.open = true;
696 } else if (notificationPanel) {
697 notificationPanel.open = true;
698 }
699 });
700
701 notificationPanel.addEventListener("notification-click", (e) => {
702 const notification = e.detail.notification;
703
704 // Focus the source webview if possible
705 if (notification.webviewId && layoutManager.webviews) {
706 // Try to find the webview by its ID
707 for (const [id, entry] of layoutManager.webviews) {
708 if (id.toString() === notification.webviewId) {
709 layoutManager.setActiveWebView(id);
710 layoutManager.scrollToPanel(entry.panelIndex);
711 break;
712 }
713 }
714 }
715
716 // Dismiss the notification (no actions supported for now)
717 dismissNotification(notification);
718
719 // Close the panel
720 notificationPanel.open = false;
721 });
722
723 notificationPanel.addEventListener("notification-dismiss", (e) => {
724 dismissNotification(e.detail.notification);
725 });
726
727 notificationPanel.addEventListener("notification-clear-all", () => {
728 clearAllNotifications();
729 });
730
731 notificationPanel.addEventListener("panel-closed", () => {
732 notificationPanel.open = false;
733 });
734
735 // Listen for notifications from webviews
736 document
737 .getElementById("root")
738 .addEventListener("webview-notification", (e) => {
739 console.log("[Notification] Received from webview:", e.detail);
740 addNotification({
741 webviewId: e.detail.webviewId?.toString(),
742 title: e.detail.title,
743 body: e.detail.body,
744 tag: e.detail.tag,
745 iconUrl: e.detail.iconUrl,
746 });
747 });
748
749 // Listen for virtual keyboard show/hide events from webviews
750 document
751 .getElementById("root")
752 .addEventListener("webview-inputmethod-show", (event) => {
753 openVirtualKeyboard(event.detail);
754 });
755
756 document
757 .getElementById("root")
758 .addEventListener("webview-inputmethod-hide", () => {
759 console.log("[Keyboard] Hide event received");
760 closeVirtualKeyboard();
761 });
762
763 // Listen for radial menu show events from webviews (mobile mode)
764 document
765 .getElementById("root")
766 .addEventListener("webview-show-radial-menu", (e) => {
767 if (isMobileMode && mobileRadialMenu) {
768 console.log("[RadialMenu] Show event received from webview:", e.detail);
769 mobileRadialMenu.canGoBack = e.detail.canGoBack;
770 mobileRadialMenu.canGoForward = e.detail.canGoForward;
771 mobileRadialMenu.isHomescreen =
772 layoutManager.activeWebviewId === layoutManager.homescreenWebviewId;
773 mobileRadialMenu.show(e.detail.x, e.detail.y, e.detail.contextMenu);
774 }
775 });
776
777 const params = new URLSearchParams(window.location.search);
778 const openValue = params.get("open");
779 if (openValue) {
780 const webView = new WebView(openValue, "", {});
781 layoutManager.addWebView(webView);
782 } else if (!isMobileMode) {
783 // In mobile mode, homescreen is already created as the initial view
784 createNewView();
785 }
786
787 // BroadcastChannel for receiving URLs from search window
788 const searchChannel = new BroadcastChannel("servo-search");
789 searchChannel.onmessage = (e) => {
790 if (e.data.type === "discover") {
791 // Respond to discovery with our window ID
792 searchChannel.postMessage({ type: "available", windowId: windowId });
793 } else if (
794 e.data.type === "openUrl" &&
795 e.data.targetWindowId === windowId
796 ) {
797 // This message is targeted to us - handle it
798 searchChannel.postMessage({ type: "ack", id: e.data.id });
799 const webView = new WebView(e.data.url, "", {});
800 layoutManager.addWebView(webView);
801 } else if (e.data.type === "listWebViews") {
802 // Respond with list of our web-views
803 const webviews = [];
804 for (const [webviewId, entry] of layoutManager.webviews) {
805 webviews.push({
806 webviewId: webviewId,
807 title: entry.webview.title || "",
808 url: entry.webview.url || "",
809 });
810 }
811 searchChannel.postMessage({
812 type: "webviewList",
813 windowId: windowId,
814 webviews: webviews,
815 });
816 } else if (
817 e.data.type === "selectWebView" &&
818 e.data.targetWindowId === windowId
819 ) {
820 // Select and focus the specified web-view
821 searchChannel.postMessage({ type: "ack", id: e.data.id });
822 layoutManager.setActiveWebView(e.data.webviewId);
823 const entry = layoutManager.webviews.get(e.data.webviewId);
824 if (entry) {
825 layoutManager.scrollToPanel(entry.panelIndex);
826 }
827 }
828 };
829});