@tutti-os/browser-node 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1179 @@
1
+ import {
2
+ resolveBrowserSessionPartition
3
+ } from "./chunk-UTXZLRPE.js";
4
+ import {
5
+ normalizeBrowserComparableUrl,
6
+ normalizeHostBrowserComparableUrl
7
+ } from "./chunk-LVVPDNEF.js";
8
+
9
+ // src/react/BrowserNode.tsx
10
+ import {
11
+ ArrowLeftIcon,
12
+ ArrowRightIcon,
13
+ Badge,
14
+ Button,
15
+ Input,
16
+ LaunchIcon,
17
+ LoadingIcon,
18
+ RefreshIcon,
19
+ WarningLinedIcon,
20
+ ViewportMenuSurface,
21
+ cn,
22
+ menuItemClassName
23
+ } from "@tutti-os/ui-system";
24
+ import { useEffect as useEffect3, useRef, useState } from "react";
25
+
26
+ // src/react/useBrowserNodeController.ts
27
+ import { useEffect, useMemo } from "react";
28
+ import { useExternalStoreSnapshot } from "@tutti-os/ui-react-hooks";
29
+
30
+ // src/core/nodeController.ts
31
+ var controllerRegistry = /* @__PURE__ */ new Map();
32
+ function acquireBrowserNodeController(input) {
33
+ const existing = controllerRegistry.get(input.nodeId);
34
+ const entry = existing ?? createBrowserNodeControllerEntry({
35
+ defaultUrl: input.defaultUrl,
36
+ feature: input.feature,
37
+ navigationPolicy: input.navigationPolicy,
38
+ nodeId: input.nodeId,
39
+ profileId: input.profileId ?? null,
40
+ sessionMode: input.sessionMode ?? "shared",
41
+ sessionPartition: input.sessionPartition,
42
+ syncDefaultUrl: input.syncDefaultUrl ?? false
43
+ });
44
+ entry.context = {
45
+ defaultUrl: input.defaultUrl,
46
+ feature: input.feature,
47
+ navigationPolicy: input.navigationPolicy,
48
+ nodeId: input.nodeId,
49
+ profileId: input.profileId ?? null,
50
+ sessionMode: input.sessionMode ?? "shared",
51
+ sessionPartition: input.sessionPartition,
52
+ syncDefaultUrl: input.syncDefaultUrl ?? false
53
+ };
54
+ if (!existing) {
55
+ controllerRegistry.set(input.nodeId, entry);
56
+ }
57
+ reconcileBrowserNodeControllerState(entry, {
58
+ allowAutoActivate: false,
59
+ notifyListeners: false
60
+ });
61
+ return entry.controller;
62
+ }
63
+ function createBrowserNodeControllerEntry(context) {
64
+ const runtime = context.feature.runtimeStore.getNodeState(context.nodeId);
65
+ const displayUrl = resolveBrowserNodeDisplayUrl(runtime, context.defaultUrl);
66
+ const entry = {
67
+ connectedRelease: null,
68
+ controller: null,
69
+ context,
70
+ lastColdActivationUrl: resolveInitialLastColdActivationUrl(
71
+ runtime,
72
+ context.defaultUrl
73
+ ),
74
+ listeners: /* @__PURE__ */ new Set(),
75
+ pendingColdActivationUrl: null,
76
+ refCount: 0,
77
+ runtimeUnsubscribe: null,
78
+ state: {
79
+ displayUrl,
80
+ draftUrl: displayUrl,
81
+ runtime
82
+ }
83
+ };
84
+ entry.controller = {
85
+ getState() {
86
+ return entry.state;
87
+ },
88
+ goBack() {
89
+ return entry.context.feature.hostApi.goBack({
90
+ nodeId: entry.context.nodeId
91
+ });
92
+ },
93
+ goForward() {
94
+ return entry.context.feature.hostApi.goForward({
95
+ nodeId: entry.context.nodeId
96
+ });
97
+ },
98
+ reload() {
99
+ return entry.context.feature.hostApi.reload({
100
+ nodeId: entry.context.nodeId
101
+ });
102
+ },
103
+ release() {
104
+ entry.refCount = Math.max(0, entry.refCount - 1);
105
+ if (entry.refCount > 0) {
106
+ return;
107
+ }
108
+ entry.connectedRelease?.();
109
+ entry.connectedRelease = null;
110
+ entry.runtimeUnsubscribe?.();
111
+ entry.runtimeUnsubscribe = null;
112
+ controllerRegistry.delete(entry.context.nodeId);
113
+ },
114
+ retain() {
115
+ entry.refCount += 1;
116
+ if (entry.refCount > 1) {
117
+ return;
118
+ }
119
+ if (!controllerRegistry.has(entry.context.nodeId)) {
120
+ controllerRegistry.set(entry.context.nodeId, entry);
121
+ }
122
+ entry.connectedRelease = entry.context.feature.connect();
123
+ entry.runtimeUnsubscribe = entry.context.feature.runtimeStore.subscribe(
124
+ () => {
125
+ reconcileBrowserNodeControllerState(entry, {
126
+ allowAutoActivate: true,
127
+ notifyListeners: true
128
+ });
129
+ }
130
+ );
131
+ reconcileBrowserNodeControllerState(entry, {
132
+ allowAutoActivate: true,
133
+ notifyListeners: true
134
+ });
135
+ },
136
+ setDraftUrl(nextUrl) {
137
+ if (entry.state.draftUrl === nextUrl) {
138
+ return;
139
+ }
140
+ entry.state = {
141
+ ...entry.state,
142
+ draftUrl: nextUrl
143
+ };
144
+ notifyBrowserNodeControllerListeners(entry);
145
+ },
146
+ sync() {
147
+ reconcileBrowserNodeControllerState(entry, {
148
+ allowAutoActivate: true,
149
+ notifyListeners: true
150
+ });
151
+ },
152
+ subscribe(listener) {
153
+ entry.listeners.add(listener);
154
+ return () => {
155
+ entry.listeners.delete(listener);
156
+ };
157
+ },
158
+ async submitDraftUrl() {
159
+ const resolved = entry.context.feature.resolveAddressInput(
160
+ entry.state.draftUrl
161
+ );
162
+ if (!resolved.url) {
163
+ return;
164
+ }
165
+ if (entry.state.draftUrl !== resolved.url) {
166
+ entry.state = {
167
+ ...entry.state,
168
+ draftUrl: resolved.url
169
+ };
170
+ notifyBrowserNodeControllerListeners(entry);
171
+ }
172
+ await entry.context.feature.hostApi.navigate({
173
+ navigationPolicy: entry.context.navigationPolicy,
174
+ nodeId: entry.context.nodeId,
175
+ url: resolved.url
176
+ });
177
+ }
178
+ };
179
+ return entry;
180
+ }
181
+ function resolveInitialLastColdActivationUrl(runtime, defaultUrl) {
182
+ const trimmedUrl = defaultUrl.trim();
183
+ if (runtime.lifecycle === "cold" || runtime.error !== null || trimmedUrl.length === 0 || trimmedUrl === "about:blank") {
184
+ return null;
185
+ }
186
+ const comparableDefaultUrl = normalizeBrowserComparableUrl(trimmedUrl);
187
+ const comparableRuntimeUrl = runtime.url ? normalizeBrowserComparableUrl(runtime.url) : null;
188
+ return comparableDefaultUrl !== null && comparableDefaultUrl === comparableRuntimeUrl ? trimmedUrl : null;
189
+ }
190
+ function notifyBrowserNodeControllerListeners(entry) {
191
+ for (const listener of entry.listeners) {
192
+ listener();
193
+ }
194
+ }
195
+ function resolveBrowserNodeDisplayUrl(runtime, defaultUrl) {
196
+ const resolvedRuntimeUrl = runtime.url?.trim() ?? "";
197
+ return resolvedRuntimeUrl.length > 0 ? resolvedRuntimeUrl : defaultUrl;
198
+ }
199
+ function reconcileBrowserNodeControllerState(entry, options) {
200
+ const runtime = entry.context.feature.runtimeStore.getNodeState(
201
+ entry.context.nodeId
202
+ );
203
+ const displayUrl = resolveBrowserNodeDisplayUrl(
204
+ runtime,
205
+ entry.context.defaultUrl
206
+ );
207
+ const nextDraftUrl = displayUrl !== entry.state.displayUrl ? displayUrl : entry.state.draftUrl;
208
+ const changed = entry.state.runtime !== runtime || entry.state.displayUrl !== displayUrl || entry.state.draftUrl !== nextDraftUrl;
209
+ if (changed) {
210
+ entry.state = {
211
+ displayUrl,
212
+ draftUrl: nextDraftUrl,
213
+ runtime
214
+ };
215
+ if (options.notifyListeners) {
216
+ notifyBrowserNodeControllerListeners(entry);
217
+ }
218
+ }
219
+ if (options.allowAutoActivate) {
220
+ void maybeActivateBrowserNodeDefaultUrl(entry).catch(() => void 0);
221
+ }
222
+ }
223
+ async function maybeActivateBrowserNodeDefaultUrl(entry) {
224
+ const {
225
+ defaultUrl,
226
+ feature,
227
+ navigationPolicy,
228
+ nodeId,
229
+ profileId,
230
+ sessionMode,
231
+ sessionPartition,
232
+ syncDefaultUrl
233
+ } = entry.context;
234
+ const trimmedUrl = defaultUrl.trim();
235
+ const comparableDefaultUrl = normalizeHostBrowserComparableUrl(trimmedUrl);
236
+ const comparableRuntimeUrl = entry.state.runtime.url ? normalizeHostBrowserComparableUrl(entry.state.runtime.url) : null;
237
+ const shouldActivateColdNode = entry.state.runtime.lifecycle === "cold";
238
+ const shouldSyncDefaultUrl = syncDefaultUrl && entry.state.runtime.lifecycle !== "cold" && comparableDefaultUrl !== null && (comparableRuntimeUrl !== comparableDefaultUrl || entry.state.runtime.error !== null) && entry.lastColdActivationUrl !== trimmedUrl;
239
+ if (trimmedUrl.length === 0 || trimmedUrl === "about:blank" || entry.state.runtime.isLoading || entry.pendingColdActivationUrl === trimmedUrl || !shouldActivateColdNode && !shouldSyncDefaultUrl || shouldActivateColdNode && entry.state.runtime.error !== null && entry.lastColdActivationUrl === trimmedUrl) {
240
+ return;
241
+ }
242
+ entry.pendingColdActivationUrl = trimmedUrl;
243
+ try {
244
+ await feature.hostApi.activate({
245
+ navigationPolicy,
246
+ nodeId,
247
+ profileId,
248
+ sessionMode,
249
+ sessionPartition,
250
+ url: trimmedUrl
251
+ });
252
+ entry.lastColdActivationUrl = trimmedUrl;
253
+ } finally {
254
+ if (entry.pendingColdActivationUrl === trimmedUrl) {
255
+ entry.pendingColdActivationUrl = null;
256
+ }
257
+ }
258
+ }
259
+
260
+ // src/react/useBrowserNodeController.ts
261
+ function useBrowserNodeController(input) {
262
+ const controller = useMemo(
263
+ () => acquireBrowserNodeController({
264
+ defaultUrl: input.defaultUrl,
265
+ feature: input.feature,
266
+ navigationPolicy: input.navigationPolicy,
267
+ nodeId: input.nodeId,
268
+ profileId: input.profileId ?? null,
269
+ sessionMode: input.sessionMode ?? "shared",
270
+ sessionPartition: input.sessionPartition,
271
+ syncDefaultUrl: input.syncDefaultUrl ?? false
272
+ }),
273
+ [
274
+ input.defaultUrl,
275
+ input.feature,
276
+ input.navigationPolicy,
277
+ input.nodeId,
278
+ input.profileId,
279
+ input.sessionMode,
280
+ input.sessionPartition,
281
+ input.syncDefaultUrl
282
+ ]
283
+ );
284
+ useEffect(() => {
285
+ controller.retain();
286
+ return () => {
287
+ controller.release();
288
+ };
289
+ }, [controller]);
290
+ useEffect(() => {
291
+ controller.sync();
292
+ }, [
293
+ controller,
294
+ input.defaultUrl,
295
+ input.feature,
296
+ input.navigationPolicy,
297
+ input.nodeId,
298
+ input.profileId,
299
+ input.sessionMode,
300
+ input.sessionPartition,
301
+ input.syncDefaultUrl
302
+ ]);
303
+ const state = useExternalStoreSnapshot({
304
+ getSnapshot() {
305
+ return controller.getState();
306
+ },
307
+ subscribe(listener) {
308
+ return controller.subscribe(listener);
309
+ }
310
+ });
311
+ return {
312
+ controller,
313
+ state
314
+ };
315
+ }
316
+
317
+ // src/react/useBrowserNodeWebview.ts
318
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2 } from "react";
319
+ import { useExternalStoreSnapshot as useExternalStoreSnapshot2 } from "@tutti-os/ui-react-hooks";
320
+
321
+ // src/core/webviewController.ts
322
+ var browserGuestUnregisterGraceMs = 250;
323
+ var browserNodeInitialWebviewSrc = "about:blank";
324
+ var webviewControllerRegistry = /* @__PURE__ */ new Map();
325
+ var pendingGuestIdsByNodeId = /* @__PURE__ */ new Map();
326
+ var pendingUnregisterTimersByNodeId = /* @__PURE__ */ new Map();
327
+ function acquireBrowserNodeWebviewController(input) {
328
+ const existing = webviewControllerRegistry.get(input.nodeId);
329
+ const entry = existing ?? createBrowserNodeWebviewControllerEntry({
330
+ feature: input.feature,
331
+ initialUrl: input.initialUrl,
332
+ lifecycle: input.lifecycle,
333
+ navigationPolicy: input.navigationPolicy,
334
+ nodeId: input.nodeId,
335
+ onGuestInteraction: input.onGuestInteraction,
336
+ profileId: input.profileId,
337
+ sessionMode: input.sessionMode,
338
+ sessionPartition: input.sessionPartition
339
+ });
340
+ entry.context = {
341
+ feature: input.feature,
342
+ initialUrl: input.initialUrl,
343
+ lifecycle: input.lifecycle,
344
+ navigationPolicy: input.navigationPolicy,
345
+ nodeId: input.nodeId,
346
+ onGuestInteraction: input.onGuestInteraction,
347
+ profileId: input.profileId,
348
+ sessionMode: input.sessionMode,
349
+ sessionPartition: input.sessionPartition
350
+ };
351
+ if (!existing) {
352
+ webviewControllerRegistry.set(input.nodeId, entry);
353
+ }
354
+ return entry.controller;
355
+ }
356
+ function createBrowserNodeWebviewControllerEntry(context) {
357
+ const state = resolveBrowserNodeWebviewControllerState(context);
358
+ const entry = {
359
+ attachedListeners: [],
360
+ context,
361
+ controller: null,
362
+ listeners: /* @__PURE__ */ new Set(),
363
+ refCount: 0,
364
+ registeredGuestId: null,
365
+ registeringGuestId: null,
366
+ state,
367
+ webview: null
368
+ };
369
+ entry.controller = {
370
+ dismissDevToolsContextMenu() {
371
+ setBrowserNodeDevToolsContextMenu(entry, null);
372
+ },
373
+ getState() {
374
+ return entry.state;
375
+ },
376
+ async openDevToolsFromContextMenu() {
377
+ setBrowserNodeDevToolsContextMenu(entry, null);
378
+ try {
379
+ await entry.context.feature.hostApi.openDevTools?.({
380
+ nodeId: entry.context.nodeId
381
+ });
382
+ } catch (error) {
383
+ reportBrowserNodeWebviewDiagnostic(
384
+ entry,
385
+ "devtools.open.failed",
386
+ {
387
+ error: error instanceof Error ? error.message : String(error),
388
+ webContentsId: readBrowserNodeWebContentsId(entry.webview),
389
+ webviewPartition: entry.state.webviewPartition
390
+ },
391
+ "warn"
392
+ );
393
+ throw error;
394
+ }
395
+ },
396
+ release() {
397
+ entry.refCount = Math.max(0, entry.refCount - 1);
398
+ if (entry.refCount > 0) {
399
+ return;
400
+ }
401
+ scheduleBrowserNodeGuestUnregister(entry);
402
+ detachBrowserNodeWebview(entry);
403
+ webviewControllerRegistry.delete(entry.context.nodeId);
404
+ },
405
+ retain() {
406
+ entry.refCount += 1;
407
+ if (entry.refCount > 1) {
408
+ return;
409
+ }
410
+ reconcileBrowserNodeWebviewControllerState(entry, {
411
+ allowHostEffects: true,
412
+ notifyListeners: true,
413
+ rebindWebview: true
414
+ });
415
+ },
416
+ setWebview(element) {
417
+ if (entry.webview === element) {
418
+ return;
419
+ }
420
+ detachBrowserNodeWebview(entry);
421
+ entry.webview = element;
422
+ attachBrowserNodeWebview(entry);
423
+ },
424
+ sync() {
425
+ reconcileBrowserNodeWebviewControllerState(entry, {
426
+ allowHostEffects: true,
427
+ notifyListeners: true,
428
+ rebindWebview: true
429
+ });
430
+ },
431
+ subscribe(listener) {
432
+ entry.listeners.add(listener);
433
+ return () => {
434
+ entry.listeners.delete(listener);
435
+ };
436
+ }
437
+ };
438
+ return entry;
439
+ }
440
+ function resolveBrowserNodeWebviewControllerState(context) {
441
+ const webviewPartition = resolveBrowserSessionPartition({
442
+ profileId: context.profileId,
443
+ sessionMode: context.sessionMode,
444
+ sessionPartition: context.sessionPartition
445
+ });
446
+ return {
447
+ devToolsContextMenu: null,
448
+ shouldRenderWebview: context.lifecycle !== "cold",
449
+ webviewKey: `${context.nodeId}:${webviewPartition}`,
450
+ webviewPartition,
451
+ webviewSrc: browserNodeInitialWebviewSrc
452
+ };
453
+ }
454
+ function reconcileBrowserNodeWebviewControllerState(entry, options) {
455
+ const nextState = resolveBrowserNodeWebviewControllerState(entry.context);
456
+ const changed = entry.state.devToolsContextMenu !== nextState.devToolsContextMenu || entry.state.shouldRenderWebview !== nextState.shouldRenderWebview || entry.state.webviewKey !== nextState.webviewKey || entry.state.webviewPartition !== nextState.webviewPartition || entry.state.webviewSrc !== nextState.webviewSrc;
457
+ if (options.allowHostEffects) {
458
+ if (entry.context.lifecycle === "cold") {
459
+ scheduleBrowserNodeGuestUnregister(entry);
460
+ } else {
461
+ clearPendingBrowserNodeGuestUnregister(entry.context.nodeId);
462
+ void entry.context.feature.hostApi.prepareSession({
463
+ navigationPolicy: entry.context.navigationPolicy,
464
+ nodeId: entry.context.nodeId,
465
+ profileId: entry.context.profileId,
466
+ sessionMode: entry.context.sessionMode,
467
+ sessionPartition: entry.context.sessionPartition
468
+ }).catch(() => void 0);
469
+ }
470
+ }
471
+ if (!changed) {
472
+ if (options.rebindWebview && entry.webview && entry.attachedListeners.length === 0) {
473
+ attachBrowserNodeWebview(entry);
474
+ }
475
+ return;
476
+ }
477
+ entry.state = nextState;
478
+ detachBrowserNodeWebview(entry);
479
+ attachBrowserNodeWebview(entry);
480
+ if (options.notifyListeners) {
481
+ notifyBrowserNodeWebviewControllerListeners(entry);
482
+ }
483
+ }
484
+ function setBrowserNodeDevToolsContextMenu(entry, devToolsContextMenu) {
485
+ if (entry.state.devToolsContextMenu?.x === devToolsContextMenu?.x && entry.state.devToolsContextMenu?.y === devToolsContextMenu?.y) {
486
+ return;
487
+ }
488
+ entry.state = {
489
+ ...entry.state,
490
+ devToolsContextMenu
491
+ };
492
+ notifyBrowserNodeWebviewControllerListeners(entry);
493
+ }
494
+ function notifyBrowserNodeWebviewControllerListeners(entry) {
495
+ for (const listener of entry.listeners) {
496
+ listener();
497
+ }
498
+ }
499
+ function clearPendingBrowserNodeGuestUnregister(nodeId) {
500
+ const timerId = pendingUnregisterTimersByNodeId.get(nodeId);
501
+ if (timerId !== void 0) {
502
+ globalThis.clearTimeout(timerId);
503
+ pendingUnregisterTimersByNodeId.delete(nodeId);
504
+ }
505
+ pendingGuestIdsByNodeId.delete(nodeId);
506
+ }
507
+ function scheduleBrowserNodeGuestUnregister(entry) {
508
+ const guestId = entry.registeredGuestId;
509
+ const nodeId = entry.context.nodeId;
510
+ entry.registeringGuestId = null;
511
+ if (guestId === null) {
512
+ clearPendingBrowserNodeGuestUnregister(nodeId);
513
+ return;
514
+ }
515
+ entry.registeredGuestId = null;
516
+ clearPendingBrowserNodeGuestUnregister(nodeId);
517
+ pendingGuestIdsByNodeId.set(nodeId, guestId);
518
+ const timerId = globalThis.setTimeout(() => {
519
+ pendingUnregisterTimersByNodeId.delete(nodeId);
520
+ const pendingGuestId = pendingGuestIdsByNodeId.get(nodeId);
521
+ pendingGuestIdsByNodeId.delete(nodeId);
522
+ if (typeof pendingGuestId !== "number" || !Number.isFinite(pendingGuestId)) {
523
+ return;
524
+ }
525
+ void entry.context.feature.hostApi.unregisterGuest({
526
+ nodeId: entry.context.nodeId,
527
+ webContentsId: pendingGuestId
528
+ }).catch(() => void 0);
529
+ }, browserGuestUnregisterGraceMs);
530
+ pendingUnregisterTimersByNodeId.set(nodeId, timerId);
531
+ }
532
+ function detachBrowserNodeWebview(entry) {
533
+ if (!entry.webview) {
534
+ entry.attachedListeners = [];
535
+ return;
536
+ }
537
+ for (const record of entry.attachedListeners) {
538
+ entry.webview.removeEventListener(record.event, record.listener);
539
+ }
540
+ entry.attachedListeners = [];
541
+ }
542
+ function attachBrowserNodeWebview(entry) {
543
+ const webview = entry.webview;
544
+ if (!webview || !entry.state.shouldRenderWebview) {
545
+ return;
546
+ }
547
+ const registerGuest = async () => {
548
+ const guestId = webview.getWebContentsId?.();
549
+ if (typeof guestId !== "number" || !Number.isFinite(guestId) || guestId <= 0 || entry.registeredGuestId === guestId || entry.registeringGuestId === guestId) {
550
+ return;
551
+ }
552
+ clearPendingBrowserNodeGuestUnregister(entry.context.nodeId);
553
+ entry.registeringGuestId = guestId;
554
+ try {
555
+ await entry.context.feature.hostApi.registerGuest({
556
+ navigationPolicy: entry.context.navigationPolicy,
557
+ nodeId: entry.context.nodeId,
558
+ profileId: entry.context.profileId,
559
+ sessionMode: entry.context.sessionMode,
560
+ sessionPartition: entry.context.sessionPartition,
561
+ webContentsId: guestId
562
+ });
563
+ entry.registeredGuestId = guestId;
564
+ } finally {
565
+ if (entry.registeringGuestId === guestId) {
566
+ entry.registeringGuestId = null;
567
+ }
568
+ }
569
+ };
570
+ const handleDidAttach = () => {
571
+ void registerGuest().catch(() => void 0);
572
+ };
573
+ const handleDomReady = () => {
574
+ void registerGuest().catch(() => void 0);
575
+ };
576
+ const handleGuestInteraction = () => {
577
+ entry.context.onGuestInteraction?.();
578
+ };
579
+ const handleDevToolsContextMenu = (event) => {
580
+ const hasNativeContextMenu = entry.context.feature.hostApi.showDevToolsContextMenu !== void 0;
581
+ const hasInlineContextMenu = entry.context.feature.hostApi.openDevTools !== void 0;
582
+ if (!hasNativeContextMenu && !hasInlineContextMenu) {
583
+ return;
584
+ }
585
+ event.preventDefault();
586
+ const point = resolveBrowserNodeContextMenuPoint(event, webview);
587
+ if (hasNativeContextMenu) {
588
+ void entry.context.feature.hostApi.showDevToolsContextMenu?.({
589
+ label: entry.context.feature.i18n.t("actions.openDevTools"),
590
+ nodeId: entry.context.nodeId,
591
+ point
592
+ }).catch((error) => {
593
+ reportBrowserNodeWebviewDiagnostic(
594
+ entry,
595
+ "devtools.native-context-menu.failed",
596
+ {
597
+ error: error instanceof Error ? error.message : String(error),
598
+ webContentsId: readBrowserNodeWebContentsId(webview),
599
+ webviewPartition: entry.state.webviewPartition
600
+ },
601
+ "warn"
602
+ );
603
+ });
604
+ return;
605
+ }
606
+ setBrowserNodeDevToolsContextMenu(entry, point);
607
+ };
608
+ const records = [
609
+ { event: "did-attach", listener: handleDidAttach },
610
+ { event: "dom-ready", listener: handleDomReady },
611
+ { event: "context-menu", listener: handleDevToolsContextMenu },
612
+ { event: "contextmenu", listener: handleDevToolsContextMenu },
613
+ { event: "focus", listener: handleGuestInteraction },
614
+ { event: "ipc-message", listener: handleGuestInteraction }
615
+ ];
616
+ for (const record of records) {
617
+ webview.addEventListener(record.event, record.listener);
618
+ }
619
+ entry.attachedListeners = records;
620
+ }
621
+ function resolveBrowserNodeContextMenuPoint(event, webview) {
622
+ const eventWithPoint = event;
623
+ const clientX = readFiniteNumber(eventWithPoint.clientX);
624
+ const clientY = readFiniteNumber(eventWithPoint.clientY);
625
+ if (clientX !== null && clientY !== null) {
626
+ return { x: clientX, y: clientY };
627
+ }
628
+ const paramX = readFiniteNumber(eventWithPoint.params?.x);
629
+ const paramY = readFiniteNumber(eventWithPoint.params?.y);
630
+ if (paramX !== null && paramY !== null) {
631
+ return { x: paramX, y: paramY };
632
+ }
633
+ const rect = webview.getBoundingClientRect();
634
+ return {
635
+ x: rect.left,
636
+ y: rect.top
637
+ };
638
+ }
639
+ function readFiniteNumber(value) {
640
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
641
+ }
642
+ function readBrowserNodeWebContentsId(webview) {
643
+ const webContentsId = webview?.getWebContentsId?.();
644
+ return typeof webContentsId === "number" && Number.isFinite(webContentsId) ? webContentsId : null;
645
+ }
646
+ function reportBrowserNodeWebviewDiagnostic(entry, event, details, level = "info") {
647
+ entry.context.feature.reportDiagnostic?.({
648
+ details: {
649
+ ...details,
650
+ nodeId: entry.context.nodeId
651
+ },
652
+ event,
653
+ level
654
+ });
655
+ }
656
+
657
+ // src/react/useBrowserNodeWebview.ts
658
+ function useBrowserNodeWebview({
659
+ feature,
660
+ initialUrl,
661
+ lifecycle,
662
+ navigationPolicy,
663
+ nodeId,
664
+ onGuestInteraction,
665
+ profileId,
666
+ sessionMode,
667
+ sessionPartition
668
+ }) {
669
+ const controller = useMemo2(
670
+ () => acquireBrowserNodeWebviewController({
671
+ feature,
672
+ initialUrl,
673
+ lifecycle,
674
+ navigationPolicy,
675
+ nodeId,
676
+ onGuestInteraction,
677
+ profileId,
678
+ sessionMode,
679
+ sessionPartition
680
+ }),
681
+ [
682
+ feature,
683
+ initialUrl,
684
+ lifecycle,
685
+ navigationPolicy,
686
+ nodeId,
687
+ onGuestInteraction,
688
+ profileId,
689
+ sessionMode,
690
+ sessionPartition
691
+ ]
692
+ );
693
+ useEffect2(() => {
694
+ controller.retain();
695
+ return () => {
696
+ controller.release();
697
+ };
698
+ }, [controller]);
699
+ useEffect2(() => {
700
+ controller.sync();
701
+ }, [
702
+ controller,
703
+ initialUrl,
704
+ lifecycle,
705
+ navigationPolicy,
706
+ nodeId,
707
+ onGuestInteraction,
708
+ profileId,
709
+ sessionMode,
710
+ sessionPartition
711
+ ]);
712
+ const state = useExternalStoreSnapshot2({
713
+ getSnapshot() {
714
+ return controller.getState();
715
+ },
716
+ subscribe(listener) {
717
+ return controller.subscribe(listener);
718
+ }
719
+ });
720
+ const setWebviewRef = useCallback(
721
+ (element) => {
722
+ controller.setWebview(element);
723
+ },
724
+ [controller]
725
+ );
726
+ const dismissDevToolsContextMenu = useCallback(() => {
727
+ controller.dismissDevToolsContextMenu();
728
+ }, [controller]);
729
+ const openDevToolsFromContextMenu = useCallback(() => {
730
+ return controller.openDevToolsFromContextMenu();
731
+ }, [controller]);
732
+ return {
733
+ devToolsContextMenu: state.devToolsContextMenu,
734
+ dismissDevToolsContextMenu,
735
+ openDevToolsFromContextMenu,
736
+ shouldRenderWebview: state.shouldRenderWebview,
737
+ setWebviewRef,
738
+ webviewKey: state.webviewKey,
739
+ webviewPartition: state.webviewPartition,
740
+ webviewSrc: state.webviewSrc
741
+ };
742
+ }
743
+
744
+ // src/react/BrowserNode.tsx
745
+ import { jsx, jsxs } from "react/jsx-runtime";
746
+ var browserNodeAllowPopupsAttribute = "true";
747
+ function BrowserNode({
748
+ defaultUrl,
749
+ feature,
750
+ navigationPolicy = null,
751
+ nodeId,
752
+ onFocusRequest,
753
+ onNavigated,
754
+ profileId = null,
755
+ sessionMode = "shared",
756
+ sessionPartition = null,
757
+ showHeader = true,
758
+ syncDefaultUrl = false
759
+ }) {
760
+ const { controller, state } = useBrowserNodeController({
761
+ defaultUrl,
762
+ feature,
763
+ navigationPolicy,
764
+ nodeId,
765
+ profileId,
766
+ sessionMode,
767
+ sessionPartition,
768
+ syncDefaultUrl
769
+ });
770
+ const runtime = state.runtime;
771
+ const lastNavigatedUrlRef = useRef(
772
+ state.runtime.url?.trim() || null
773
+ );
774
+ const errorMessage = runtime.error ? formatBrowserNodeErrorMessage(feature, runtime.error) : null;
775
+ const errorStatus = runtime.error ? formatBrowserNodeErrorStatus(feature, runtime.error) : null;
776
+ const isShowingLoadError = errorMessage !== null;
777
+ const openExternalUrl = feature.hostApi.openExternal ? feature.resolveAddressInput(state.displayUrl).url : null;
778
+ const {
779
+ devToolsContextMenu,
780
+ dismissDevToolsContextMenu,
781
+ openDevToolsFromContextMenu,
782
+ shouldRenderWebview,
783
+ setWebviewRef,
784
+ webviewKey,
785
+ webviewPartition,
786
+ webviewSrc
787
+ } = useBrowserNodeWebview({
788
+ feature,
789
+ initialUrl: state.displayUrl,
790
+ lifecycle: runtime.lifecycle,
791
+ navigationPolicy,
792
+ nodeId,
793
+ onGuestInteraction: onFocusRequest,
794
+ profileId,
795
+ sessionMode,
796
+ sessionPartition
797
+ });
798
+ useEffect3(() => {
799
+ const navigatedUrl = state.runtime.url?.trim() ?? "";
800
+ if (!onNavigated || state.runtime.isLoading || state.runtime.error || navigatedUrl.length === 0 || navigatedUrl === "about:blank" || lastNavigatedUrlRef.current === navigatedUrl) {
801
+ return;
802
+ }
803
+ lastNavigatedUrlRef.current = navigatedUrl;
804
+ onNavigated(navigatedUrl);
805
+ }, [
806
+ onNavigated,
807
+ state.runtime.error,
808
+ state.runtime.isLoading,
809
+ state.runtime.url
810
+ ]);
811
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col overflow-hidden bg-[var(--background-panel)]", children: [
812
+ showHeader ? /* @__PURE__ */ jsx(
813
+ BrowserNodeHeader,
814
+ {
815
+ canGoBack: runtime.canGoBack,
816
+ canGoForward: runtime.canGoForward,
817
+ draftUrl: state.draftUrl,
818
+ feature,
819
+ isCold: runtime.lifecycle === "cold",
820
+ isLoading: runtime.isLoading,
821
+ onDraftUrlChange: (nextUrl) => controller.setDraftUrl(nextUrl),
822
+ onFocusRequest,
823
+ onSubmitUrl: () => {
824
+ void controller.submitDraftUrl().catch(() => void 0);
825
+ },
826
+ onOpenExternal: openExternalUrl ? () => {
827
+ void feature.hostApi.openExternal?.({ url: openExternalUrl }).catch(() => void 0);
828
+ } : void 0,
829
+ onGoBack: () => {
830
+ void controller.goBack().catch(() => void 0);
831
+ },
832
+ onGoForward: () => {
833
+ void controller.goForward().catch(() => void 0);
834
+ },
835
+ onReload: () => {
836
+ void controller.reload().catch(() => void 0);
837
+ }
838
+ }
839
+ ) : null,
840
+ /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden bg-[var(--background-panel)]", children: [
841
+ shouldRenderWebview ? /* @__PURE__ */ jsx(
842
+ "webview",
843
+ {
844
+ allowpopups: browserNodeAllowPopupsAttribute,
845
+ ref: setWebviewRef,
846
+ className: cn(
847
+ "absolute inset-0 h-full w-full border-0 bg-[var(--background-panel)]",
848
+ isShowingLoadError ? "hidden pointer-events-none" : "visible"
849
+ ),
850
+ "data-browser-node-webview": "true",
851
+ partition: webviewPartition,
852
+ src: webviewSrc
853
+ },
854
+ webviewKey
855
+ ) : null,
856
+ errorMessage ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center bg-[var(--background-panel)] px-8 py-10 text-center", children: /* @__PURE__ */ jsxs(
857
+ "div",
858
+ {
859
+ className: "flex w-full max-w-[440px] flex-col items-center",
860
+ role: "status",
861
+ "aria-live": "polite",
862
+ children: [
863
+ /* @__PURE__ */ jsx("div", { className: "flex size-11 items-center justify-center rounded-full border border-[color-mix(in_srgb,var(--state-danger)_22%,transparent)] bg-[color-mix(in_srgb,var(--state-danger)_8%,transparent)] text-[var(--state-danger)]", children: /* @__PURE__ */ jsx(WarningLinedIcon, { className: "size-5" }) }),
864
+ /* @__PURE__ */ jsx("div", { className: "mt-4 text-lg font-semibold text-[var(--text-primary)]", children: feature.i18n.t("loadFailed") }),
865
+ /* @__PURE__ */ jsx("div", { className: "mt-1 max-w-[360px] text-sm leading-5 text-[var(--text-secondary)]", children: errorMessage }),
866
+ errorStatus ? /* @__PURE__ */ jsx("div", { className: "mt-4 rounded-full border border-border bg-[var(--transparency-block)] px-2.5 py-1 text-xs font-medium text-[var(--text-secondary)]", children: errorStatus }) : null,
867
+ openExternalUrl ? /* @__PURE__ */ jsx("div", { className: "mt-5 flex flex-wrap items-center justify-center gap-2", children: /* @__PURE__ */ jsxs(
868
+ Button,
869
+ {
870
+ size: "sm",
871
+ type: "button",
872
+ variant: "outline",
873
+ onClick: () => {
874
+ void feature.hostApi.openExternal?.({ url: openExternalUrl }).catch(() => void 0);
875
+ },
876
+ children: [
877
+ /* @__PURE__ */ jsx(LaunchIcon, { className: "size-3.5" }),
878
+ feature.i18n.t("actions.openExternal")
879
+ ]
880
+ }
881
+ ) }) : null
882
+ ]
883
+ }
884
+ ) }) : null,
885
+ devToolsContextMenu ? /* @__PURE__ */ jsx(
886
+ ViewportMenuSurface,
887
+ {
888
+ open: true,
889
+ className: "w-44",
890
+ dismissOnEscape: true,
891
+ dismissOnPointerDownOutside: true,
892
+ onDismiss: dismissDevToolsContextMenu,
893
+ placement: {
894
+ type: "point",
895
+ point: devToolsContextMenu,
896
+ alignX: "start",
897
+ alignY: "start",
898
+ estimatedSize: {
899
+ width: 176,
900
+ height: 40
901
+ }
902
+ },
903
+ children: /* @__PURE__ */ jsx(
904
+ "button",
905
+ {
906
+ className: cn(menuItemClassName, "w-full"),
907
+ type: "button",
908
+ onClick: () => {
909
+ void openDevToolsFromContextMenu().catch(() => void 0);
910
+ },
911
+ children: feature.i18n.t("actions.openDevTools")
912
+ }
913
+ )
914
+ }
915
+ ) : null
916
+ ] })
917
+ ] });
918
+ }
919
+ function BrowserNodeWorkbenchHeader({
920
+ className,
921
+ defaultActions,
922
+ defaultUrl,
923
+ dragHandleProps,
924
+ feature,
925
+ nodeId,
926
+ onCloseRequest,
927
+ onFocusRequest
928
+ }) {
929
+ const { controller, state } = useBrowserNodeController({
930
+ defaultUrl,
931
+ feature,
932
+ nodeId
933
+ });
934
+ const runtime = state.runtime;
935
+ const openExternalUrl = feature.hostApi.openExternal ? feature.resolveAddressInput(state.displayUrl).url : null;
936
+ return /* @__PURE__ */ jsx(
937
+ BrowserNodeHeader,
938
+ {
939
+ canGoBack: runtime.canGoBack,
940
+ canGoForward: runtime.canGoForward,
941
+ className,
942
+ defaultActions,
943
+ draftUrl: state.draftUrl,
944
+ dragHandleProps,
945
+ feature,
946
+ isCold: runtime.lifecycle === "cold",
947
+ isLoading: runtime.isLoading,
948
+ onCloseRequest,
949
+ onDraftUrlChange: (nextUrl) => controller.setDraftUrl(nextUrl),
950
+ onFocusRequest,
951
+ onSubmitUrl: () => {
952
+ void controller.submitDraftUrl().catch(() => void 0);
953
+ },
954
+ onOpenExternal: openExternalUrl ? () => {
955
+ void feature.hostApi.openExternal?.({ url: openExternalUrl }).catch(() => void 0);
956
+ } : void 0,
957
+ onGoBack: () => {
958
+ void controller.goBack().catch(() => void 0);
959
+ },
960
+ onGoForward: () => {
961
+ void controller.goForward().catch(() => void 0);
962
+ },
963
+ onReload: () => {
964
+ void controller.reload().catch(() => void 0);
965
+ },
966
+ withBorder: false
967
+ }
968
+ );
969
+ }
970
+ function BrowserNodeHeader({
971
+ canGoBack,
972
+ canGoForward,
973
+ className,
974
+ defaultActions,
975
+ draftUrl,
976
+ dragHandleProps,
977
+ feature,
978
+ isCold = false,
979
+ isLoading,
980
+ onCloseRequest,
981
+ onDraftUrlChange,
982
+ onFocusRequest,
983
+ onGoBack,
984
+ onGoForward,
985
+ onOpenExternal,
986
+ onReload,
987
+ onSubmitUrl,
988
+ withBorder = true
989
+ }) {
990
+ const [reloadAnimationKey, setReloadAnimationKey] = useState(0);
991
+ const handleReload = () => {
992
+ setReloadAnimationKey((currentKey) => currentKey + 1);
993
+ onReload();
994
+ };
995
+ return /* @__PURE__ */ jsxs(
996
+ "div",
997
+ {
998
+ className: cn(
999
+ "flex h-[var(--workbench-header-height,38px)] min-h-[var(--workbench-header-height,38px)] items-center gap-2 bg-[var(--background-panel)] px-2 pl-3",
1000
+ withBorder ? "border-b border-border" : null,
1001
+ className
1002
+ ),
1003
+ "data-browser-node-header": "true",
1004
+ onDoubleClick: (event) => {
1005
+ if (event.target instanceof Element && event.target.closest(".nodrag")) {
1006
+ return;
1007
+ }
1008
+ event.stopPropagation();
1009
+ dragHandleProps?.onDoubleClick?.(event);
1010
+ },
1011
+ children: [
1012
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1", children: [
1013
+ /* @__PURE__ */ jsx(
1014
+ BrowserNodeHeaderButton,
1015
+ {
1016
+ disabled: !canGoBack,
1017
+ label: feature.i18n.t("actions.back"),
1018
+ onClick: onGoBack,
1019
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-[15px]" })
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsx(
1023
+ BrowserNodeHeaderButton,
1024
+ {
1025
+ disabled: !canGoForward,
1026
+ label: feature.i18n.t("actions.forward"),
1027
+ onClick: onGoForward,
1028
+ children: /* @__PURE__ */ jsx(ArrowRightIcon, { className: "size-[15px]" })
1029
+ }
1030
+ ),
1031
+ /* @__PURE__ */ jsx(
1032
+ BrowserNodeHeaderButton,
1033
+ {
1034
+ label: feature.i18n.t("actions.reload"),
1035
+ onClick: handleReload,
1036
+ children: /* @__PURE__ */ jsx(
1037
+ RefreshIcon,
1038
+ {
1039
+ className: cn(
1040
+ "size-[15px]",
1041
+ reloadAnimationKey > 0 && "motion-safe:animate-[spin_520ms_cubic-bezier(0.4,0,0.2,1)_1_reverse]"
1042
+ )
1043
+ },
1044
+ reloadAnimationKey
1045
+ )
1046
+ }
1047
+ )
1048
+ ] }),
1049
+ /* @__PURE__ */ jsx(
1050
+ "div",
1051
+ {
1052
+ ...dragHandleProps,
1053
+ className: "h-full w-8 shrink-0 cursor-grab active:cursor-grabbing",
1054
+ "data-browser-node-drag-gutter": "true",
1055
+ "data-node-drag-handle": "true",
1056
+ "aria-hidden": "true"
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsxs(
1060
+ "form",
1061
+ {
1062
+ className: "nodrag relative min-w-0 flex-1",
1063
+ onSubmit: (event) => {
1064
+ event.preventDefault();
1065
+ event.stopPropagation();
1066
+ onSubmitUrl();
1067
+ },
1068
+ children: [
1069
+ /* @__PURE__ */ jsx(
1070
+ Input,
1071
+ {
1072
+ "aria-label": feature.i18n.t("addressLabel"),
1073
+ className: "pr-8 focus-visible:border-input focus-visible:ring-0 focus-visible:ring-offset-0",
1074
+ placeholder: feature.i18n.t("addressPlaceholder"),
1075
+ size: "sm",
1076
+ value: draftUrl,
1077
+ onChange: (event) => onDraftUrlChange(event.target.value),
1078
+ onFocus: onFocusRequest
1079
+ }
1080
+ ),
1081
+ isLoading ? /* @__PURE__ */ jsx(LoadingIcon, { className: "pointer-events-none absolute right-2 top-1/2 z-[1] size-4 -translate-y-1/2 animate-spin text-[var(--text-tertiary)]" }) : null
1082
+ ]
1083
+ }
1084
+ ),
1085
+ onOpenExternal ? /* @__PURE__ */ jsx(
1086
+ BrowserNodeHeaderButton,
1087
+ {
1088
+ label: feature.i18n.t("actions.openExternal"),
1089
+ onClick: onOpenExternal,
1090
+ children: /* @__PURE__ */ jsx(LaunchIcon, { className: "size-[15px]" })
1091
+ }
1092
+ ) : null,
1093
+ defaultActions ? /* @__PURE__ */ jsxs("div", { className: "nodrag flex shrink-0 items-center gap-1.5", children: [
1094
+ isCold ? /* @__PURE__ */ jsx(
1095
+ Badge,
1096
+ {
1097
+ className: "h-[26px] min-w-7 rounded-md text-[10px] font-semibold lowercase tracking-[0.08em]",
1098
+ "aria-label": feature.i18n.t("coldStatus"),
1099
+ children: feature.i18n.t("coldStatus")
1100
+ }
1101
+ ) : null,
1102
+ /* @__PURE__ */ jsx(
1103
+ "span",
1104
+ {
1105
+ className: "contents",
1106
+ onClickCapture: (event) => {
1107
+ if (!onCloseRequest || !(event.target instanceof Element) || !event.target.closest('[data-workbench-action="close"]')) {
1108
+ return;
1109
+ }
1110
+ onCloseRequest();
1111
+ },
1112
+ children: defaultActions
1113
+ }
1114
+ )
1115
+ ] }) : null
1116
+ ]
1117
+ }
1118
+ );
1119
+ }
1120
+ function formatBrowserNodeErrorMessage(feature, error) {
1121
+ switch (error.code) {
1122
+ case "invalid-url":
1123
+ return feature.i18n.t("errors.invalidUrl", error.params);
1124
+ case "navigation-failed":
1125
+ if (error.params && error.params.statusCode !== void 0) {
1126
+ return feature.i18n.t(
1127
+ "errors.navigationFailedWithStatus",
1128
+ error.params
1129
+ );
1130
+ }
1131
+ return feature.i18n.t("errors.navigationFailed", error.params);
1132
+ case "unsupported-protocol":
1133
+ return feature.i18n.t("errors.unsupportedProtocol", error.params);
1134
+ case "unsupported-url":
1135
+ return feature.i18n.t("errors.unsupportedUrl", error.params);
1136
+ }
1137
+ }
1138
+ function formatBrowserNodeErrorStatus(feature, error) {
1139
+ if (error.code !== "navigation-failed" || !error.params) {
1140
+ return null;
1141
+ }
1142
+ const statusCode = error.params.statusCode;
1143
+ if (typeof statusCode === "number") {
1144
+ return feature.i18n.t("errors.statusCode", { statusCode });
1145
+ }
1146
+ const errorCode = error.params.errorCode;
1147
+ if (typeof errorCode === "number") {
1148
+ return feature.i18n.t("errors.errorCode", { errorCode });
1149
+ }
1150
+ return null;
1151
+ }
1152
+ function BrowserNodeHeaderButton({
1153
+ children,
1154
+ disabled,
1155
+ label,
1156
+ onClick
1157
+ }) {
1158
+ return /* @__PURE__ */ jsx(
1159
+ Button,
1160
+ {
1161
+ "aria-label": label,
1162
+ className: "rounded-md",
1163
+ disabled,
1164
+ size: "icon-sm",
1165
+ title: label,
1166
+ type: "button",
1167
+ variant: "chrome",
1168
+ onClick,
1169
+ children
1170
+ }
1171
+ );
1172
+ }
1173
+
1174
+ export {
1175
+ BrowserNode,
1176
+ BrowserNodeWorkbenchHeader,
1177
+ BrowserNodeHeader
1178
+ };
1179
+ //# sourceMappingURL=chunk-2GEY55PS.js.map