cli-jaw 2.17.30 → 2.17.35
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.
- package/README.md +2 -0
- package/dist/src/agent/codex-app-client.js +69 -1
- package/dist/src/agent/codex-app-client.js.map +1 -1
- package/dist/src/agent/cursor-runtime.js +9 -0
- package/dist/src/agent/cursor-runtime.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +1 -1
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/prompt-context.js +39 -0
- package/dist/src/agent/prompt-context.js.map +1 -1
- package/dist/src/agent/spawn.js +138 -20
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/browser/web-ai/capability-observation-presets.js +4 -0
- package/dist/src/browser/web-ai/capability-observation-presets.js.map +1 -1
- package/dist/src/browser/web-ai/chatgpt-model.js +9 -5
- package/dist/src/browser/web-ai/chatgpt-model.js.map +1 -1
- package/dist/src/browser/web-ai/grok-model.js +18 -5
- package/dist/src/browser/web-ai/grok-model.js.map +1 -1
- package/dist/src/cli/claude-models.js +6 -0
- package/dist/src/cli/claude-models.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +29 -4
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/registry.js +56 -15
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/code-mode/model-options.js +4 -4
- package/dist/src/code-mode/model-options.js.map +1 -1
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +19 -0
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/runtime-settings.js +11 -2
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/orchestrator/attestation.js +66 -0
- package/dist/src/orchestrator/attestation.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +11 -3
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +5 -0
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/routes/command.js +4 -1
- package/dist/src/routes/command.js.map +1 -1
- package/dist/src/routes/orchestrate.js +14 -2
- package/dist/src/routes/orchestrate.js.map +1 -1
- package/dist/src/slack/commands.js +1 -0
- package/dist/src/slack/commands.js.map +1 -1
- package/dist/src/telegram/bot.js +3 -2
- package/dist/src/telegram/bot.js.map +1 -1
- package/package.json +5 -1
- package/public/dist/assets/{Agent-BjxJRsKG.js → Agent-kyyjcnqC.js} +1 -1
- package/public/dist/assets/BrowserPanel-0b3deLGR.js +3 -0
- package/public/dist/assets/BrowserPanel-D7SnTts8.css +1 -0
- package/public/dist/assets/{Heartbeat-Ca8DmJcw.js → Heartbeat-CD1R95Kv.js} +1 -1
- package/public/dist/assets/{Memory-BY88ur_h.js → Memory-CpZIdE1o.js} +1 -1
- package/public/dist/assets/{ModelProvider-mtAtYLmW.js → ModelProvider-tL42E5dy.js} +1 -1
- package/public/dist/assets/agent-meta-KPnVam9G.js +1 -0
- package/public/dist/assets/{app-CYayckBa.js → app-BvRV-aEt.js} +4 -4
- package/public/dist/assets/bgtask-badge-DLqpImpn.js +1 -0
- package/public/dist/assets/{bgtask-badge-CT6s6rND.js → bgtask-badge-FiEB3FJm.js} +1 -1
- package/public/dist/assets/constants-BfYYzHNr.js +1 -0
- package/public/dist/assets/{employees-DCju7V1x.js → employees-86F_D_Ce.js} +1 -1
- package/public/dist/assets/iframe-renderer-C_swzquc.js +1 -0
- package/public/dist/assets/{iframe-renderer-LZp7Nqm1.js → iframe-renderer-eozJpgjk.js} +2 -2
- package/public/dist/assets/manager-BnmWx9xL.js +13 -0
- package/public/dist/assets/manager-Cmu0aKPb.css +1 -0
- package/public/dist/assets/{memory-V5caYbpS.js → memory-DX7U3WV_.js} +1 -1
- package/public/dist/assets/memory-DfPbqPTD.js +1 -0
- package/public/dist/assets/{message-history-Bp_sUDSx.js → message-history-B_fIGorC.js} +1 -1
- package/public/dist/assets/message-history-BhSsLjis.js +1 -0
- package/public/dist/assets/{render-Cd4frZ2U.js → render-fKAuISeM.js} +1 -1
- package/public/dist/assets/settings-BNqSR9Zr.js +1 -0
- package/public/dist/assets/{settings-a0blK1fr.js → settings-CH6exiZf.js} +1 -1
- package/public/dist/assets/{settings-core-C5qo8giT.js → settings-core-DqKyyxbQ.js} +1 -1
- package/public/dist/assets/settings-core-x7CmBZq1.js +1 -0
- package/public/dist/assets/{sidebar-FbI4udzs.js → sidebar-qyLy9Og8.js} +3 -3
- package/public/dist/assets/skills-C7Yi3LIq.js +1 -0
- package/public/dist/assets/{skills-DeG9lWF2.js → skills-DAhOtbfP.js} +1 -1
- package/public/dist/assets/{slash-commands-FoQuZXga.js → slash-commands-BYH6YVS4.js} +1 -1
- package/public/dist/assets/slash-commands-DEbJk3sj.js +1 -0
- package/public/dist/assets/{trace-drawer-CWs8ydMO.js → trace-drawer-nyAJ3Bm_.js} +1 -1
- package/public/dist/assets/{ui-Cd8etFwN.js → ui-C62w6bJS.js} +1 -1
- package/public/dist/assets/ui-CBErZPBp.js +1 -0
- package/public/dist/index.html +1 -1
- package/public/dist/manager/index.html +2 -2
- package/public/js/constants.ts +22 -5
- package/public/js/features/settings-types.ts +5 -0
- package/public/manager/src/App.tsx +1 -1
- package/public/manager/src/AppChrome.tsx +18 -3
- package/public/manager/src/SidebarRailRouter.tsx +47 -1
- package/public/manager/src/browser-panel/BrowserPanel.tsx +151 -57
- package/public/manager/src/browser-panel/browser-address-bar.tsx +53 -0
- package/public/manager/src/browser-panel/browser-address-state.ts +41 -0
- package/public/manager/src/browser-panel/browser-favicon.ts +18 -0
- package/public/manager/src/browser-panel/browser-history-store.ts +80 -0
- package/public/manager/src/browser-panel/browser-panel.css +141 -24
- package/public/manager/src/components/CommandBar.tsx +17 -5
- package/public/manager/src/components/InstanceGroups.tsx +143 -23
- package/public/manager/src/components/InstanceNavigator.tsx +55 -2
- package/public/manager/src/components/InstanceRow.tsx +51 -3
- package/public/manager/src/components/SidebarRail.tsx +10 -10
- package/public/manager/src/components/SidebarResizeHandle.tsx +22 -0
- package/public/manager/src/components/WorkspaceLayout.tsx +22 -8
- package/public/manager/src/components/instance-row-status.ts +48 -0
- package/public/manager/src/components/sidebar-keyboard.ts +129 -0
- package/public/manager/src/dashboard-settings/DashboardSettingsWorkspace.tsx +42 -0
- package/public/manager/src/help/helpContent.tsx +1 -0
- package/public/manager/src/hooks/useSidebarGroupCollapse.ts +46 -0
- package/public/manager/src/hooks/useSidebarWidth.ts +88 -0
- package/public/manager/src/hooks/useWindowFullscreen.ts +23 -0
- package/public/manager/src/manager-components.css +206 -31
- package/public/manager/src/manager-layout.css +30 -19
- package/public/manager/src/manager-p0-1-1.css +72 -36
- package/public/manager/src/manager-polish.css +6 -45
- package/public/manager/src/manager-shortcut-runner.ts +37 -1
- package/public/manager/src/manager-shortcuts.ts +21 -0
- package/public/manager/src/manager-tokens.css +190 -127
- package/public/manager/src/panels/PanelResizer.tsx +18 -2
- package/public/manager/src/panels/desktop-bridge.ts +12 -1
- package/public/manager/src/panels/panels.css +32 -32
- package/public/manager/src/settings/pages/Agent.tsx +26 -0
- package/public/manager/src/settings/pages/components/agent/agent-meta.ts +6 -5
- package/public/manager/src/styles.css +15 -7
- package/public/manager/src/types.ts +5 -1
- package/public/dist/assets/BrowserPanel-CsYKexNf.js +0 -3
- package/public/dist/assets/BrowserPanel-DpntR8EJ.css +0 -1
- package/public/dist/assets/agent-meta-_Idad3qq.js +0 -1
- package/public/dist/assets/bgtask-badge-CidVvjWb.js +0 -1
- package/public/dist/assets/constants-DK-21dMG.js +0 -1
- package/public/dist/assets/iframe-renderer-aFXUtTXX.js +0 -1
- package/public/dist/assets/manager-CwqrZ2MH.js +0 -13
- package/public/dist/assets/manager-LkGs6qEJ.css +0 -1
- package/public/dist/assets/memory-BcZdf_oz.js +0 -1
- package/public/dist/assets/message-history-DrEPUdTy.js +0 -1
- package/public/dist/assets/settings-core-Cm-CmVLA.js +0 -1
- package/public/dist/assets/settings-mbGEtP5B.js +0 -1
- package/public/dist/assets/skills-G8KUeHBV.js +0 -1
- package/public/dist/assets/slash-commands-BYv3yboz.js +0 -1
- package/public/dist/assets/ui-yrn3s72A.js +0 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createElement, useCallback, useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from 'react';
|
|
2
2
|
import { getDesktop, isElectron } from '../panels/desktop-bridge';
|
|
3
|
-
import type { BrowserPickedElement, BrowserWebviewNativeAction, BrowserWebviewScreenshot, BrowserWebviewTabState } from '../panels/desktop-bridge';
|
|
3
|
+
import type { BrowserPickedElement, BrowserWebviewCommand, BrowserWebviewNativeAction, BrowserWebviewScreenshot, BrowserWebviewTabState } from '../panels/desktop-bridge';
|
|
4
4
|
import { DEFAULT_BROWSER_URL, isRestrictedBrowserHost, normalizeBrowserTarget } from './browser-url';
|
|
5
|
+
import { BrowserAddressBar } from './browser-address-bar';
|
|
6
|
+
import { createAddressBarState, displayedAddress, reduceAddressBar, type AddressBarAction, type AddressBarState } from './browser-address-state';
|
|
7
|
+
import { pickFaviconUrl, faviconInitial } from './browser-favicon';
|
|
8
|
+
import { loadBrowserHistory, saveBrowserHistory, upsertBrowserHistory, type BrowserHistoryEntry } from './browser-history-store';
|
|
5
9
|
import './browser-panel.css';
|
|
6
10
|
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
@@ -69,6 +73,7 @@ type ElectronWebviewEvent = Event & {
|
|
|
69
73
|
errorDescription?: string;
|
|
70
74
|
validatedURL?: string;
|
|
71
75
|
isMainFrame?: boolean;
|
|
76
|
+
favicons?: string[];
|
|
72
77
|
details?: {
|
|
73
78
|
reason?: string;
|
|
74
79
|
};
|
|
@@ -77,7 +82,6 @@ type ElectronWebviewEvent = Event & {
|
|
|
77
82
|
type BrowserTabState = {
|
|
78
83
|
id: string;
|
|
79
84
|
url: string;
|
|
80
|
-
inputUrl: string;
|
|
81
85
|
title: string;
|
|
82
86
|
blocked: boolean;
|
|
83
87
|
loading: boolean;
|
|
@@ -85,6 +89,8 @@ type BrowserTabState = {
|
|
|
85
89
|
error: string | null;
|
|
86
90
|
canGoBack: boolean;
|
|
87
91
|
canGoForward: boolean;
|
|
92
|
+
faviconUrl: string | null;
|
|
93
|
+
zoomFactor?: number;
|
|
88
94
|
};
|
|
89
95
|
|
|
90
96
|
type BrowserPanelProps = {
|
|
@@ -265,7 +271,6 @@ function createBrowserTab(id: string, target = DEFAULT_BROWSER_URL): BrowserTabS
|
|
|
265
271
|
return {
|
|
266
272
|
id,
|
|
267
273
|
url: target,
|
|
268
|
-
inputUrl: target,
|
|
269
274
|
title: titleFromUrl(target),
|
|
270
275
|
blocked: false,
|
|
271
276
|
loading: false,
|
|
@@ -273,9 +278,20 @@ function createBrowserTab(id: string, target = DEFAULT_BROWSER_URL): BrowserTabS
|
|
|
273
278
|
error: null,
|
|
274
279
|
canGoBack: false,
|
|
275
280
|
canGoForward: false,
|
|
281
|
+
faviconUrl: null,
|
|
276
282
|
};
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
function isRecordableBrowserUrl(target: string): boolean {
|
|
286
|
+
if (!target || target === 'about:blank') return false;
|
|
287
|
+
try {
|
|
288
|
+
const parsed = new URL(target);
|
|
289
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
280
296
|
const desktop = isElectron();
|
|
281
297
|
const desktopBridge = getDesktop();
|
|
@@ -290,8 +306,9 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
290
306
|
const [tabs, setTabs] = useState<BrowserTabState[]>(() => [initialTab.current]);
|
|
291
307
|
const [activeTabId, setActiveTabId] = useState(initialTab.current.id);
|
|
292
308
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
293
|
-
const
|
|
294
|
-
const
|
|
309
|
+
const [addressState, setAddressState] = useState<AddressBarState>(() => createAddressBarState(initialTab.current.url));
|
|
310
|
+
const [historyEntries, setHistoryEntries] = useState<BrowserHistoryEntry[]>(() => loadBrowserHistory());
|
|
311
|
+
const committedByUserRef = useRef<Set<string>>(new Set());
|
|
295
312
|
const pendingNavigationRefs = useRef<Map<string, string>>(new Map());
|
|
296
313
|
/**
|
|
297
314
|
* The webview `src` attribute is bound ONCE per page tab and never
|
|
@@ -312,6 +329,10 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
312
329
|
|
|
313
330
|
const activeTab = tabs.find(tab => tab.id === activeTabId) ?? tabs[0] ?? initialTab.current;
|
|
314
331
|
|
|
332
|
+
const dispatchAddress = useCallback((action: AddressBarAction) => {
|
|
333
|
+
setAddressState(current => reduceAddressBar(current, action));
|
|
334
|
+
}, []);
|
|
335
|
+
|
|
315
336
|
const updateTab = useCallback((id: string, patch: Partial<BrowserTabState>) => {
|
|
316
337
|
setTabs(current => {
|
|
317
338
|
const index = current.findIndex(tab => tab.id === id);
|
|
@@ -335,6 +356,10 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
335
356
|
const registrationIdFor = useCallback((tabId: string): string => (
|
|
336
357
|
moduleTabId ?? `panel:${tabId}`
|
|
337
358
|
), [moduleTabId]);
|
|
359
|
+
const moduleTabIdRef = useRef(moduleTabId);
|
|
360
|
+
moduleTabIdRef.current = moduleTabId;
|
|
361
|
+
const activeTabIdRef = useRef(activeTabId);
|
|
362
|
+
activeTabIdRef.current = activeTabId;
|
|
338
363
|
|
|
339
364
|
const [bridgeStates, setBridgeStates] = useState<Record<string, BrowserWebviewTabState>>({});
|
|
340
365
|
const [lastScreenshot, setLastScreenshot] = useState<BrowserWebviewScreenshot | null>(null);
|
|
@@ -400,8 +425,19 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
400
425
|
if (!browserBridge?.onWebviewState) return undefined;
|
|
401
426
|
return browserBridge.onWebviewState(state => {
|
|
402
427
|
setBridgeStates(current => (state.tabId in current ? { ...current, [state.tabId]: state } : current));
|
|
428
|
+
const liveModuleTabId = moduleTabIdRef.current;
|
|
429
|
+
const pageTabId = liveModuleTabId && state.tabId === liveModuleTabId
|
|
430
|
+
? activeTabIdRef.current
|
|
431
|
+
: state.tabId.startsWith('panel:')
|
|
432
|
+
? state.tabId.slice('panel:'.length)
|
|
433
|
+
: state.tabId;
|
|
434
|
+
const patch: Partial<BrowserTabState> = {};
|
|
435
|
+
const faviconUrl = pickFaviconUrl(state.favicons);
|
|
436
|
+
if (faviconUrl) patch.faviconUrl = faviconUrl;
|
|
437
|
+
if (typeof state.zoomFactor === 'number') patch.zoomFactor = state.zoomFactor;
|
|
438
|
+
if (Object.keys(patch).length > 0) updateTab(pageTabId, patch);
|
|
403
439
|
});
|
|
404
|
-
}, []);
|
|
440
|
+
}, [updateTab]);
|
|
405
441
|
|
|
406
442
|
// v5: native inspect returns the REAL element (selector/role/name/bounds).
|
|
407
443
|
// When it fires for this panel's active tab, pin the element and open the
|
|
@@ -462,9 +498,6 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
462
498
|
}
|
|
463
499
|
patch.url = current;
|
|
464
500
|
patch.title = titleFromUrl(current);
|
|
465
|
-
if (editingTabIdRef.current !== tabId) {
|
|
466
|
-
patch.inputUrl = current;
|
|
467
|
-
}
|
|
468
501
|
}
|
|
469
502
|
updateTab(tabId, patch);
|
|
470
503
|
} catch {
|
|
@@ -480,6 +513,14 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
480
513
|
const handleStop = () => {
|
|
481
514
|
updateTab(tabId, { loading: false });
|
|
482
515
|
refreshNavState(tabId);
|
|
516
|
+
const current = webview.getURL?.() ?? lastKnownUrlRefs.current.get(tabId);
|
|
517
|
+
if (!current || !isRecordableBrowserUrl(current) || !isUrlAllowed(current, desktop)) return;
|
|
518
|
+
const title = webview.getTitle?.()?.trim() || titleFromUrl(current);
|
|
519
|
+
setHistoryEntries(entries => {
|
|
520
|
+
const next = upsertBrowserHistory(entries, { url: current, title, at: Date.now() });
|
|
521
|
+
saveBrowserHistory(next);
|
|
522
|
+
return next;
|
|
523
|
+
});
|
|
483
524
|
};
|
|
484
525
|
const handleNavigate = (event: Event) => {
|
|
485
526
|
const nextUrl = (event as ElectronWebviewEvent).url;
|
|
@@ -496,9 +537,6 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
496
537
|
url: nextUrl,
|
|
497
538
|
title: titleFromUrl(nextUrl),
|
|
498
539
|
};
|
|
499
|
-
if (editingTabIdRef.current !== tabId) {
|
|
500
|
-
patch.inputUrl = nextUrl;
|
|
501
|
-
}
|
|
502
540
|
lastKnownUrlRefs.current.set(tabId, nextUrl);
|
|
503
541
|
updateTab(tabId, patch);
|
|
504
542
|
}
|
|
@@ -508,6 +546,10 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
508
546
|
const nextTitle = (event as ElectronWebviewEvent).title?.trim();
|
|
509
547
|
if (nextTitle) updateTab(tabId, { title: nextTitle });
|
|
510
548
|
};
|
|
549
|
+
const handleFavicon = (event: Event) => {
|
|
550
|
+
const faviconUrl = pickFaviconUrl((event as ElectronWebviewEvent).favicons);
|
|
551
|
+
updateTab(tabId, { faviconUrl });
|
|
552
|
+
};
|
|
511
553
|
const handleFail = (event: Event) => {
|
|
512
554
|
const failure = event as ElectronWebviewEvent;
|
|
513
555
|
if (failure.isMainFrame === false) return;
|
|
@@ -538,6 +580,7 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
538
580
|
webview.addEventListener('did-navigate', handleNavigate);
|
|
539
581
|
webview.addEventListener('did-navigate-in-page', handleNavigate);
|
|
540
582
|
webview.addEventListener('page-title-updated', handleTitle);
|
|
583
|
+
webview.addEventListener('page-favicon-updated', handleFavicon);
|
|
541
584
|
webview.addEventListener('did-fail-load', handleFail);
|
|
542
585
|
webview.addEventListener('render-process-gone', handleRenderGone);
|
|
543
586
|
webview.addEventListener('dom-ready', handleDomReady);
|
|
@@ -547,6 +590,7 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
547
590
|
webview.removeEventListener('did-navigate', handleNavigate);
|
|
548
591
|
webview.removeEventListener('did-navigate-in-page', handleNavigate);
|
|
549
592
|
webview.removeEventListener('page-title-updated', handleTitle);
|
|
593
|
+
webview.removeEventListener('page-favicon-updated', handleFavicon);
|
|
550
594
|
webview.removeEventListener('did-fail-load', handleFail);
|
|
551
595
|
webview.removeEventListener('render-process-gone', handleRenderGone);
|
|
552
596
|
webview.removeEventListener('dom-ready', handleDomReady);
|
|
@@ -610,7 +654,6 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
610
654
|
pendingNavigationRefs.current.delete(tabId);
|
|
611
655
|
updateTab(tabId, {
|
|
612
656
|
blocked: true,
|
|
613
|
-
inputUrl: rawTarget,
|
|
614
657
|
error: blockedUrlMessage(),
|
|
615
658
|
});
|
|
616
659
|
return;
|
|
@@ -620,9 +663,9 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
620
663
|
blocked: false,
|
|
621
664
|
error: null,
|
|
622
665
|
status: canUseElectronWebview ? null : 'Opened in a new browser tab. Embedded browser is only available in the Electron manager window.',
|
|
623
|
-
inputUrl: target,
|
|
624
666
|
url: target,
|
|
625
667
|
title: titleFromUrl(target),
|
|
668
|
+
faviconUrl: null,
|
|
626
669
|
});
|
|
627
670
|
if (canUseElectronWebview) {
|
|
628
671
|
lastKnownUrlRefs.current.set(tabId, target);
|
|
@@ -652,7 +695,6 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
652
695
|
const tab = createBrowserTab(`browser-tab-${nextTabIndex.current++}`, allowed ? target : DEFAULT_BROWSER_URL);
|
|
653
696
|
if (!allowed) {
|
|
654
697
|
tab.blocked = true;
|
|
655
|
-
tab.inputUrl = rawTarget;
|
|
656
698
|
tab.error = blockedUrlMessage();
|
|
657
699
|
}
|
|
658
700
|
setTabs(current => [...current, tab]);
|
|
@@ -663,6 +705,7 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
663
705
|
pendingNavigationRefs.current.delete(id);
|
|
664
706
|
initialSrcRefs.current.delete(id);
|
|
665
707
|
lastKnownUrlRefs.current.delete(id);
|
|
708
|
+
committedByUserRef.current.delete(id);
|
|
666
709
|
stableWebviewRefCallbacks.current.delete(id);
|
|
667
710
|
unregisterWebviewTarget(id);
|
|
668
711
|
setTabs(current => {
|
|
@@ -682,30 +725,30 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
682
725
|
}, [activeTabId, unregisterWebviewTarget]);
|
|
683
726
|
|
|
684
727
|
const navigate = useCallback(() => {
|
|
685
|
-
const rawTarget =
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
728
|
+
const rawTarget = addressState.focused ? addressState.draft : displayedAddress(addressState);
|
|
729
|
+
dispatchAddress({ type: 'submit' });
|
|
730
|
+
const normalized = normalizeBrowserTarget(rawTarget);
|
|
731
|
+
if (normalized && normalized !== DEFAULT_BROWSER_URL) {
|
|
732
|
+
committedByUserRef.current.add(activeTab.id);
|
|
733
|
+
}
|
|
690
734
|
openUrlInTab(activeTab.id, rawTarget);
|
|
691
|
-
|
|
735
|
+
inputRef.current?.blur();
|
|
736
|
+
}, [activeTab.id, addressState, dispatchAddress, openUrlInTab]);
|
|
692
737
|
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}, [activeTab.id, activeTab.inputUrl]);
|
|
738
|
+
const openHistoryEntry = useCallback((url: string) => {
|
|
739
|
+
committedByUserRef.current.add(activeTab.id);
|
|
740
|
+
dispatchAddress({ type: 'submit' });
|
|
741
|
+
openUrlInTab(activeTab.id, url);
|
|
742
|
+
inputRef.current?.blur();
|
|
743
|
+
}, [activeTab.id, dispatchAddress, openUrlInTab]);
|
|
700
744
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}, [activeTab.id, refreshNavState]);
|
|
745
|
+
useEffect(() => {
|
|
746
|
+
setAddressState({ focused: false, draft: '', liveUrl: activeTab.url });
|
|
747
|
+
}, [activeTab.id]);
|
|
748
|
+
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
dispatchAddress({ type: 'sync-live', liveUrl: activeTab.url });
|
|
751
|
+
}, [activeTab.url, dispatchAddress]);
|
|
709
752
|
|
|
710
753
|
// --- 030 toolbar action handlers ---
|
|
711
754
|
|
|
@@ -908,6 +951,33 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
908
951
|
else webview?.reload();
|
|
909
952
|
}, [activeTab.id]);
|
|
910
953
|
|
|
954
|
+
const handleReloadOrStop = useCallback(() => {
|
|
955
|
+
const webview = webviewRefs.current.get(activeTab.id);
|
|
956
|
+
if (!webview) return;
|
|
957
|
+
if (activeTab.loading) {
|
|
958
|
+
if (typeof webview.stop === 'function') webview.stop();
|
|
959
|
+
else void getDesktop()?.browser?.controlWebview?.({ kind: 'stop', tabId: activeRegistrationId });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
webview.reload();
|
|
963
|
+
}, [activeRegistrationId, activeTab.id, activeTab.loading]);
|
|
964
|
+
|
|
965
|
+
const handleZoom = useCallback(async (kind: 'zoomIn' | 'zoomOut' | 'zoomReset') => {
|
|
966
|
+
const command: BrowserWebviewCommand = { kind, tabId: activeRegistrationId };
|
|
967
|
+
const result = await getDesktop()?.browser?.controlWebview?.(command);
|
|
968
|
+
if (result?.ok && result.state) {
|
|
969
|
+
setBridgeStates(current => ({ ...current, [result.state!.tabId]: result.state! }));
|
|
970
|
+
const zoomFactor = result.state.zoomFactor;
|
|
971
|
+
const faviconUrl = pickFaviconUrl(result.state.favicons);
|
|
972
|
+
const patch: Partial<BrowserTabState> = {};
|
|
973
|
+
if (typeof zoomFactor === 'number') patch.zoomFactor = zoomFactor;
|
|
974
|
+
if (faviconUrl) patch.faviconUrl = faviconUrl;
|
|
975
|
+
if (Object.keys(patch).length > 0) updateTab(activeTab.id, patch);
|
|
976
|
+
} else if (result && !result.ok) {
|
|
977
|
+
reportActionError(result.error ?? 'Zoom unavailable');
|
|
978
|
+
}
|
|
979
|
+
}, [activeRegistrationId, activeTab.id, reportActionError, updateTab]);
|
|
980
|
+
|
|
911
981
|
useEffect(() => {
|
|
912
982
|
function handleShortcutAction(e: Event) {
|
|
913
983
|
const detail = (e as CustomEvent).detail;
|
|
@@ -992,7 +1062,19 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
992
1062
|
onClick={() => setActiveTabId(tab.id)}
|
|
993
1063
|
title={tab.title}
|
|
994
1064
|
>
|
|
995
|
-
|
|
1065
|
+
{tab.faviconUrl ? (
|
|
1066
|
+
<img
|
|
1067
|
+
className="browser-tab-favicon"
|
|
1068
|
+
src={tab.faviconUrl}
|
|
1069
|
+
alt=""
|
|
1070
|
+
width={16}
|
|
1071
|
+
height={16}
|
|
1072
|
+
onError={() => updateTab(tab.id, { faviconUrl: null })}
|
|
1073
|
+
/>
|
|
1074
|
+
) : (
|
|
1075
|
+
<span className="browser-tab-favicon is-fallback" aria-hidden="true">{faviconInitial(tab.title, tab.url)}</span>
|
|
1076
|
+
)}
|
|
1077
|
+
<span className="browser-tab-title">{tab.title}</span>
|
|
996
1078
|
</button>
|
|
997
1079
|
<button
|
|
998
1080
|
type="button"
|
|
@@ -1021,24 +1103,12 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
1021
1103
|
<div className="browser-toolbar">
|
|
1022
1104
|
<button type="button" className="browser-nav-btn" aria-label="Back" data-tooltip="Back" disabled={!canUseElectronWebview || !activeTab.canGoBack} onClick={() => webviewRefs.current.get(activeTab.id)?.goBack()}>‹</button>
|
|
1023
1105
|
<button type="button" className="browser-nav-btn" aria-label="Forward" data-tooltip="Forward" disabled={!canUseElectronWebview || !activeTab.canGoForward} onClick={() => webviewRefs.current.get(activeTab.id)?.goForward()}>›</button>
|
|
1024
|
-
<button type="button" className="browser-nav-btn" aria-label=
|
|
1025
|
-
<
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
onFocus={markUrlEditing}
|
|
1031
|
-
onBlur={clearUrlEditingSoon}
|
|
1032
|
-
onChange={event => {
|
|
1033
|
-
editingTabIdRef.current = activeTab.id;
|
|
1034
|
-
inputDraftRef.current = {
|
|
1035
|
-
tabId: activeTab.id,
|
|
1036
|
-
value: event.target.value,
|
|
1037
|
-
};
|
|
1038
|
-
updateTab(activeTab.id, { inputUrl: event.target.value });
|
|
1039
|
-
}}
|
|
1040
|
-
onKeyDown={event => { if (event.key === 'Enter') navigate(); }}
|
|
1041
|
-
aria-label="URL"
|
|
1106
|
+
<button type="button" className="browser-nav-btn" aria-label={activeTab.loading ? 'Stop' : 'Reload'} data-tooltip="Reload" disabled={!canUseElectronWebview} onClick={handleReloadOrStop}>↻</button>
|
|
1107
|
+
<BrowserAddressBar
|
|
1108
|
+
inputRef={inputRef}
|
|
1109
|
+
state={addressState}
|
|
1110
|
+
onDispatch={dispatchAddress}
|
|
1111
|
+
onSubmit={() => navigate()}
|
|
1042
1112
|
/>
|
|
1043
1113
|
<button type="button" className="browser-go-btn" data-tooltip={canUseElectronWebview ? 'Go' : 'Open'} onMouseDown={event => event.preventDefault()} onClick={navigate}>{canUseElectronWebview ? 'Go' : 'Open'}</button>
|
|
1044
1114
|
{canUseElectronWebview && (
|
|
@@ -1059,20 +1129,44 @@ export function BrowserPanel(props: BrowserPanelProps = {}) {
|
|
|
1059
1129
|
{activeBridgeState?.devToolsOpen && (
|
|
1060
1130
|
<button type="button" role="menuitem" className="browser-more-item" onClick={handleCloseDevTools}>Close DevTools</button>
|
|
1061
1131
|
)}
|
|
1132
|
+
<div className="browser-more-zoom" role="group" aria-label="Page zoom" onClick={event => event.stopPropagation()}>
|
|
1133
|
+
<button type="button" className="browser-more-item" onClick={() => void handleZoom('zoomOut')}>Zoom out</button>
|
|
1134
|
+
<span className="browser-more-zoom-label">{Math.round((activeTab.zoomFactor ?? 1) * 100)}%</span>
|
|
1135
|
+
<button type="button" className="browser-more-item" onClick={() => void handleZoom('zoomIn')}>Zoom in</button>
|
|
1136
|
+
<button type="button" className="browser-more-item" onClick={() => void handleZoom('zoomReset')}>Reset</button>
|
|
1137
|
+
</div>
|
|
1062
1138
|
</div>
|
|
1063
1139
|
)}
|
|
1064
1140
|
</div>
|
|
1065
1141
|
</div>
|
|
1066
1142
|
)}
|
|
1067
1143
|
</div>
|
|
1068
|
-
|
|
1144
|
+
<div className="browser-loading-track" aria-hidden="true">
|
|
1145
|
+
<div className="browser-loading-bar" data-loading={activeTab.loading ? 'true' : 'false'} />
|
|
1146
|
+
</div>
|
|
1147
|
+
{(activeTab.blocked || activeTab.error) && (
|
|
1069
1148
|
<div className={`browser-status${activeTab.error ? ' is-error' : ''}`}>
|
|
1070
|
-
{activeTab.error ??
|
|
1149
|
+
{activeTab.error ?? 'Blocked'}
|
|
1071
1150
|
</div>
|
|
1072
1151
|
)}
|
|
1073
1152
|
{canUseElectronWebview ? (
|
|
1074
1153
|
<div className="browser-webview-stack">
|
|
1075
1154
|
<div key={activeTab.id} className="browser-webview-host is-active">
|
|
1155
|
+
{historyEntries.length > 0 && !activeTab.loading && ((addressState.focused && addressState.draft.trim() === '') || !committedByUserRef.current.has(activeTab.id)) && (
|
|
1156
|
+
<div className="browser-history-empty" aria-label="Recent visits">
|
|
1157
|
+
{historyEntries.map(entry => (
|
|
1158
|
+
<button
|
|
1159
|
+
key={`${entry.url}:${entry.at}`}
|
|
1160
|
+
type="button"
|
|
1161
|
+
className="browser-history-item"
|
|
1162
|
+
onClick={() => openHistoryEntry(entry.url)}
|
|
1163
|
+
>
|
|
1164
|
+
<span className="browser-history-title">{entry.title || entry.url}</span>
|
|
1165
|
+
<span className="browser-history-url">{entry.url}</span>
|
|
1166
|
+
</button>
|
|
1167
|
+
))}
|
|
1168
|
+
</div>
|
|
1169
|
+
)}
|
|
1076
1170
|
{createElement('webview', {
|
|
1077
1171
|
ref: webviewRefCallbackFor(activeTab.id),
|
|
1078
1172
|
className: 'browser-webview',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
displayedAddress,
|
|
4
|
+
type AddressBarAction,
|
|
5
|
+
type AddressBarState,
|
|
6
|
+
} from './browser-address-state';
|
|
7
|
+
|
|
8
|
+
type BrowserAddressBarProps = {
|
|
9
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
10
|
+
state: AddressBarState;
|
|
11
|
+
onDispatch: (action: AddressBarAction) => void;
|
|
12
|
+
onSubmit: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function BrowserAddressBar(props: BrowserAddressBarProps) {
|
|
16
|
+
const { inputRef, state, onDispatch, onSubmit } = props;
|
|
17
|
+
|
|
18
|
+
function handleKeyDown(event: ReactKeyboardEvent<HTMLInputElement>): void {
|
|
19
|
+
if (event.key === 'Enter') {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
onDispatch({ type: 'submit' });
|
|
22
|
+
onSubmit();
|
|
23
|
+
inputRef.current?.blur();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (event.key === 'Escape') {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
onDispatch({ type: 'escape' });
|
|
29
|
+
inputRef.current?.blur();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<input
|
|
35
|
+
ref={inputRef}
|
|
36
|
+
className="browser-url-input"
|
|
37
|
+
type="text"
|
|
38
|
+
value={displayedAddress(state)}
|
|
39
|
+
placeholder="Search or enter URL"
|
|
40
|
+
spellCheck={false}
|
|
41
|
+
autoCorrect="off"
|
|
42
|
+
autoCapitalize="off"
|
|
43
|
+
aria-label="Search or enter URL"
|
|
44
|
+
onFocus={() => {
|
|
45
|
+
onDispatch({ type: 'focus' });
|
|
46
|
+
queueMicrotask(() => inputRef.current?.select());
|
|
47
|
+
}}
|
|
48
|
+
onBlur={() => onDispatch({ type: 'blur' })}
|
|
49
|
+
onChange={event => onDispatch({ type: 'change', draft: event.target.value })}
|
|
50
|
+
onKeyDown={handleKeyDown}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type AddressBarState = {
|
|
2
|
+
focused: boolean;
|
|
3
|
+
draft: string;
|
|
4
|
+
liveUrl: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type AddressBarAction =
|
|
8
|
+
| { type: 'sync-live'; liveUrl: string }
|
|
9
|
+
| { type: 'focus' }
|
|
10
|
+
| { type: 'change'; draft: string }
|
|
11
|
+
| { type: 'blur' }
|
|
12
|
+
| { type: 'escape' }
|
|
13
|
+
| { type: 'submit' };
|
|
14
|
+
|
|
15
|
+
export function createAddressBarState(liveUrl: string): AddressBarState {
|
|
16
|
+
return { focused: false, draft: liveUrl, liveUrl };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function displayedAddress(state: AddressBarState): string {
|
|
20
|
+
return state.focused ? state.draft : state.liveUrl;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reduceAddressBar(state: AddressBarState, action: AddressBarAction): AddressBarState {
|
|
24
|
+
switch (action.type) {
|
|
25
|
+
case 'sync-live':
|
|
26
|
+
if (state.focused) return { ...state, liveUrl: action.liveUrl };
|
|
27
|
+
return { ...state, liveUrl: action.liveUrl, draft: action.liveUrl };
|
|
28
|
+
case 'focus':
|
|
29
|
+
return { ...state, focused: true, draft: state.liveUrl };
|
|
30
|
+
case 'change':
|
|
31
|
+
return { ...state, focused: true, draft: action.draft };
|
|
32
|
+
case 'blur':
|
|
33
|
+
return { ...state, focused: false };
|
|
34
|
+
case 'escape':
|
|
35
|
+
return { focused: false, draft: state.liveUrl, liveUrl: state.liveUrl };
|
|
36
|
+
case 'submit':
|
|
37
|
+
return { focused: false, draft: state.draft, liveUrl: state.draft };
|
|
38
|
+
default:
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function pickFaviconUrl(favicons: string[] | undefined): string | null {
|
|
2
|
+
const first = favicons?.find(item => typeof item === 'string' && item.trim().length > 0);
|
|
3
|
+
return first ? first.trim() : null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function hostnameOf(url: string): string {
|
|
7
|
+
try {
|
|
8
|
+
return new URL(url).hostname;
|
|
9
|
+
} catch {
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function faviconInitial(title: string, url: string): string {
|
|
15
|
+
const source = title.trim() || hostnameOf(url);
|
|
16
|
+
const ch = source.replace(/^www\./, '').charAt(0);
|
|
17
|
+
return (ch || '?').toUpperCase();
|
|
18
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const BROWSER_HISTORY_STORAGE_KEY = 'jaw.browserHistory';
|
|
2
|
+
export const BROWSER_HISTORY_MAX = 20;
|
|
3
|
+
|
|
4
|
+
export type BrowserHistoryEntry = {
|
|
5
|
+
url: string;
|
|
6
|
+
title: string;
|
|
7
|
+
at: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isHistoryEntry(value: unknown): value is BrowserHistoryEntry {
|
|
11
|
+
if (value === null || typeof value !== 'object') return false;
|
|
12
|
+
const record = value as Record<string, unknown>;
|
|
13
|
+
return typeof record['url'] === 'string'
|
|
14
|
+
&& typeof record['title'] === 'string'
|
|
15
|
+
&& typeof record['at'] === 'number'
|
|
16
|
+
&& Number.isFinite(record['at']);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseBrowserHistory(raw: string | null): BrowserHistoryEntry[] {
|
|
20
|
+
if (raw == null || raw.trim() === '') return [];
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(raw);
|
|
23
|
+
if (!Array.isArray(parsed)) return [];
|
|
24
|
+
const entries: BrowserHistoryEntry[] = [];
|
|
25
|
+
for (const item of parsed) {
|
|
26
|
+
if (!isHistoryEntry(item)) continue;
|
|
27
|
+
const url = item.url.trim();
|
|
28
|
+
if (!url) continue;
|
|
29
|
+
entries.push({
|
|
30
|
+
url,
|
|
31
|
+
title: item.title.trim().slice(0, 512),
|
|
32
|
+
at: item.at,
|
|
33
|
+
});
|
|
34
|
+
if (entries.length >= BROWSER_HISTORY_MAX) break;
|
|
35
|
+
}
|
|
36
|
+
return entries;
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function upsertBrowserHistory(entries: BrowserHistoryEntry[], next: BrowserHistoryEntry): BrowserHistoryEntry[] {
|
|
43
|
+
const url = next.url.trim();
|
|
44
|
+
if (!url) return entries;
|
|
45
|
+
const without = entries.filter(item => item.url !== url);
|
|
46
|
+
return [{
|
|
47
|
+
url,
|
|
48
|
+
title: next.title.trim().slice(0, 512),
|
|
49
|
+
at: next.at,
|
|
50
|
+
}, ...without].slice(0, BROWSER_HISTORY_MAX);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function storage(): Storage | null {
|
|
54
|
+
try {
|
|
55
|
+
if (typeof globalThis.localStorage === 'undefined') return null;
|
|
56
|
+
return globalThis.localStorage;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadBrowserHistory(): BrowserHistoryEntry[] {
|
|
63
|
+
const store = storage();
|
|
64
|
+
if (!store) return [];
|
|
65
|
+
try {
|
|
66
|
+
return parseBrowserHistory(store.getItem(BROWSER_HISTORY_STORAGE_KEY));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function saveBrowserHistory(entries: BrowserHistoryEntry[]): void {
|
|
73
|
+
const store = storage();
|
|
74
|
+
if (!store) return;
|
|
75
|
+
try {
|
|
76
|
+
store.setItem(BROWSER_HISTORY_STORAGE_KEY, JSON.stringify(entries.slice(0, BROWSER_HISTORY_MAX)));
|
|
77
|
+
} catch {
|
|
78
|
+
// Storage may be disabled (private mode); fall through silently.
|
|
79
|
+
}
|
|
80
|
+
}
|