Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(_))
Expand Down
76 changes: 76 additions & 0 deletions src/apps/desktop/src/window_webview_geometry.rs
Original file line number Diff line number Diff line change
@@ -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<u32>,
) -> Option<tauri::PhysicalSize<u32>> {
(!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));
}
}
6 changes: 2 additions & 4 deletions src/web-ui/src/app/components/NavPanel/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ const AssistantSessionCreateMenu: React.FC<AssistantSessionCreateMenuProps> = ({
const closeMenu = useCallback(() => setMenuOpen(false), []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
if (!menuOpen) return;

const handleMouseDown = (event: MouseEvent) => {
Expand All @@ -67,8 +65,8 @@ const AssistantSessionCreateMenu: React.FC<AssistantSessionCreateMenuProps> = ({
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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
});

useEffect(() => {
let removeOverlayKeydown0: (() => void) | undefined;
if (!open) return undefined;
void refresh();
const onKeyDown = (event: KeyboardEvent) => {
Expand All @@ -188,7 +187,7 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
triggerRef.current?.focus();
}
};
removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown);
const removeOverlayKeydown0 = subscribeOverlayInteraction(popoverRef, 'keydown', onKeyDown);
return () => removeOverlayKeydown0?.();
}, [onOpenChange, open, refresh]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ const WorkspaceSessionFilterMenu: React.FC = () => {
}, []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
if (!open) return;
updatePosition();
requestAnimationFrame(updatePosition);
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,15 +688,14 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
}, []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
if (!openMenuSessionId) return;
const handleOutside = (event: MouseEvent) => {
if (!sessionMenuPopoverRef.current?.contains(event.target as Node)
&& !sessionMenuAnchorRef.current?.contains(event.target as Node)) {
closeSessionMenu();
}
};
removeOverlayMousedown0 = subscribeOverlayInteraction(sessionMenuPopoverRef, 'mousedown', handleOutside);
const removeOverlayMousedown0 = subscribeOverlayInteraction(sessionMenuPopoverRef, 'mousedown', handleOutside);
return () => removeOverlayMousedown0?.();
}, [closeSessionMenu, openMenuSessionId]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,6 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
}, []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
if (!menuOpen) return;
const handleOutside = (event: MouseEvent) => {
const target = event.target as Node;
Expand All @@ -476,7 +475,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
setMenuOpen(false);
}
};
removeOverlayMousedown0 = subscribeOverlayInteraction(menuPopoverRef, 'mousedown', handleOutside);
const removeOverlayMousedown0 = subscribeOverlayInteraction(menuPopoverRef, 'mousedown', handleOutside);
return () => removeOverlayMousedown0?.();
}, [menuOpen]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ const DateTimePickerPopover: React.FC<DateTimePickerPopoverProps> = ({
}, [anchorRef]);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node | null;
if (!target) return;
Expand All @@ -107,8 +105,8 @@ const DateTimePickerPopover: React.FC<DateTimePickerPopoverProps> = ({
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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ const MiniAppGalleryView: React.FC<MiniAppGalleryViewProps> = ({ tabs }) => {
const closeImportMenu = useCallback(() => setImportMenuOpen(false), []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
if (!importMenuOpen) return;

const handlePointerDown = (event: MouseEvent) => {
Expand All @@ -125,8 +123,8 @@ const MiniAppGalleryView: React.FC<MiniAppGalleryViewProps> = ({ 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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ const MiniAppLibraryContent: React.FC<MiniAppLibraryViewProps> = ({ tabs }) => {
const closeImportMenu = useCallback(() => setImportMenuOpen(false), []);

useEffect(() => {
let removeOverlayMousedown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
if (!importMenuOpen) return;

const handlePointerDown = (event: MouseEvent) => {
Expand All @@ -182,8 +180,8 @@ const MiniAppLibraryContent: React.FC<MiniAppLibraryViewProps> = ({ 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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ const AssistantAvatarPicker: React.FC<AssistantAvatarPickerProps> = ({
}, [displayedValue]);

useEffect(() => {
let removeOverlayPointerdown0: (() => void) | undefined;
let removeOverlayKeydown1: (() => void) | undefined;
if (!isOpen) return;

const handlePointerDown = (event: PointerEvent) => {
Expand All @@ -73,8 +71,8 @@ const AssistantAvatarPicker: React.FC<AssistantAvatarPickerProps> = ({
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?.();
Expand Down
6 changes: 2 additions & 4 deletions src/web-ui/src/features/dispatch/DispatchTargetPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ export const DispatchTargetPicker: React.FC<DispatchTargetPickerProps> = ({
: 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;
Expand All @@ -102,8 +100,8 @@ export const DispatchTargetPicker: React.FC<DispatchTargetPickerProps> = ({
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?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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?.();
Expand Down
Loading
Loading