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.
Files changed (135) hide show
  1. package/README.md +2 -0
  2. package/dist/src/agent/codex-app-client.js +69 -1
  3. package/dist/src/agent/codex-app-client.js.map +1 -1
  4. package/dist/src/agent/cursor-runtime.js +9 -0
  5. package/dist/src/agent/cursor-runtime.js.map +1 -1
  6. package/dist/src/agent/pi-runtime.js +1 -1
  7. package/dist/src/agent/pi-runtime.js.map +1 -1
  8. package/dist/src/agent/prompt-context.js +39 -0
  9. package/dist/src/agent/prompt-context.js.map +1 -1
  10. package/dist/src/agent/spawn.js +138 -20
  11. package/dist/src/agent/spawn.js.map +1 -1
  12. package/dist/src/browser/web-ai/capability-observation-presets.js +4 -0
  13. package/dist/src/browser/web-ai/capability-observation-presets.js.map +1 -1
  14. package/dist/src/browser/web-ai/chatgpt-model.js +9 -5
  15. package/dist/src/browser/web-ai/chatgpt-model.js.map +1 -1
  16. package/dist/src/browser/web-ai/grok-model.js +18 -5
  17. package/dist/src/browser/web-ai/grok-model.js.map +1 -1
  18. package/dist/src/cli/claude-models.js +6 -0
  19. package/dist/src/cli/claude-models.js.map +1 -1
  20. package/dist/src/cli/handlers-runtime.js +29 -4
  21. package/dist/src/cli/handlers-runtime.js.map +1 -1
  22. package/dist/src/cli/registry.js +56 -15
  23. package/dist/src/cli/registry.js.map +1 -1
  24. package/dist/src/code-mode/model-options.js +4 -4
  25. package/dist/src/code-mode/model-options.js.map +1 -1
  26. package/dist/src/core/config.js +1 -1
  27. package/dist/src/core/config.js.map +1 -1
  28. package/dist/src/core/db.js +19 -0
  29. package/dist/src/core/db.js.map +1 -1
  30. package/dist/src/core/runtime-settings.js +11 -2
  31. package/dist/src/core/runtime-settings.js.map +1 -1
  32. package/dist/src/orchestrator/attestation.js +66 -0
  33. package/dist/src/orchestrator/attestation.js.map +1 -1
  34. package/dist/src/orchestrator/gateway.js +11 -3
  35. package/dist/src/orchestrator/gateway.js.map +1 -1
  36. package/dist/src/orchestrator/pipeline.js +5 -0
  37. package/dist/src/orchestrator/pipeline.js.map +1 -1
  38. package/dist/src/routes/command.js +4 -1
  39. package/dist/src/routes/command.js.map +1 -1
  40. package/dist/src/routes/orchestrate.js +14 -2
  41. package/dist/src/routes/orchestrate.js.map +1 -1
  42. package/dist/src/slack/commands.js +1 -0
  43. package/dist/src/slack/commands.js.map +1 -1
  44. package/dist/src/telegram/bot.js +3 -2
  45. package/dist/src/telegram/bot.js.map +1 -1
  46. package/package.json +5 -1
  47. package/public/dist/assets/{Agent-BjxJRsKG.js → Agent-kyyjcnqC.js} +1 -1
  48. package/public/dist/assets/BrowserPanel-0b3deLGR.js +3 -0
  49. package/public/dist/assets/BrowserPanel-D7SnTts8.css +1 -0
  50. package/public/dist/assets/{Heartbeat-Ca8DmJcw.js → Heartbeat-CD1R95Kv.js} +1 -1
  51. package/public/dist/assets/{Memory-BY88ur_h.js → Memory-CpZIdE1o.js} +1 -1
  52. package/public/dist/assets/{ModelProvider-mtAtYLmW.js → ModelProvider-tL42E5dy.js} +1 -1
  53. package/public/dist/assets/agent-meta-KPnVam9G.js +1 -0
  54. package/public/dist/assets/{app-CYayckBa.js → app-BvRV-aEt.js} +4 -4
  55. package/public/dist/assets/bgtask-badge-DLqpImpn.js +1 -0
  56. package/public/dist/assets/{bgtask-badge-CT6s6rND.js → bgtask-badge-FiEB3FJm.js} +1 -1
  57. package/public/dist/assets/constants-BfYYzHNr.js +1 -0
  58. package/public/dist/assets/{employees-DCju7V1x.js → employees-86F_D_Ce.js} +1 -1
  59. package/public/dist/assets/iframe-renderer-C_swzquc.js +1 -0
  60. package/public/dist/assets/{iframe-renderer-LZp7Nqm1.js → iframe-renderer-eozJpgjk.js} +2 -2
  61. package/public/dist/assets/manager-BnmWx9xL.js +13 -0
  62. package/public/dist/assets/manager-Cmu0aKPb.css +1 -0
  63. package/public/dist/assets/{memory-V5caYbpS.js → memory-DX7U3WV_.js} +1 -1
  64. package/public/dist/assets/memory-DfPbqPTD.js +1 -0
  65. package/public/dist/assets/{message-history-Bp_sUDSx.js → message-history-B_fIGorC.js} +1 -1
  66. package/public/dist/assets/message-history-BhSsLjis.js +1 -0
  67. package/public/dist/assets/{render-Cd4frZ2U.js → render-fKAuISeM.js} +1 -1
  68. package/public/dist/assets/settings-BNqSR9Zr.js +1 -0
  69. package/public/dist/assets/{settings-a0blK1fr.js → settings-CH6exiZf.js} +1 -1
  70. package/public/dist/assets/{settings-core-C5qo8giT.js → settings-core-DqKyyxbQ.js} +1 -1
  71. package/public/dist/assets/settings-core-x7CmBZq1.js +1 -0
  72. package/public/dist/assets/{sidebar-FbI4udzs.js → sidebar-qyLy9Og8.js} +3 -3
  73. package/public/dist/assets/skills-C7Yi3LIq.js +1 -0
  74. package/public/dist/assets/{skills-DeG9lWF2.js → skills-DAhOtbfP.js} +1 -1
  75. package/public/dist/assets/{slash-commands-FoQuZXga.js → slash-commands-BYH6YVS4.js} +1 -1
  76. package/public/dist/assets/slash-commands-DEbJk3sj.js +1 -0
  77. package/public/dist/assets/{trace-drawer-CWs8ydMO.js → trace-drawer-nyAJ3Bm_.js} +1 -1
  78. package/public/dist/assets/{ui-Cd8etFwN.js → ui-C62w6bJS.js} +1 -1
  79. package/public/dist/assets/ui-CBErZPBp.js +1 -0
  80. package/public/dist/index.html +1 -1
  81. package/public/dist/manager/index.html +2 -2
  82. package/public/js/constants.ts +22 -5
  83. package/public/js/features/settings-types.ts +5 -0
  84. package/public/manager/src/App.tsx +1 -1
  85. package/public/manager/src/AppChrome.tsx +18 -3
  86. package/public/manager/src/SidebarRailRouter.tsx +47 -1
  87. package/public/manager/src/browser-panel/BrowserPanel.tsx +151 -57
  88. package/public/manager/src/browser-panel/browser-address-bar.tsx +53 -0
  89. package/public/manager/src/browser-panel/browser-address-state.ts +41 -0
  90. package/public/manager/src/browser-panel/browser-favicon.ts +18 -0
  91. package/public/manager/src/browser-panel/browser-history-store.ts +80 -0
  92. package/public/manager/src/browser-panel/browser-panel.css +141 -24
  93. package/public/manager/src/components/CommandBar.tsx +17 -5
  94. package/public/manager/src/components/InstanceGroups.tsx +143 -23
  95. package/public/manager/src/components/InstanceNavigator.tsx +55 -2
  96. package/public/manager/src/components/InstanceRow.tsx +51 -3
  97. package/public/manager/src/components/SidebarRail.tsx +10 -10
  98. package/public/manager/src/components/SidebarResizeHandle.tsx +22 -0
  99. package/public/manager/src/components/WorkspaceLayout.tsx +22 -8
  100. package/public/manager/src/components/instance-row-status.ts +48 -0
  101. package/public/manager/src/components/sidebar-keyboard.ts +129 -0
  102. package/public/manager/src/dashboard-settings/DashboardSettingsWorkspace.tsx +42 -0
  103. package/public/manager/src/help/helpContent.tsx +1 -0
  104. package/public/manager/src/hooks/useSidebarGroupCollapse.ts +46 -0
  105. package/public/manager/src/hooks/useSidebarWidth.ts +88 -0
  106. package/public/manager/src/hooks/useWindowFullscreen.ts +23 -0
  107. package/public/manager/src/manager-components.css +206 -31
  108. package/public/manager/src/manager-layout.css +30 -19
  109. package/public/manager/src/manager-p0-1-1.css +72 -36
  110. package/public/manager/src/manager-polish.css +6 -45
  111. package/public/manager/src/manager-shortcut-runner.ts +37 -1
  112. package/public/manager/src/manager-shortcuts.ts +21 -0
  113. package/public/manager/src/manager-tokens.css +190 -127
  114. package/public/manager/src/panels/PanelResizer.tsx +18 -2
  115. package/public/manager/src/panels/desktop-bridge.ts +12 -1
  116. package/public/manager/src/panels/panels.css +32 -32
  117. package/public/manager/src/settings/pages/Agent.tsx +26 -0
  118. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +6 -5
  119. package/public/manager/src/styles.css +15 -7
  120. package/public/manager/src/types.ts +5 -1
  121. package/public/dist/assets/BrowserPanel-CsYKexNf.js +0 -3
  122. package/public/dist/assets/BrowserPanel-DpntR8EJ.css +0 -1
  123. package/public/dist/assets/agent-meta-_Idad3qq.js +0 -1
  124. package/public/dist/assets/bgtask-badge-CidVvjWb.js +0 -1
  125. package/public/dist/assets/constants-DK-21dMG.js +0 -1
  126. package/public/dist/assets/iframe-renderer-aFXUtTXX.js +0 -1
  127. package/public/dist/assets/manager-CwqrZ2MH.js +0 -13
  128. package/public/dist/assets/manager-LkGs6qEJ.css +0 -1
  129. package/public/dist/assets/memory-BcZdf_oz.js +0 -1
  130. package/public/dist/assets/message-history-DrEPUdTy.js +0 -1
  131. package/public/dist/assets/settings-core-Cm-CmVLA.js +0 -1
  132. package/public/dist/assets/settings-mbGEtP5B.js +0 -1
  133. package/public/dist/assets/skills-G8KUeHBV.js +0 -1
  134. package/public/dist/assets/slash-commands-BYv3yboz.js +0 -1
  135. 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 editingTabIdRef = useRef<string | null>(null);
294
- const inputDraftRef = useRef<{ tabId: string; value: string } | null>(null);
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 = inputDraftRef.current?.tabId === activeTab.id
686
- ? inputDraftRef.current.value
687
- : inputRef.current?.value ?? activeTab.inputUrl;
688
- editingTabIdRef.current = null;
689
- inputDraftRef.current = null;
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
- }, [activeTab.id, activeTab.inputUrl, openUrlInTab]);
735
+ inputRef.current?.blur();
736
+ }, [activeTab.id, addressState, dispatchAddress, openUrlInTab]);
692
737
 
693
- const markUrlEditing = useCallback(() => {
694
- editingTabIdRef.current = activeTab.id;
695
- inputDraftRef.current = {
696
- tabId: activeTab.id,
697
- value: inputRef.current?.value ?? activeTab.inputUrl,
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
- const clearUrlEditingSoon = useCallback(() => {
702
- window.setTimeout(() => {
703
- if (document.activeElement !== inputRef.current && editingTabIdRef.current === activeTab.id) {
704
- editingTabIdRef.current = null;
705
- refreshNavState(activeTab.id);
706
- }
707
- }, 150);
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
- <span className="browser-tab-title">{tab.loading ? 'Loading...' : tab.title}</span>
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="Reload" data-tooltip="Reload" disabled={!canUseElectronWebview} onClick={() => webviewRefs.current.get(activeTab.id)?.reload()}>↻</button>
1025
- <input
1026
- ref={inputRef}
1027
- className="browser-url-input"
1028
- type="text"
1029
- value={activeTab.inputUrl}
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
- {(activeTab.blocked || activeTab.error || activeTab.loading || activeTab.status) && (
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 ?? (activeTab.loading ? 'Loading...' : activeTab.status ?? 'Blocked')}
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
+ }