From 6e454215e85295ad5a09abe450b0e526113da5d8 Mon Sep 17 00:00:00 2001 From: wsp Date: Sun, 20 Sep 2026 10:27:19 +0800 Subject: [PATCH 1/4] fix(web-ui): Copy complete command output Copy all output available to the command card regardless of terminal scroll position or line wrapping. Remove the viewport-based copy dependency and cover completed, running, and cancelled output across ExecCommand, WriteStdin, and ExecControl. --- .../ExecProcessToolCardView.test.tsx | 27 ++++++++++++++++++- .../tool-cards/ExecProcessToolCardView.tsx | 9 +------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx index be0b0a608d..e1b241e172 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx @@ -6,6 +6,11 @@ import { JSDOM } from 'jsdom'; import { ExecProcessToolCardView, type ExecProcessCardModel } from './ExecProcessToolCardView'; import type { FlowToolItem } from '../types/flow-chat'; +import { copyTextToClipboard } from '@/shared/utils/textSelection'; + +vi.mock('@/shared/utils/textSelection', () => ({ + copyTextToClipboard: vi.fn().mockResolvedValue(true), +})); globalThis.IS_REACT_ACT_ENVIRONMENT = true; @@ -38,7 +43,7 @@ vi.mock('@/tools/terminal/components/LazyTerminalOutputRenderer', () => ({ { getVisibleText: () => string }, { content: string; className?: string; maxRows?: number } >(({ content, className, maxRows }, ref) => { - React.useImperativeHandle(ref, () => ({ getVisibleText: () => content }), [content]); + React.useImperativeHandle(ref, () => ({ getVisibleText: () => content.slice(-3) }), [content]); return
{content}
; }), })); @@ -110,6 +115,26 @@ describe.each([ const expandedSurface = '[data-openbitfun-part="surface"][data-openbitfun-state~="expanded"]'; + it.each(['completed', 'running', 'cancelled'] as const)('copies the complete %s output beyond the terminal viewport', async (status) => { + const output = `${'long output '.repeat(30)}\nlast line\r\n`; + act(() => { + root.render(); + }); + if (!container.querySelector(expandedSurface)) { + act(() => { + container.querySelector('[data-openbitfun-part="surface"][data-openbitfun-attention="prominent"]')!.click(); + }); + } + vi.mocked(copyTextToClipboard).mockClear(); + await act(async () => { + container.querySelector('button[aria-label="toolCards.execProcess.copyOutput"]')!.click(); + }); + expect(copyTextToClipboard).toHaveBeenCalledExactlyOnceWith(output); + }); + it.each(['_progressLogs', '_progressMessage'])('expands only when live output arrives through %s, then collapses on completion', (field) => { vi.useFakeTimers(); for (const status of ['preparing', 'streaming', 'running', 'receiving'] as const) { diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx index a9b1b07340..ac13216867 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import type { FlowToolItem } from '../types/flow-chat'; import { LazyTerminalOutputRenderer, - type TerminalOutputRendererHandle, } from '@/tools/terminal/components/LazyTerminalOutputRenderer'; import { CommandToolCard, @@ -117,7 +116,6 @@ export const ExecProcessToolCardView: React.FC = ( const [isExpanded, setIsExpandedState] = useState(() => getInitialExpandedState(status, hasLiveOutput)); const userToggledRef = useRef(false); const autoExpandedAtRef = useRef(null); - const outputRendererRef = useRef(null); const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ toolId, toolName: toolItem.toolName, @@ -213,13 +211,9 @@ export const ExecProcessToolCardView: React.FC = ( return ''; }, [isRunning, liveOutput, model.resultOutput, status]); - const getVisibleOutputText = useCallback(() => { - return outputRendererRef.current?.getVisibleText() ?? getOutputText(); - }, [getOutputText]); - const renderCopyOutputButton = () => ( = ( onToggle={toggleExpanded} output={outputText ? ( From c90b0c60272091982980fca0b3f759ff41367b3e Mon Sep 17 00:00:00 2001 From: wsp Date: Sun, 20 Sep 2026 11:21:24 +0800 Subject: [PATCH 2/4] fix(desktop): Preserve WebView size when minimized Prevent taskbar minimization from shrinking the main Windows WebView and reflowing the page into a tiny viewport before restoration. Let the desktop host synchronize valid, non-minimized window bounds instead of using Tauri's multi-WebView autoresizer for the main view. Keep embedded browser layout ownership unchanged and document focused verification for the new geometry guard. Validation: Rust test compilation passed; the focused regression test passed with the desktop manifest embedded in a temporary test copy. The user confirmed that taskbar restoration no longer flickers. --- src/apps/desktop/AGENTS.md | 6 ++ src/apps/desktop/src/appearance.rs | 4 + src/apps/desktop/src/lib.rs | 4 + .../desktop/src/window_webview_geometry.rs | 76 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 src/apps/desktop/src/window_webview_geometry.rs diff --git a/src/apps/desktop/AGENTS.md b/src/apps/desktop/AGENTS.md index 63d11b46d4..f9bbb5294e 100644 --- a/src/apps/desktop/AGENTS.md +++ b/src/apps/desktop/AGENTS.md @@ -167,6 +167,12 @@ For peer system-info response compatibility, run `cargo test -p openbitfun-desktop --lib system_info_home_contract`. For window geometry recovery, legacy state compatibility, and snapshot persistence, run `cargo test -p openbitfun-desktop --lib window_state_support::tests`. +For Windows main-WebView minimize/restore size filtering, run +`cargo test -p openbitfun-desktop --no-default-features --lib window_webview_geometry::tests`. +The desktop host owns main-WebView resizing on Windows so minimized client bounds +do not trigger page reflow; embedded browser WebViews keep their existing owners. +After rebuilding, manually compare taskbar minimize/restore with tray hide/show +while a session is open, and check normal resize, maximize, and monitor DPI changes. For the matching startup wiring contract, run `pnpm --dir src/web-ui run test:run src/app/startup/startupPerformanceContract.test.ts`. For native sidebar material and appearance bootstrap, run diff --git a/src/apps/desktop/src/appearance.rs b/src/apps/desktop/src/appearance.rs index f7991441c1..443c2e2d7d 100644 --- a/src/apps/desktop/src/appearance.rs +++ b/src/apps/desktop/src/appearance.rs @@ -668,6 +668,10 @@ pub fn create_main_window( let build_started_at = Instant::now(); match builder.build() { Ok(window) => { + #[cfg(target_os = "windows")] + if let Err(error) = crate::window_webview_geometry::install(&window) { + error!("Failed to install main WebView geometry protection: {error}"); + } let reapply_maximized = crate::restore_main_window_state(&window); crate::webview_recovery::install(&window); startup_trace.record_elapsed_step("native_window", "webview_build", build_started_at); diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 69a0b70a1c..f63c1ecdb3 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -36,6 +36,8 @@ pub mod startup_trace; pub mod tray; mod webview_recovery; mod window_state_support; +#[cfg(target_os = "windows")] +mod window_webview_geometry; use openbitfun_agent_runtime::sdk::{attach_session_event_cursor, SessionEventJournal}; use openbitfun_core::agentic::tools::computer_use_capability::set_computer_use_desktop_available; @@ -1183,6 +1185,8 @@ pub async fn run() { }) .on_window_event({ move |window, event| { + #[cfg(target_os = "windows")] + window_webview_geometry::handle_event(window, event); if window.label() == "main" && !MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.load(Ordering::SeqCst) && matches!(event, tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_)) diff --git a/src/apps/desktop/src/window_webview_geometry.rs b/src/apps/desktop/src/window_webview_geometry.rs new file mode 100644 index 0000000000..e4df093eff --- /dev/null +++ b/src/apps/desktop/src/window_webview_geometry.rs @@ -0,0 +1,76 @@ +//! Keep the main WebView's last usable bounds while Windows minimizes its host. +//! +//! Tauri's unstable multi-WebView autoresizer forwards the minimized client +//! rectangle (for example 215x26) through Wry's explicit set_bounds path, which +//! bypasses Wry's WM_SIZE/SIZE_MINIMIZED guard. Own only the main WebView resize +//! here; child browser previews retain their existing layout owners. + +use tauri::Manager; + +pub(crate) fn install(window: &tauri::WebviewWindow) -> Result<(), String> { + let webview: &tauri::Webview = window.as_ref(); + webview + .set_auto_resize(false) + .map_err(|error| format!("Failed to disable main WebView autoresize: {error}")) +} + +fn usable_size( + minimized: bool, + size: tauri::PhysicalSize, +) -> Option> { + (!minimized && size.width > 0 && size.height > 0).then_some(size) +} + +pub(crate) fn handle_event(window: &tauri::Window, event: &tauri::WindowEvent) { + if window.label() != "main" + || !matches!( + event, + tauri::WindowEvent::Resized(_) | tauri::WindowEvent::ScaleFactorChanged { .. } + ) + { + return; + } + // Read current host geometry, not a potentially stale queued event payload. + // On query failure retain the last bounds rather than guessing a size. + let result = (|| -> tauri::Result<()> { + let minimized = window.is_minimized()?; + if minimized { + return Ok(()); + } + let Some(size) = usable_size(minimized, window.inner_size()?) else { + return Ok(()); + }; + if let Some(webview) = window.app_handle().get_webview("main") { + webview.set_size(size)?; + } + Ok(()) + })(); + if let Err(error) = result { + log::warn!("Failed to synchronize main WebView geometry: {error}"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn minimize_restore_retains_last_usable_bounds() { + let normal = tauri::PhysicalSize::new(2488, 1600); + let mut bounds = normal; + for (minimized, size) in [ + (true, tauri::PhysicalSize::new(215, 26)), + (false, tauri::PhysicalSize::new(0, 0)), + ] { + if let Some(next) = usable_size(minimized, size) { + bounds = next; + } + assert_eq!(bounds, normal); + } + let restored = tauri::PhysicalSize::new(1920, 1080); + assert_eq!(usable_size(false, restored), Some(restored)); + // Small but legitimate non-minimized windows must not be size-clamped. + let small = tauri::PhysicalSize::new(215, 26); + assert_eq!(usable_size(false, small), Some(small)); + } +} From a037efdf90fd3e52516b89e856bc161c2c379a74 Mon Sep 17 00:00:00 2001 From: wsp Date: Sun, 20 Sep 2026 12:38:56 +0800 Subject: [PATCH 3/4] fix(markdown): Stabilize code block rendering Wait for the Prism AST engine before replacing the fallback to avoid an intermediate line-number layout shifting code on first session open. Match fallback text and line-number colors to the Prism theme and keep line numbers italic throughout the transition. Add coverage for engine readiness and concurrent loader requests. --- .../markdown/MarkdownRenderer.tsx | 9 ++++--- .../utils/syntaxHighlighterLoader.test.ts | 24 +++++++++++++++++++ .../shared/utils/syntaxHighlighterLoader.ts | 13 ++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/web-ui/src/shared/utils/syntaxHighlighterLoader.test.ts diff --git a/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx b/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx index bd599fcbea..6b6677e940 100644 --- a/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx +++ b/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx @@ -807,6 +807,7 @@ const CodeBlockFallback: React.FC = ({ minWidth: '3em', paddingRight: '1em', textAlign: 'right', + fontStyle: 'italic', color: gutterColor, userSelect: 'none', whiteSpace: 'pre', @@ -1307,10 +1308,11 @@ export const MarkdownRenderer = React.memo(({ const codeTagStyle: React.CSSProperties = { fontFamily: 'var(--openbitfun-type-code-md-font-family)', fontWeight: 'var(--openbitfun-type-code-md-font-weight)', + color: syntaxThemeRef.current['code[class*="language-"]']?.color, }; - const gutterColor = isLightRef.current - ? 'color-mix(in srgb, var(--openbitfun-color-content-on-light) 40%, var(--openbitfun-color-content-on-dark))' - : 'color-mix(in srgb, var(--openbitfun-color-content-on-dark) 40%, var(--openbitfun-color-content-on-light))'; + // Prism's line-number nodes use the comment token, applied after + // lineNumberStyle. Reuse that final color in the fallback as well. + const gutterColor = syntaxThemeRef.current.comment.color as string; return (
@@ -1333,6 +1335,7 @@ export const MarkdownRenderer = React.memo(({ codeTagProps={{ style: codeTagStyle }} lineNumberStyle={{ color: gutterColor, + fontStyle: 'italic', paddingRight: '1em', textAlign: 'right', userSelect: 'none', diff --git a/src/web-ui/src/shared/utils/syntaxHighlighterLoader.test.ts b/src/web-ui/src/shared/utils/syntaxHighlighterLoader.test.ts new file mode 100644 index 0000000000..5596cad98a --- /dev/null +++ b/src/web-ui/src/shared/utils/syntaxHighlighterLoader.test.ts @@ -0,0 +1,24 @@ +import { expect, it, vi } from 'vitest'; + +it('keeps the fallback available until the AST engine is ready and shares concurrent loads', async () => { + vi.resetModules(); + let finishEngine!: () => void; + const engineReady = new Promise(resolve => { finishEngine = resolve; }); + const preload = vi.fn(() => engineReady); + const component = Object.assign(() => null, { preload }); + vi.doMock('react-syntax-highlighter/dist/esm/prism-async-light', () => ({ default: component })); + try { + const loader = await import('./syntaxHighlighterLoader'); + const first = loader.loadPrismSyntaxHighlighter(); + const second = loader.loadPrismSyntaxHighlighter(); + expect(second).toBe(first); + await vi.waitFor(() => expect(preload).toHaveBeenCalledTimes(1)); + expect(loader.getLoadedPrismSyntaxHighlighter()).toBeNull(); + finishEngine(); + expect(await first).toBe(component); + expect(loader.getLoadedPrismSyntaxHighlighter()).toBe(component); + } finally { + vi.doUnmock('react-syntax-highlighter/dist/esm/prism-async-light'); + vi.resetModules(); + } +}); diff --git a/src/web-ui/src/shared/utils/syntaxHighlighterLoader.ts b/src/web-ui/src/shared/utils/syntaxHighlighterLoader.ts index e1c2ce937c..0eb6f85c83 100644 --- a/src/web-ui/src/shared/utils/syntaxHighlighterLoader.ts +++ b/src/web-ui/src/shared/utils/syntaxHighlighterLoader.ts @@ -11,8 +11,17 @@ export function getLoadedPrismSyntaxHighlighter(): SyntaxHighlighterComponent | export function loadPrismSyntaxHighlighter(): Promise { prismSyntaxHighlighterPromise ??= import('react-syntax-highlighter/dist/esm/prism-async-light').then( - (module) => { - prismSyntaxHighlighterComponent = module.default as SyntaxHighlighterComponent; + async (module) => { + // The async component module can resolve before its AST engine. Keep the + // caller's fallback until the engine is ready to avoid its interim, + // separate line-number column replacing the final inline-number layout. + // The package implements preload on async variants, but its shared + // declaration only describes the synchronous component API. + const component = module.default as unknown as SyntaxHighlighterComponent & { + preload: () => Promise; + }; + await component.preload(); + prismSyntaxHighlighterComponent = component; return prismSyntaxHighlighterComponent; }, ); From 083f7fd51e82512291d14e9aee75792639a18ae8 Mon Sep 17 00:00:00 2001 From: wsp Date: Sun, 20 Sep 2026 12:59:28 +0800 Subject: [PATCH 4/4] fix(web): clean up lint warnings Remove unnecessary mutable overlay cleanup bindings and move shared hooks and helpers out of component modules so ESLint can enforce the intended boundaries. --- src/web-ui/src/app/components/NavPanel/MainNav.tsx | 6 ++---- .../components/AssistantSessionCreateMenu.tsx | 6 ++---- .../NavPanel/components/DeviceStatusControl.tsx | 3 +-- .../NavPanel/components/PersistentFooterActions.tsx | 3 ++- .../components/WorkspaceSessionFilterMenu.tsx | 6 ++---- .../NavPanel/sections/sessions/SessionsSection.tsx | 3 +-- .../NavPanel/sections/workspaces/WorkspaceItem.tsx | 3 +-- .../scheduled-jobs/DateTimePickerPopover.tsx | 6 ++---- .../ecosystem-compatibility/EcosystemAccounts.tsx | 6 ------ .../ecosystem-compatibility/ExternalAgentContent.tsx | 3 ++- .../ecosystemCompatibilityModel.ts | 5 +++++ .../app/scenes/miniapps/views/MiniAppGalleryView.tsx | 6 ++---- .../app/scenes/miniapps/views/MiniAppLibraryView.tsx | 6 ++---- .../scenes/profile/views/AssistantAvatarPicker.tsx | 6 ++---- .../src/features/dispatch/DispatchTargetPicker.tsx | 6 ++---- .../market-account/AccountIdentityControls.tsx | 6 ++---- .../src/features/ssh-remote/RemoteFileBrowser.tsx | 3 +-- .../src/flow_chat/components/AcpModeSelector.tsx | 3 +-- .../src/flow_chat/components/ChatContextPicker.tsx | 6 ++---- .../flow_chat/components/ChatInputWorkspaceStrip.tsx | 12 ++++-------- .../flow_chat/components/HarnessProfileSelector.tsx | 6 ++---- .../flow_chat/components/ReasoningPresetSelector.tsx | 3 +-- src/web-ui/src/flow_chat/components/WelcomePanel.tsx | 6 ++---- .../components/modern/ExportImageButton.tsx | 6 ++---- .../flow_chat/components/modern/FlowChatHeader.tsx | 6 ++---- .../flow_chat/components/modern/ModelRoundItem.tsx | 6 ++---- .../components/modern/SessionTreePopover.tsx | 9 +++------ .../flow_chat/tool-cards/ToolTimeoutIndicator.tsx | 6 ++---- .../src/infrastructure/markdown/MarkdownRenderer.tsx | 2 -- .../peer-device/DeviceSurfaceSwitcher.tsx | 3 +-- .../src/infrastructure/update/UpdateIndicator.tsx | 8 +------- .../src/infrastructure/update/useHasAppUpdate.ts | 7 +++++++ .../src/tools/editor/meditor/utils/tiptapMarkdown.ts | 2 +- 33 files changed, 64 insertions(+), 110 deletions(-) create mode 100644 src/web-ui/src/infrastructure/update/useHasAppUpdate.ts diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 68af881c58..01fd4f55d5 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -192,8 +192,6 @@ const MainNav: React.FC = () => { }, [sshRemote]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!workspaceMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node | null; @@ -205,8 +203,8 @@ const MainNav: React.FC = () => { const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && !isImeOwnedKeyboardEvent(event)) closeWorkspaceMenu(); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(workspaceMenuRef, 'mousedown', handleClickOutside); - removeOverlayKeydown1 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleEscape); + const removeOverlayMousedown0 = subscribeOverlayInteraction(workspaceMenuRef, 'mousedown', handleClickOutside); + const removeOverlayKeydown1 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleEscape); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx index be3dadc59e..685ff467a0 100644 --- a/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/AssistantSessionCreateMenu.tsx @@ -49,8 +49,6 @@ const AssistantSessionCreateMenu: React.FC = ({ const closeMenu = useCallback(() => setMenuOpen(false), []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!menuOpen) return; const handleMouseDown = (event: MouseEvent) => { @@ -67,8 +65,8 @@ const AssistantSessionCreateMenu: React.FC = ({ if (event.key === 'Escape' && !isImeOwnedKeyboardEvent(event)) closeMenu(); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handleMouseDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleEscape); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handleMouseDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleEscape); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx index 42b7e5dde4..7569c57c60 100644 --- a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx @@ -179,7 +179,6 @@ const DeviceStatusControl: React.FC = ({ }); useEffect(() => { - let removeOverlayKeydown0: (() => void) | undefined; if (!open) return undefined; void refresh(); const onKeyDown = (event: KeyboardEvent) => { @@ -188,7 +187,7 @@ const DeviceStatusControl: React.FC = ({ triggerRef.current?.focus(); } }; - removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown); + const removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown); return () => removeOverlayKeydown0?.(); }, [onOpenChange, open, refresh]); diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx index 89f2164a7d..9020e56888 100644 --- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx @@ -30,7 +30,8 @@ import { useSettingsStore } from '@/app/scenes/settings/settingsStore'; import { PeerConnectionStatus } from '@/infrastructure/peer-device/PeerConnectionStatus'; import DeviceStatusControl from './DeviceStatusControl'; import AppearanceQuickSwitchMenuItem from './AppearanceQuickSwitchMenuItem'; -import { UpdateIndicator, useHasAppUpdate } from '@/infrastructure/update/UpdateIndicator'; +import { UpdateIndicator } from '@/infrastructure/update/UpdateIndicator'; +import { useHasAppUpdate } from '@/infrastructure/update/useHasAppUpdate'; import { UpdateDownloadIndicator } from '@/infrastructure/update/UpdateDownloadIndicator'; import { UpdateMenuItems } from '@/infrastructure/update/UpdateMenuItems'; import { useUpdateInstallStore } from '@/infrastructure/update/updateInstallStore'; diff --git a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx index e3ef6e69bc..9a5d7702c4 100644 --- a/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/WorkspaceSessionFilterMenu.tsx @@ -92,8 +92,6 @@ const WorkspaceSessionFilterMenu: React.FC = () => { }, []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!open) return; updatePosition(); requestAnimationFrame(updatePosition); @@ -103,8 +101,8 @@ const WorkspaceSessionFilterMenu: React.FC = () => { close(); }; const handleKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && close(); - removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', updatePosition, true); return () => { diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 605661dfbb..0f677754c5 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -688,7 +688,6 @@ const SessionsSection: React.FC = ({ }, []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!openMenuSessionId) return; const handleOutside = (event: MouseEvent) => { if (!sessionMenuPopoverRef.current?.contains(event.target as Node) @@ -696,7 +695,7 @@ const SessionsSection: React.FC = ({ closeSessionMenu(); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(sessionMenuPopoverRef, 'mousedown', handleOutside); + const removeOverlayMousedown0 = subscribeOverlayInteraction(sessionMenuPopoverRef, 'mousedown', handleOutside); return () => removeOverlayMousedown0?.(); }, [closeSessionMenu, openMenuSessionId]); diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index c76f4e955d..b0001e0a69 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -465,7 +465,6 @@ const WorkspaceItem: React.FC = ({ }, []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!menuOpen) return; const handleOutside = (event: MouseEvent) => { const target = event.target as Node; @@ -476,7 +475,7 @@ const WorkspaceItem: React.FC = ({ setMenuOpen(false); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(menuPopoverRef, 'mousedown', handleOutside); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuPopoverRef, 'mousedown', handleOutside); return () => removeOverlayMousedown0?.(); }, [menuOpen]); diff --git a/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx b/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx index 8769e33f0c..b82e37cd86 100644 --- a/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx +++ b/src/web-ui/src/app/components/scheduled-jobs/DateTimePickerPopover.tsx @@ -94,8 +94,6 @@ const DateTimePickerPopover: React.FC = ({ }, [anchorRef]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; const handlePointerDown = (event: MouseEvent) => { const target = event.target as Node | null; if (!target) return; @@ -107,8 +105,8 @@ const DateTimePickerPopover: React.FC = ({ if (event.key === 'Escape') onClose(); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(popoverRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKeyDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(popoverRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKeyDown); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemAccounts.tsx b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemAccounts.tsx index 3d8193aeba..ee5bdbad29 100644 --- a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemAccounts.tsx +++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemAccounts.tsx @@ -6,12 +6,6 @@ import { useI18n } from '@/infrastructure/i18n'; import { getActiveSurfaceScope } from '@/infrastructure/peer-device/deviceSurface'; import { useSettingsStore } from '@/app/scenes/settings/settingsStore'; import { useSceneStore } from '@/app/stores/sceneStore'; -import type { EcosystemProductId } from './ecosystemCompatibilityModel'; - -/** Native subscription connections are separate from external credential discovery. */ -export function ecosystemAccountProvider(product: EcosystemProductId): 'codex' | 'opencode' | undefined { - return product === 'codex' || product === 'opencode' ? product : undefined; -} interface Props { provider: 'codex' | 'opencode'; diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/ExternalAgentContent.tsx b/src/web-ui/src/app/scenes/ecosystem-compatibility/ExternalAgentContent.tsx index 5697074274..d94758442a 100644 --- a/src/web-ui/src/app/scenes/ecosystem-compatibility/ExternalAgentContent.tsx +++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/ExternalAgentContent.tsx @@ -3,7 +3,8 @@ import { Alert, Button, Checkbox, DialogBody, DialogClose, DialogFooter, DialogH import { EcosystemDialog as Dialog } from './EcosystemDialog'; import { EcosystemBatchLayout } from './EcosystemBatchLayout'; import EcosystemPets from './EcosystemPets'; -import EcosystemAccounts, { ecosystemAccountProvider } from './EcosystemAccounts'; +import EcosystemAccounts from './EcosystemAccounts'; +import { ecosystemAccountProvider } from './ecosystemCompatibilityModel'; import { presentEcosystemContent } from './ecosystemContentPresentation'; import { ecosystemDiscoveryCache, rememberEcosystemHooks, rememberEcosystemSkills } from './ecosystemDiscoveryCache'; import { importErrorMessage } from './ecosystemSkillImport'; diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/ecosystemCompatibilityModel.ts b/src/web-ui/src/app/scenes/ecosystem-compatibility/ecosystemCompatibilityModel.ts index 5aa74d3b89..7581f31027 100644 --- a/src/web-ui/src/app/scenes/ecosystem-compatibility/ecosystemCompatibilityModel.ts +++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/ecosystemCompatibilityModel.ts @@ -484,3 +484,8 @@ export function buildEcosystemImportItems( export function totalDiscoveredAssets(counts: CompatibilityCapabilityCounts): number { return counts.command + counts.tool + counts.subagent + counts.mcp; } + +/** Native subscription connections are separate from external credential discovery. */ +export function ecosystemAccountProvider(product: EcosystemProductId): 'codex' | 'opencode' | undefined { + return product === 'codex' || product === 'opencode' ? product : undefined; +} diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx index fcf52eaee1..e893588243 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx @@ -105,8 +105,6 @@ const MiniAppGalleryView: React.FC = ({ tabs }) => { const closeImportMenu = useCallback(() => setImportMenuOpen(false), []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!importMenuOpen) return; const handlePointerDown = (event: MouseEvent) => { @@ -125,8 +123,8 @@ const MiniAppGalleryView: React.FC = ({ tabs }) => { requestAnimationFrame(() => importTriggerRef.current?.focus()); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(importMenuRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(importMenuRef, 'keydown', handleEscape); + const removeOverlayMousedown0 = subscribeOverlayInteraction(importMenuRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(importMenuRef, 'keydown', handleEscape); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppLibraryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppLibraryView.tsx index f2fb382aad..edf6505f7d 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppLibraryView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppLibraryView.tsx @@ -162,8 +162,6 @@ const MiniAppLibraryContent: React.FC = ({ tabs }) => { const closeImportMenu = useCallback(() => setImportMenuOpen(false), []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!importMenuOpen) return; const handlePointerDown = (event: MouseEvent) => { @@ -182,8 +180,8 @@ const MiniAppLibraryContent: React.FC = ({ tabs }) => { requestAnimationFrame(() => importTriggerRef.current?.focus()); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(importMenuRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(importMenuRef, 'keydown', handleEscape); + const removeOverlayMousedown0 = subscribeOverlayInteraction(importMenuRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(importMenuRef, 'keydown', handleEscape); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantAvatarPicker.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantAvatarPicker.tsx index d7ae34b456..df8110148a 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantAvatarPicker.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantAvatarPicker.tsx @@ -57,8 +57,6 @@ const AssistantAvatarPicker: React.FC = ({ }, [displayedValue]); useEffect(() => { - let removeOverlayPointerdown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!isOpen) return; const handlePointerDown = (event: PointerEvent) => { @@ -73,8 +71,8 @@ const AssistantAvatarPicker: React.FC = ({ triggerRef.current?.focus(); }; - removeOverlayPointerdown0 = subscribeOverlayInteraction(popoverRef, 'pointerdown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKeyDown); + const removeOverlayPointerdown0 = subscribeOverlayInteraction(popoverRef, 'pointerdown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKeyDown); return () => { removeOverlayPointerdown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx index 6b4362ae7b..8dce296a65 100644 --- a/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx +++ b/src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx @@ -87,8 +87,6 @@ export const DispatchTargetPicker: React.FC = ({ : t('chatInput.dispatch.current', { target: displayLabel }); useEffect(() => { - let removeOverlayPointerdown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!open) return; const handlePointerDown = (event: PointerEvent) => { const targetNode = event.target as Node; @@ -102,8 +100,8 @@ export const DispatchTargetPicker: React.FC = ({ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setOpen(false); }; - removeOverlayPointerdown0 = subscribeOverlayInteraction(menuRef, 'pointerdown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); + const removeOverlayPointerdown0 = subscribeOverlayInteraction(menuRef, 'pointerdown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); return () => { removeOverlayPointerdown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/features/market-account/AccountIdentityControls.tsx b/src/web-ui/src/features/market-account/AccountIdentityControls.tsx index c80ddce351..eb8ef71fc1 100644 --- a/src/web-ui/src/features/market-account/AccountIdentityControls.tsx +++ b/src/web-ui/src/features/market-account/AccountIdentityControls.tsx @@ -100,8 +100,6 @@ export function AccountIdentityControls({ }, [menuOpen, scheduleMenuPositionUpdate, updateMenuPosition]); useEffect(() => { - let removeOverlayPointerdown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!menuOpen) return; const closeOnOutsideClick = (event: PointerEvent) => { const target = event.target as Node; @@ -114,8 +112,8 @@ export function AccountIdentityControls({ setMenuOpen(false); menuTriggerRef.current?.focus(); }; - removeOverlayPointerdown0 = subscribeOverlayInteraction(menuPanelRef, 'pointerdown', closeOnOutsideClick); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuPanelRef, 'keydown', closeOnEscape); + const removeOverlayPointerdown0 = subscribeOverlayInteraction(menuPanelRef, 'pointerdown', closeOnOutsideClick); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuPanelRef, 'keydown', closeOnEscape); return () => { removeOverlayPointerdown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx index 5789c59151..6a039a558a 100644 --- a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx +++ b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx @@ -147,13 +147,12 @@ export const RemoteFileBrowser: React.FC = ({ // Close context menu when clicking outside useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; const handleClickOutside = (e: MouseEvent) => { if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { setContextMenu({ show: false, x: 0, y: 0, entry: null }); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(contextMenuRef, 'mousedown', handleClickOutside); + const removeOverlayMousedown0 = subscribeOverlayInteraction(contextMenuRef, 'mousedown', handleClickOutside); return () => removeOverlayMousedown0?.(); }, []); diff --git a/src/web-ui/src/flow_chat/components/AcpModeSelector.tsx b/src/web-ui/src/flow_chat/components/AcpModeSelector.tsx index 6745cb011f..5cd32d8b7f 100644 --- a/src/web-ui/src/flow_chat/components/AcpModeSelector.tsx +++ b/src/web-ui/src/flow_chat/components/AcpModeSelector.tsx @@ -63,7 +63,6 @@ export const AcpModeSelector: React.FC = ({ }, [candidates.length]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!open) return; const handlePointerDown = (event: MouseEvent) => { const target = event.target as Node; @@ -72,7 +71,7 @@ export const AcpModeSelector: React.FC = ({ setKeyboardOpen(false); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); return () => removeOverlayMousedown0?.(); }, [open]); diff --git a/src/web-ui/src/flow_chat/components/ChatContextPicker.tsx b/src/web-ui/src/flow_chat/components/ChatContextPicker.tsx index 7e59d34b49..fcb362657d 100644 --- a/src/web-ui/src/flow_chat/components/ChatContextPicker.tsx +++ b/src/web-ui/src/flow_chat/components/ChatContextPicker.tsx @@ -728,19 +728,17 @@ export const ChatContextPicker: React.FC = ({ }, [canNavigateBack, displayItems, enterDirectory, goBack, handleItemClick, handleSelect, isOpen, isSearchMode, onClose, openSource, selectedIndex]); useEffect(() => { - let removeOverlayKeydown0: (() => void) | undefined; if (!isOpen) return; - removeOverlayKeydown0 = subscribeOverlayInteraction(containerRef, 'keydown', handleKeyDown); + const removeOverlayKeydown0 = subscribeOverlayInteraction(containerRef, 'keydown', handleKeyDown); return () => removeOverlayKeydown0?.(); }, [handleKeyDown, isOpen]); useEffect(() => { - let removeOverlayMousedown1: (() => void) | undefined; if (!isOpen) return; const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) onClose(); }; - removeOverlayMousedown1 = subscribeOverlayInteraction(containerRef, 'mousedown', handleClickOutside); + const removeOverlayMousedown1 = subscribeOverlayInteraction(containerRef, 'mousedown', handleClickOutside); return () => removeOverlayMousedown1?.(); }, [isOpen, onClose]); diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx index cdd0ccef22..21a5b0e0f7 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx @@ -287,8 +287,6 @@ export const ChatInputWorkspaceStrip: React.FC = ( }, [permissionMenuOpen, permissionMenuView]); useEffect(() => { - let removeOverlayPointerdown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!permissionMenuOpen) return; const handlePointerDown = (event: PointerEvent) => { @@ -313,8 +311,8 @@ export const ChatInputWorkspaceStrip: React.FC = ( } }; - removeOverlayPointerdown0 = subscribeOverlayInteraction(permissionMenuRef, 'pointerdown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(permissionMenuRef, 'keydown', handleKeyDown); + const removeOverlayPointerdown0 = subscribeOverlayInteraction(permissionMenuRef, 'pointerdown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(permissionMenuRef, 'keydown', handleKeyDown); return () => { removeOverlayPointerdown0?.(); removeOverlayKeydown1?.(); @@ -327,8 +325,6 @@ export const ChatInputWorkspaceStrip: React.FC = ( ]); useEffect(() => { - let removeOverlayPointerdown2: (() => void) | undefined; - let removeOverlayKeydown3: (() => void) | undefined; if (!workspaceMenuOpen) return; const handlePointerDown = (event: PointerEvent) => { @@ -346,8 +342,8 @@ export const ChatInputWorkspaceStrip: React.FC = ( } }; - removeOverlayPointerdown2 = subscribeOverlayInteraction(workspaceMenuRef, 'pointerdown', handlePointerDown); - removeOverlayKeydown3 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleKeyDown); + const removeOverlayPointerdown2 = subscribeOverlayInteraction(workspaceMenuRef, 'pointerdown', handlePointerDown); + const removeOverlayKeydown3 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleKeyDown); return () => { removeOverlayPointerdown2?.(); removeOverlayKeydown3?.(); diff --git a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx index 737421af10..e64bf08e32 100644 --- a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx +++ b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx @@ -178,8 +178,6 @@ export const HarnessProfileSelector: React.FC = ({ }, [finishSelection, onStartNewSession, t]); useEffect(() => { - let removeOverlayPointerdown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!open) return; const handlePointerDown = (event: PointerEvent) => { @@ -193,8 +191,8 @@ export const HarnessProfileSelector: React.FC = ({ if (event.key === 'Escape') close(); }; - removeOverlayPointerdown0 = subscribeOverlayInteraction(menuRef, 'pointerdown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); + const removeOverlayPointerdown0 = subscribeOverlayInteraction(menuRef, 'pointerdown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); return () => { removeOverlayPointerdown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/flow_chat/components/ReasoningPresetSelector.tsx b/src/web-ui/src/flow_chat/components/ReasoningPresetSelector.tsx index 1e711f2324..c052827935 100644 --- a/src/web-ui/src/flow_chat/components/ReasoningPresetSelector.tsx +++ b/src/web-ui/src/flow_chat/components/ReasoningPresetSelector.tsx @@ -109,7 +109,6 @@ export const ReasoningPresetSelector: React.FC = ( }, [presets.length]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!open) return; const handlePointerDown = (event: MouseEvent) => { const target = event.target as Node; @@ -118,7 +117,7 @@ export const ReasoningPresetSelector: React.FC = ( setKeyboardOpen(false); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handlePointerDown); return () => removeOverlayMousedown0?.(); }, [open]); diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx index 3fe1980cf5..5a8b1768cf 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx @@ -179,8 +179,6 @@ export const WelcomePanel: React.FC = ({ }, [gitState, handleGitClick, t]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!workspaceDropdownOpen) return; const handlePointerDown = (e: MouseEvent) => { const target = e.target as Node; @@ -198,8 +196,8 @@ export const WelcomePanel: React.FC = ({ setWorkspaceDropdownOpen(false); workspaceTriggerRef.current?.focus(); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(workspaceMenuRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleKeyDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(workspaceMenuRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(workspaceMenuRef, 'keydown', handleKeyDown); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx index 55b160e31f..806cd9972d 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx @@ -190,8 +190,6 @@ export const ExportImageButton: React.FC = ({ }); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!isMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; @@ -202,8 +200,8 @@ export const ExportImageButton: React.FC = ({ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setIsMenuOpen(false); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handleClickOutside); - removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(menuRef, 'mousedown', handleClickOutside); + const removeOverlayKeydown1 = subscribeOverlayInteraction(menuRef, 'keydown', handleKeyDown); return () => { removeOverlayMousedown0?.(); removeOverlayKeydown1?.(); diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx index 56ef361fb4..c0003f2ad2 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx @@ -260,8 +260,6 @@ export const FlowChatHeader: React.FC = ({ }, []); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayKeydown1: (() => void) | undefined; if (!isSessionOverviewOpen) return; const handlePointerDown = (event: MouseEvent) => { @@ -284,8 +282,8 @@ export const FlowChatHeader: React.FC = ({ } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(sessionOverviewPanelRef, 'mousedown', handlePointerDown); - removeOverlayKeydown1 = subscribeOverlayInteraction(sessionOverviewPanelRef, 'keydown', handleKeyDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(sessionOverviewPanelRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(sessionOverviewPanelRef, 'keydown', handleKeyDown); return () => { removeOverlayMousedown0?.(); diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx index e1f31beb15..f7008e6e10 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx @@ -375,7 +375,6 @@ export const ModelRoundItem = React.memo( const renderTraceStartedAtMs = renderTraceEnabled ? performance.now() : null; useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!copied && !isCopyMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { @@ -387,14 +386,13 @@ export const ModelRoundItem = React.memo( setIsCopyMenuOpen(false); }; - removeOverlayMousedown0 = subscribeOverlayInteraction(copyMenuRef, 'mousedown', handleClickOutside); + const removeOverlayMousedown0 = subscribeOverlayInteraction(copyMenuRef, 'mousedown', handleClickOutside); return () => { removeOverlayMousedown0?.(); }; }, [copied, isCopyMenuOpen]); useEffect(() => { - let removeOverlayKeydown1: (() => void) | undefined; if (!isCopyMenuOpen) return; const handleKeyDown = (event: KeyboardEvent) => { @@ -403,7 +401,7 @@ export const ModelRoundItem = React.memo( } }; - removeOverlayKeydown1 = subscribeOverlayInteraction(copyMenuRef, 'keydown', handleKeyDown); + const removeOverlayKeydown1 = subscribeOverlayInteraction(copyMenuRef, 'keydown', handleKeyDown); return () => { removeOverlayKeydown1?.(); }; diff --git a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx index 2342b0eb36..e53eeddc63 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx +++ b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx @@ -197,9 +197,6 @@ export const SessionTreePopover: React.FC = ({ }, [isOpen, refreshSnapshot]); useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; - let removeOverlayMousedown1: (() => void) | undefined; - let removeOverlayKeydown2: (() => void) | undefined; if (!isOpen) return; if (embedded) { const handleEmbeddedPointerDown = (event: MouseEvent) => { @@ -213,7 +210,7 @@ export const SessionTreePopover: React.FC = ({ setActionMenuPosition(null); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(actionMenuRef, 'mousedown', handleEmbeddedPointerDown); + const removeOverlayMousedown0 = subscribeOverlayInteraction(actionMenuRef, 'mousedown', handleEmbeddedPointerDown); return () => removeOverlayMousedown0?.(); } const handlePointerDown = (event: MouseEvent) => { @@ -231,8 +228,8 @@ export const SessionTreePopover: React.FC = ({ closePopover('keyboard'); } }; - removeOverlayMousedown1 = subscribeOverlayInteraction(panelRef, 'mousedown', handlePointerDown); - removeOverlayKeydown2 = subscribeOverlayInteraction(panelRef, 'keydown', handleKeyDown); + const removeOverlayMousedown1 = subscribeOverlayInteraction(panelRef, 'mousedown', handlePointerDown); + const removeOverlayKeydown2 = subscribeOverlayInteraction(panelRef, 'keydown', handleKeyDown); return () => { removeOverlayMousedown1?.(); removeOverlayKeydown2?.(); diff --git a/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx b/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx index a7a820fa56..e3f3bb2d37 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx @@ -100,7 +100,6 @@ export const ToolTimeoutIndicator: React.FC = ({ // Close popover on outside click. useEffect(() => { - let removeOverlayMousedown0: (() => void) | undefined; if (!isPopoverOpen) return; const handleClick = (e: MouseEvent) => { const target = e.target as Node; @@ -111,18 +110,17 @@ export const ToolTimeoutIndicator: React.FC = ({ closePopover(); } }; - removeOverlayMousedown0 = subscribeOverlayInteraction(popoverRef, 'mousedown', handleClick); + const removeOverlayMousedown0 = subscribeOverlayInteraction(popoverRef, 'mousedown', handleClick); return () => removeOverlayMousedown0?.(); }, [isPopoverOpen, closePopover]); // Close popover on Escape. useEffect(() => { - let removeOverlayKeydown1: (() => void) | undefined; if (!isPopoverOpen) return; const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') closePopover(); }; - removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKey); + const removeOverlayKeydown1 = subscribeOverlayInteraction(popoverRef, 'keydown', handleKey); return () => removeOverlayKeydown1?.(); }, [isPopoverOpen, closePopover]); diff --git a/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx b/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx index 6b6677e940..7d05bb7960 100644 --- a/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx +++ b/src/web-ui/src/infrastructure/markdown/MarkdownRenderer.tsx @@ -936,7 +936,6 @@ export const MarkdownRenderer = React.memo(({ const syntaxTheme = useMemo(() => buildMarkdownPrismStyle(isLight), [isLight]); const syntaxThemeRef = useLiveValueRef(syntaxTheme); - const isLightRef = useLiveValueRef(isLight); const contentStr = typeof content === 'string' ? content : String(content || ''); const renderTraceEnabled = isStartupRenderTraceEnabled(); @@ -1697,7 +1696,6 @@ export const MarkdownRenderer = React.memo(({ currentWorkspacePathRef, expandDetailsByDefaultRef, fileActionsViaCallbackOnlyRef, - isLightRef, markdownContentRef, onHttpLinkClickRef, remoteConnectionIdRef, diff --git a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx index 3559581359..e73d18ff76 100644 --- a/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx +++ b/src/web-ui/src/infrastructure/peer-device/DeviceSurfaceSwitcher.tsx @@ -134,7 +134,6 @@ export const DeviceSurfaceSwitcher: React.FC = () => { }, [peerDevice, switching, success, warning, t]); useEffect(() => { - let removeOverlayKeydown0: (() => void) | undefined; if (!open) { return; } @@ -143,7 +142,7 @@ export const DeviceSurfaceSwitcher: React.FC = () => { setOpen(false); } }; - removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown); + const removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown); return () => removeOverlayKeydown0?.(); }, [open]); diff --git a/src/web-ui/src/infrastructure/update/UpdateIndicator.tsx b/src/web-ui/src/infrastructure/update/UpdateIndicator.tsx index 1b2b4c0521..9439d35871 100644 --- a/src/web-ui/src/infrastructure/update/UpdateIndicator.tsx +++ b/src/web-ui/src/infrastructure/update/UpdateIndicator.tsx @@ -1,12 +1,6 @@ -import { canCheckForAppUpdates } from './tauriEnv'; -import { selectHasUpdateAttention, useUpdateInstallStore } from './updateInstallStore'; +import { useHasAppUpdate } from './useHasAppUpdate'; import './UpdateIndicator.scss'; -export function useHasAppUpdate(): boolean { - const attention = useUpdateInstallStore(selectHasUpdateAttention); - return canCheckForAppUpdates() && attention; -} - export function UpdateIndicator() { const visible = useHasAppUpdate(); return visible ?