Skip to content

馃殌 Release BOSS 9.5.6

Choose a tag to compare

@github-actions github-actions released this 31 Aug 05:43
· 90 commits to main since this release

BOSS v9.5.6

Release Date: 2026-08-31

One feature. Switching away from a Google Meet tab used to lose the call - Chrome pops it into a floating window and BOSS did not. It does now: the tab's real rendering surface is reparented into a small always-on-top window with a drag strip, a back-to-tab button and a resize grip, so the call keeps running with Meet's own tiles, controls, names and captions. Behind Settings > Browser > Video calls, on by default.

Highlights

  • The pop-out is the tab's actual surface, not a copy of it. Four earlier architectures were tried and measured: cloning tiles into a Document Picture-in-Picture window cannot work, because a hidden tab's DOM is frozen (Chromium runs no rendering pipeline for a hidden widget, so a new element never mounts however hard the page is told it is visible) and Meet's SFU does not forward video for tiles the client is not rendering - screen shares worked and participant joins never did. Reparenting the surface the way HTML fullscreen already does makes all of it work at once (#282)
  • Eligibility comes from Chromium's own capture signal. JxBrowser has published MediaStreamCaptureStarted/Stopped all along and nothing used them; they carry the same device-capture fact Chrome gates on, with no JS polling and none of a media probe's blind spots. Counted per kind rather than flagged, because a call opens audio and video as separate streams - muting the mic mid-call would otherwise declare the meeting over while the camera is still on (#282)
  • Time to a visible window went from ~3700ms to ~209ms. The delay was never in the Picture-in-Picture code: a URL-less popup sat out the full 3s POPUP_URL_TIMEOUT_MS waiting for a destination it would never have. The in-page marks measured Blink handing over its window at ~60ms, so the fast half was instrumented and the slow half was not, and two rounds of tuning went into constants that were never the cost (#282)
  • Off switch that means it. Settings > Browser > Video calls, persisted to ~/.boss/auto-pip.json, or BOSS_BROWSER_AUTO_PIP=false. The environment wins and the row says so instead of sitting there doing nothing. The value travels to the browser plugin as a system property, because the host decides whether to pop out and the plugin's hibernation guard decides whether to keep a popped-out tab alive - they must agree and cannot share a Kotlin constant (#282)

Features

  • browser: a tab capturing microphone or camera on an https page pops out into a floating window when it is backgrounded, and returns when you come back to the tab. shouldAutoPictureInPicture is a pure predicate so the gate is tested rather than merely wired, and the setting is a parameter of it - "never" from the user is a different answer from "not now" from the other inputs (#282)
  • browser: the pop-out window reuses the fullscreen path's chrome - drag strip naming the origin, back-to-tab and close grouped in the corner, resize grip - around a Swing BrowserView created after the same detach delay (#282)
  • browser: isPoppedOut is published to plugins, in the same category as the fullscreen callbacks beside it. The plugin cannot work this out for itself: the pop-out is a host window and the page inside knows nothing about which window renders it. Inferring it from visibilityState plus a playing <video> misses a call joined with the camera off - visible, no video, so unprotected, so the handle is disposed and the call dies. Defaulted, because a plugin naming a member an older host lacks is rejected wholesale by BinaryCompatibilityValidator (#282)
  • settings: Settings search indexes the row under pip, picture in picture, meet, pop out and floating window, since "Video calls" is not what someone would type (#282)

Bug Fixes

  • browser: a pop-out could open over a tab already back on screen, leaving it blank under an always-on-top window with the one signal that would have closed it already fired. The settle coroutine checked composedSurfaces and only then raised the flag, so a tab re-composed between those steps slipped through; the open re-checks on the EDT, where it happens (#282)
  • browser: a throwing detach left the frame undisposed and the window with no route to closure at all - popOutFrame was nulled first and frame.dispose() shared a try with the view detach, where ObjectClosedException from a concurrently closing browser is live. Dispose moved to a finally (#282)
  • browser: dispose() closed the browser before detaching the view, which is the ordering that leaves the window stuck. The EDT hop is bounded with FutureTask plus a timed get(), copied from FullscreenBrowserWindow - on timeout the task stays queued, so the window is still disposed once the EDT frees up rather than the app freezing (#282)
  • browser: HTML fullscreen and the pop-out can no longer fight over the tab's one rendering surface. Pressing a site's own fullscreen control while a call is popped out closes the pop-out first, and the reverse gate is per-tab now - it used to suppress pop-out for this tab whenever any other tab was fullscreen (#282)
  • browser: a browser that dies on its own takes its pop-out with it. BrowserClosed sets disposed = true and dispose() returns on its first line when that is set, so a crashed renderer or an engine recycle never reached the cleanup - and the tab is backgrounded by definition, so nothing tore down its composition either, leaving a window on screen over a dead surface (#282)
  • browser: Back-to-tab worked by luck. Its collector lived in ApiActiveTabsProviderAdapter's init, and that adapter is by lazy behind a property that exists for one plugin - where nothing read it, nothing collected and the button silently degraded to raising the window. It is started unconditionally per window now, and a request nobody can answer is logged (#282)
  • browser: the active-tabs polling loop is restored. Removing the pop-out return collector from that adapter's init took the whole init with it, including the pre-existing loop that was the only thing driving _activeTabs - so every plugin observing active tabs would have seen an empty list for the life of the process, silently (#282)
  • browser: the popup close is out of the compare-and-set. The happy path set cleanedUp before deciding what to do with the popup, so a throw after that point lost the CAS and skipped the close, leaking a Browser and its renderer. The CAS guards the unsubscribe and the map removal, which must happen once; closing is idempotent (#282)
  • browser: capture counts no longer reset on same-document navigation. NavigationFinished fires for pushState too and Meet rewrites its URL mid-call, so the reset zeroed the counts while the camera was live and automatic pop-out silently never fired again. They do still reset on a real main-frame navigation, so one missed MediaStreamCaptureStopped cannot leave a tab eligible forever (#282)
  • browser: the resize strip's cursors match what it does. It was one component carrying a single SE-resize cursor along its whole width, so the bottom-left corner promised a resize and then grew the window away from the pointer. Three zones now, and the left corner keeps the right edge still - x moves by exactly what the width loses - so a drag past the minimum size stops growing instead of walking the window sideways (#282)
  • browser: dragging the pop-out by its title no longer snaps the window by the label's inset, which happened because the press point was stored in the label's coordinate space rather than the frame's (#282)
  • browser: runCatching in the return collector swallowed CancellationException and broke structured cancellation on window close (#282)

Improvements

  • browser: no arbitrary popup is ever shown as a floating window. The chrome-less showPopupInWindow shape is deleted rather than left gated - an undecorated always-on-top window whose only chrome reads the opener's hostname is arbitrary content wearing someone else's name, and its sole caller always passes real bounds, so the branch was dead. URL-less popups are dropped unconditionally (#282)
  • browser: a document-start script that ran on every page is gone with the architecture it served. It installed wrappers around navigator.mediaSession.setActionHandler, setMicrophoneActive, setCameraActive and setScreenshareActive, an override of documentPictureInPicture.requestWindow, getter overrides for Document.prototype.visibilityState and .hidden with a requestAnimationFrame shim behind them, and wrappers around the RTCPeerConnection constructor and getDisplayMedia. All inert once the pop-out became the real surface, all detectable by the page (#282)
  • browser: a temporary WARN probe that logged raw popup URLs on every popup the user opened is deleted. AGENTS.md requires maskUriParams for anything URL-shaped, and a window.open target routinely carries session ids and single-use tokens (#282)
  • browser: AUTO_PIP_SETTLE_MS is 100ms, matching FullscreenBrowserWindow's COMPOSE_DETACH_DELAY_MS that its own comment claimed it doubled. Twenty milliseconds short of the value the other reparent path found necessary is the black-surface class of bug this file documents at length (#282)
  • browser: POPUP_URL_TIMEOUT_MS is back to 3000ms. It was cut to 600 for windows that never navigate, and this release drops those entirely - so nothing waits for display and the shorter deadline only risked a cold render process missing NavigationStarted (#282)
  • settings: the setting is written to a sibling file and moved with Files.move rather than File.renameTo, which fails on Windows once the destination exists and reports it in a return value nobody reads. encodeDefaults is on, so choosing "on" writes the value instead of {} - otherwise a later change of default would silently rewrite every stored choice (#282)
  • tests: seventeen tests across the eligibility predicate and the grip geometry, mutation-verified. Making the capture count a boolean fails "muting the mic mid-call does not end the call", dropping the capture gate fails "a tab capturing nothing is left alone", allowing http fails "only https pages qualify", and the two resize mutations that fail are the classic hand-rolled left-resize bug and the bottom-right-only strip that actually shipped and was reported (#282)

Known Limitations

  • Minimizing the window does not pop a call out. The trigger hangs off surface disposal and a minimized window keeps its composition (#282)
  • Back-to-tab raises the window but does not re-select the tab. That needs a callback on the plugin API (#282)
  • A page holding only the microphone is left alone. Dictation, a voice note or a whiteboard with the mic open all satisfy "capturing on https", and the script requires actual call tiles. Deliberately not gated on video capture, since a call joined with the camera off captures no video at all - which is exactly the case this exists to cover (#282)
  • An eligible page gets a synthetic user-activation click per surface teardown. Both Picture-in-Picture APIs refuse without user activation and a tab switch has none; Chrome mints one inside Blink, which no embedder can reach (#282)
  • The browser plugin's hibernation guard still keys on the retired Document Picture-in-Picture state. It should key on "backgrounded but visibilityState is visible", which is the popped-out signature; isPoppedOut now supplies that fact from the host (#282)

馃摝 Downloads

Platform Architecture Package
macOS Universal (Apple Silicon + Intel) BOSS-9.5.6-Universal.dmg
Windows x64 BOSS-9.5.6.msi
Windows ARM64 BOSS-9.5.6-arm64.msi
Linux DEB AMD64 (x86_64) BOSS-9.5.6-amd64.deb
Linux DEB ARM64 (aarch64) BOSS-9.5.6-arm64.deb
Linux RPM AMD64 (x86_64) BOSS-9.5.6-amd64.rpm
Linux RPM ARM64 (aarch64) BOSS-9.5.6-arm64.rpm
Linux JAR AMD64 (x86_64) BOSS-9.5.6-amd64.jar
Linux JAR ARM64 (aarch64) BOSS-9.5.6-arm64.jar

Always latest - newest stable release, resolved server-side, so these stay
correct in a bookmark and need no API key:

macOS DMGWindows x64Windows ARM64Linux DEB amd64Linux DEB arm64Linux RPM amd64Linux RPM arm64Linux JAR amd64Linux JAR arm64

Release metadata - version, every asset, sha256 checksums - is at ?app=boss.


Full Changelog: v9.5.5...v9.5.6