Repository navigation
Picture-in-picture and media tray - #9
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sidebar: Spaces strip and undo toast from main, media tray kept just above the bottom bar. feature-status.json: union of both sides; FEATURES.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rols API) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rewrite Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd, rounded window shape Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the controller - "Back to tab" and double-click select the tab at once (state first), then a still of the last frame flies from the player to the video's place in the page (MD-16); reduced motion or an unseen page: the player just closes. - The tray's Picture in Picture button sends the controller's rect, so the player grows out of the controller, and the controller gives way at once (MD-10, MD-16). - Leaving a tab right after asking for picture-in-picture hands the video to the player even before the page's report arrives (MD-02). - The player's controls hide after the enter motion when the pointer isn't over it; empty wheel events and lost pointer capture no longer stop or strand a motion. - Media e2e: frame budgets scale with FRAME_SLACK, sizes derive from the work area (macOS CI runner, 1024x768 checked), the fixture's own duration is read; unit tests for the player geometry, motion curves, window shape, clock and settings clean-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… move into the player Leaving a split while its panes were still settling could let a resize at the pane's old size reach the page after the player armed the lift: the video stayed in the page and the player showed the page's corner. Until the lift shows, other sizes now leave it armed; the player still drops it when it gives up or closes. Tests: per-pane mute from the split row's menu (MD-11), a video in a split pane floats only when the split is left and goes back into its pane (MD-01), and the late resize itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1 tests named - docs/feature-status.json: MD-01 to MD-16, FL-32, KS-11 and SB-10 rewritten for the floating player Orbit draws itself (they still described the engine's native window). - FEATURES.md regenerated. - The tray test checks it sits just above the bottom bar and pushes the list up (SB-10); the keyboard test carries KS-11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 01b43ccb-90cc-4d8c-8386-aa39fa51f983) |
The bar opens from 97 % scale and 4 px up. That keeps it centred, so the test's "centred" wait also passed mid-animation, and its top moved by about 1.1 px before the second reading. It failed both attempts on PR #9's Linux job. The test now waits for the bar's transform to come to rest before each reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 1c230b72-33c2-4893-a48e-15c98251e3a4) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A tab resting on a full, 4-pane split row no longer gets a half highlighted: the row reorders like any other, as the page drop already shows no indicator for a full split (SV-25, split review follow-up). New e2e test fails without the fix (the half is highlighted). - SV-26 grow-in test: the frame sampler is installed before the pane is added, times frames from when the card enters the page, checks the spring's shape only on frames that came in time, and stops by itself. CI hung on it for 120 s and, on retry, saw a first sample at 94 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… !important rules)
M1. The main process now reads reduced motion the same way pages do.
- richMotion() returns !systemPreferences.getAnimationSettings().prefersReducedMotion.
That value follows the OS setting and Chromium's --force-prefers-(no-)reduced-motion
switches; shouldRenderRichAnimation ignores both switches.
- launchOrbit passes --force-prefers-no-reduced-motion. With { reducedMotion: true } it passes
--force-prefers-reduced-motion instead. The main process then matches Playwright's page
default on every runner, including GitHub's macOS image, which turns Reduce Motion on.
The three MD-16 tests failed with a GTK profile that has animations off; they pass now.
- New FL-28 e2e, launched with reduced motion:
- the player is in its corner from its first frame (lastEnter null);
- a released player eases into its corner over 120 ms with no overshoot, instead of the
spring (F2);
- Back to tab ends the player at once: no flight, no still, no blank frame.
M2. MD-01 and MD-09 are now partial: media inside frames isn't seen yet. R1 (a video in a
frame floats) and R2 (x pauses audio in a frame) are test.fixme until frame support (F1).
M3. The lift rules now use video[data-orbit-pip]:not(#_):not(#_):not(#_). A page's own
!important size rule (#wrap video{width:100%!important;...}) no longer stretches the floating
video over the whole page area. The new e2e fails without this (982x800 instead of 320x180).
Follow-ups from the review:
- F11: closing the player while Back to tab takes its still now clears the still, so the next
video never shows the old frame. The e2e fails without this.
- F7: tall videos are also at least 200 px wide, and the player is never taller than the
work area.
- F4: the player's controls belong to the session's window, not to the first window, which
may have closed. The e2e fails without this.
- F9: e2e for the right-click Picture in Picture item, on and off.
- Media fixtures moved to ports 3330-3349.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FEATURES.md regenerated from the merged tests and feature status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 64d294d0-64d6-4dfe-a452-b0b5ecf1957a) |
…02, lead review F1) A Favorite split counts as one tile (D19). Separating it makes one tile per pane, and moving a pane out makes two, so both could take a profile past the 12-Favorite limit. Both are now refused with the "Favorites are full" toast when they would; a split anywhere else is never limited. The toast text is one constant, shared with the tabs code. Unit test for the count, and an e2e test that fills Favorites, checks both refusals leave everything as it was, then frees a tile and moves a pane out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playwright runs main-process evaluates through the inspector, which can run them in the middle of other main-process code, even inside a store update. A tab a test created there was overwritten when that update finished, so it vanished: about 1 run in 12 of the new Favorites test. A stack trace placed the test's tabs.create inside immer's patch generation for a favicon update. launchOrbit now wraps app.evaluate so the test's function runs after a setImmediate. The test hooks' run and action defer too, which covers the perf and capture scripts. The Favorites test then passed 60 of 60, and the full suite passes. The SP-11 flake (a profile change that reached listeners, then reverted in the store) fits the same cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They bound 127.0.0.1:3390 and 3392, so two full suites running at once on one machine (several agents do) could collide. Now the OS picks a free port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three command bar tests opened real DuckDuckGo and YouTube searches and a Peek test opened meet.google.com. They only check URLs, but the pages loaded from CI and dev machines. launchOrbit now starts Orbit with --proxy-server pointing at a closed loopback port (loopback, where the fixtures are served, bypasses it), and waits until every profile's session resolves through it: a session picks the command line's proxy up about 50 ms after the sidebar shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The favicon tests' servers moved to port 0 in 6d73442, which the OS picks from outside the project's port range. listenInRange takes the first free port from a base up to 3399, as the fixture servers already did; startFixtures, the Peek fixtures and both favicon servers use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aves only after Orbit has the page's report Verification V1: the long-video fixture is 60 one-second chunks, but its length varies by machine (67 s on the macOS runner), so a 75 % seek showed 0:50 where the test wanted 0:4x. The elapsed text is now compared with the video's position. Passes at 60 and 70 chunks. Verification V-F2 (test side): MD-07 left a tab right after its page reported playing, before Orbit had the page's media report, and a tab left then doesn't float. The test now waits for the report first, as FL-28 does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: rpc.ts (both event sets), state.ts (TabState keeps media and floating; settings keep the PiP fields with Peek's MW-11 note), feature-status.json (union; no duplicate IDs) and FEATURES.md (regenerated). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: d60794b4-b7b8-4c06-9ec8-4e948900b2bc) |
…blocked quitting (D28) Electron's default uncaughtException handler shows a modal error box that holds the main thread until someone dismisses it, so Orbit couldn't quit. Under Xvfb nobody dismisses it: e2e app.close() hung, then Playwright's worker teardown, which failed whole jobs (macOS CI on PR #7 and main, three local full runs). A user would have seen the box and an app that wouldn't quit. - src/main/index.ts: log "[orbit] uncaught exception:" and carry on. - tests/e2e/helpers.ts: app.close() fails a test whose main process logged one (takeUncaught() claims one thrown on purpose) and kills an Orbit that still hasn't quit 15 s after being asked, so one stuck quit can't hang the run. - core.spec.ts: [TB-22] a throw in a main-process timer is logged, Orbit quits and the session restores. Without the fix nothing is logged and the quit hangs until the harness kills it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before the evaluate replies fromMini sends a command from the Mini Window's own page. mini.control's close can close that page before Playwright gets the evaluate's reply, which then fails with "Target page, context or browser has been closed" (MW-10, once in a full run). The helper now ignores that error only when the page really closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… growing in A pane added a moment ago springs open from zero width (SV-26), so its first bounding box can be empty. The poll now retries instead of failing at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de-out threw and blocked quitting (D28) The strip fades out for 120 ms before its view hides. When its window closed during the fade (as happens when Orbit quits right after a pane strip hid), the fade's timer still ran on a view whose page was gone: view.webContents was undefined, and reading isDestroyed() threw. Nothing caught that, and Electron's modal error box then held the main thread, so Orbit never quit. This is the exception behind the SV-03 hangs: 15 repeats of SV-03 with the D28 check logged it once. The window's closed handler now clears the strip's show, hide and fade timers, and every check of the strip's view also handles a view whose webContents is gone. Test: "[SV-12] a window that closes while its pane strip fades out closes cleanly (D28)". It fails without the fix (3/3: the TypeError above) and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…make a 13th Favorite (PR #10 review M1) A Favorite split could still take Favorites past 12 four ways (FV-02): - Duplicate put a copy beside it as a new tile; - Open in Split View on one of its panes and a Today tab left that pane's split in place and added the new split as a tile; - ⌘⇧T or ⌘Z on a pane closed from it put the pane back in its Favorites spot when the split was full again; - ⌘⇧T on a whole Favorite split removed from the sidebar put it back after Favorites had filled up. Duplicate and Open in Split View are now refused with the "Favorites are full" toast. Open in Split View tries the change on a throwaway copy of the state and refuses it when a Favorites grid would grow past 12. A tab or split coming back to a full Favorites grid lands in Today instead. Tests (each fails without the fix: 13 tiles, or a split id instead of null): - split-tabs.test.ts: a closed pane reopened, and one undone, while its Favorite split is full; a removed Favorite split reopened once Favorites filled up. - split.spec.ts: Duplicate and Open in Split View at 12 tiles are refused, and both work once there is room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew M2) A row that has just finished growing in drops `.is-growing`, and its slot goes back to `content-visibility: auto`. For a frame or two after that, hit testing skips the row, so the test's press landed on the slot and no drag started. The reviewer saw 4 failures in 15 runs at f8a1988. The test now waits until `elementFromPoint` at the row's centre is the row. With the wait, SP-21 passed 15 of 15 runs. Users can lose a click the same way: that follow-up (F1) goes to the feel lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#11 review M1) checkQuit kept only the last 4,000 characters of stderr before it looked for the "[orbit] uncaught exception:" marker. An exception logged in one longer write (a big message, a cause chain, an AggregateError) lost its marker to the trim: the test passed and takeUncaught() returned ''. The scan now looks for the marker in the whole chunk first, keeps up to 4,000 characters from it, and otherwise carries only a marker-length tail so a marker split across two writes is still found. New TB-22 test: a main-process timer throws a 5,000-character message. It fails without the fix (takeUncaught() returns '') and passes with it, 3/3. core + split + smoke e2e: 62/62. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Ubuntu 26 (Oct 19) GitHub's notice says `ubuntu-latest` becomes Ubuntu 26 from 2026-10-19. The e2e job installs xvfb, libgbm1 and libasound2t64 with apt, which is only known to work on 24.04, the image `ubuntu-latest` runs today. So nothing changes until that date; after it, the jobs stay on 24.04 until someone checks 26 on purpose. CI-only change (D18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # tests/e2e/helpers.ts
… gets the offline proxy, deferred evaluates and the quit check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: src/shared/rpc.ts keeps both renderer views ('library' and 'pip');
FEATURES.md regenerated. Typecheck, lint and build exit 0; unit 352/352.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every conflict is two additions side by side, kept together: - state.ts: chrome's runtime tab fields (blockedPopups, permissionPrompt, pendingUrl) and media's tab media state; persist.ts lists all of them as runtime-only. - rpc.ts: NavCommands and MediaCommands/MediaEvents; renderer views keep chrome's (sidebar, tools, prompt, loading, link) and add media's 'pip'. - model.ts, ops.ts, persist.ts: both lanes' imports. - Sidebar.tsx: chrome's structure (private sidebar, floating card, ResizeHandle taking the window) with media's tray above the bottom bar in normal windows. - helpers.ts: media's launchOrbit (reducedMotion and args), with chrome's example of args in its comment. - feature-status.json: union (no ID on both sides); FEATURES.md regenerated. Typecheck and lint exit 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 56f9b7fd-b1fa-48da-ad71-53f1f343f639) |
Conflicts: - Sidebar.tsx: the update banner and media's tray both sit above the Space switcher, the banner first. - rpc.ts: RendererView takes both 'pip' and 'onboarding'. - FEATURES.md: regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/ARCHITECTURE.md §9: main's E2E bullet (dead proxy, deferred evaluates, the quit check) with this branch's merge-gate bullet in place of the CI one. FEATURES.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picture-in-picture and the media tray, built from
spec/media.md(MD) andspec/feel.mdFL-32.main(with split view, PR #5) is merged in.Built
Decisions where the spec was open
agent.ts: before the video has moved into the player, a resize at another size no longer cancels the move.Review first
close()andback()insrc/main/media/player.ts(the still frame and the flight back).src/main/media/agent.ts. It relies on the player always putting the video back when it closes, which it does today.capturePagecan return a blank image with hardware video overlays. That's detected, and the player then closes without the flight.Tests
media.test.ts23,pip.test.ts13).media.spec.ts. In the last full local runs at load average 12–50, the failures were timing budgets that also fail onmain's own build under that load (CB-22, SP-08). Media plus split together passed 54/54 on one worker.Review, verification and lead fixes
3d7bca9,88c081a).369a98f(tests only):2fbe461: mergedmain(PR Peek (link preview) and Mini Window #8, Peek and Mini Windows).rpc.ts: both event sets.state.ts:TabStatekeepsmediaandfloating.FEATURES.mdregenerated.2fbe461: build, typecheck and lint exit 0; unit tests 289/289. The final check is below, on this PR's merged head.reviews/lane-media.md.Merge with PRs #10 and #11, and the local check (merge gate, D42)
GitHub Actions is off for cost, so a local check is the merge gate. PRs #10 and #11 merged first (04:56 and 04:57 UTC), and this PR's head now includes them:
45a85b1:lead/quit(PRs Lead polish 2: full-split drop target, Favorite splits past 12, e2e evaluate race #10 and Main-process exceptions no longer block quitting (D28): the check, the strip's fade timer, two test races #11) merged with this branch. One conflict,tests/e2e/helpers.ts: media'sreducedMotionoption kept next to Lead polish 2: full-split drop target, Favorite splits past 12, e2e evaluate race #10's dead proxy,[OFFLINE, motion, …].da6a207, lead fix (D18, tests only):launchWithFakeDevicesnow goes throughlaunchOrbit({ args: ['--use-fake-device-for-media-stream'] }). Its own launcher skipped the offline proxy, the deferred evaluates and the quit check, and with Main-process exceptions no longer block quitting (D28): the check, the strip's fade timer, two test races #11 in, it failed typecheck (takeUncaughtmissing).The check, on
da6a207(Linux under Xvfb, one worker):test.fixmetests for media inside frames, which isn't built yet (their notes say so).🤖 Generated with Claude Code
Note
High Risk
Large cross-cutting feature (main window management, injected page scripts, tab view borrowing, and global media state) with complex PiP lifecycle; bugs could affect playback, focus, or quit behavior.
Overview
Adds Orbit’s media lane: automatic and manual picture-in-picture, a floating player that borrows the tab’s view (with enter/exit motion and sidebar tray), background media/call controllers, and camera/mic indicators on tab rows.
Main process installs an isolated page script (
agent.ts) that long-polls media state, lifts video for PiP, and drives controls;MediaManagerties tab visibility to auto-PiP and tray membership;PipPlayerowns the always-on-top window, gestures, and “back to tab” flight. Renderer addspipview (PipApp),MediaTrayabove the bottom bar, and hooks/commands/actions for PiP, site snooze settings, and hardware media keys.Tab lifecycle now treats playing/PiP/camera/mic tabs as busy for sleep, auto-archive, and Clear (via
isMediaBusy). Split/favorites enforces the 12-tile cap when separating, duplicating, or creating splits from Favorites (FV-02). Reliability: logs uncaught main exceptions instead of blocking quit; e2e defers__orbitevaluates; pane-strip timers clear on window close; CI pins Linux toubuntu-24.04for Xvfb deps. Feature checklist and persistence pick up PiP settings and runtimetab.media.Reviewed by Cursor Bugbot for commit da6a207. Bugbot is set up for automated code reviews on this repo. Configure here.