Skip to content

feat: share comparison snapshots, fit and resize panels, rating prompt - #91

Merged
Manho merged 45 commits into
mainfrom
feature/share-snapshot
Oct 22, 2026
Merged

Manho merged 45 commits into
mainfrom
feature/share-snapshot

Conversation

@Manho

@Manho Manho commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds shareable comparison snapshots, panels that fit wide provider pages and can be resized by dragging, and a Chrome Web Store rating prompt.

Share a snapshot of the comparison

  • A Share button (also the toolbar icon, its shortcut and the context menu) turns the Multi-Panel page into a framed PNG: brand row with a panelize.dev link, the prompt as an editable title, the providers and date, and the panels.
  • Two layouts:
    • Current view: aligns each conversation on the shared prompt and captures what is on screen.
    • Full answers: scrolls every panel in lock-step from the latest prompt down and stitches the screens. It is capped at 8000 px of output, and a note names the providers whose answer was cut short.
  • Capture path:
    • Silent captureVisibleTab when the activeTab grant is present.
    • Otherwise it falls back to tab sharing (getDisplayMedia). The shared tab is kept between snapshots so Chrome asks only once, and the dialog shows a shortcut tip.
  • While stitching, provider pages:
    • hide floating buttons, fades, pinned composers and overlay scrollbars;
    • un-stick headings;
    • put virtual lists back afterwards.
  • Capture checks:
    • A capture starts over when the window changes size under it.
    • Only panels fully on screen are captured; the others are named in a note.
  • Tab sharing and the pointer: Chrome draws the pointer into tab-sharing frames for 2 s after it moves, whatever the page's CSS cursor. So during capture a transparent shield covers the panels, and each frame waits until the pointer near them has been still. Meanwhile the progress bar asks the user to hold the mouse still.
  • Dialog: copy to clipboard, download, a watermark toggle in Settings, and a progress bar with Cancel.

Panels

  • Provider pages wider than their panel (e.g. Kimi, Qwen, Zhipu in 1x12) are laid out at the width they need and scaled down to fit.
  • Panel edges can be dragged, or moved with the arrow keys, to resize columns. Widths are kept per layout; double-click resets them.

Rating prompt

  • Engaged users are asked once to rate Panelize on the Chrome Web Store, with Later / Never options.

Other

  • All new strings are translated in the 10 shipped locales.
  • Manifest unchanged: no new permissions or host access, so existing installs are not disabled on update.
  • No data leaves the browser: snapshots are composed locally on a canvas.

Validation

  • Unit tests: 625/625 passed (npx vitest run).
  • E2E: 74/74 passed (npx playwright test tests/e2e).
  • Live smoke runs against real providers (2x2 and 1x12) were used during development to check alignment, stitching and the tab-sharing path.
  • Not verified automatically: that a real OS pointer moving during a tab-sharing capture stays out of the image. Playwright's synthetic mouse does not trigger Chrome's cursor overlay.

Manho and others added 30 commits October 20, 2026 23:52
Count successful Send All actions in chrome.storage.local. After 20 sends
the comparison page shows a banner linking to the store reviews page.
Rate and Don't ask again stop the prompt; Later snoozes it for 50 sends.
Chrome writes _metadata into the unpacked repo while a test loads it, so a
concurrent copy could fail with ENOENT when the directory disappeared.
Add a pure module that lays out the prompt strip, the captured panels and the
optional Panelize watermark on a canvas, plus a tab capture helper built on
getDisplayMedia with preferCurrentTab. Chrome asks the user to share the tab,
so no extension permission is needed, and a capture handle confirms the
stream shows this tab before it is cropped.
A Share button captures the panels, adds the last prompt and a "Compared
with Panelize" strip, and opens a preview that warns about sidebars and
avatars before the image is copied or downloaded as PNG. Everything stays on
the device. A new setting turns the watermark off.
Sends one prompt in temporary chat to ChatGPT, Claude, Gemini and DeepSeek on
the live profile, then takes, downloads and copies a snapshot. Runs only with
PANELIZE_LIVE_SEND=1 and stores evidence under artifacts/share-snapshot/live.
The focus restore after loading, sending or starting a new chat pulled
focus back to the prompt box even while a dialog was open, so typing into
a dialog field right after sending went to the prompt box instead.
…shots

Provider frames answer PREPARE, SCROLL and RESTORE messages from the page
that embeds them: align the latest message matching the prompt under the
provider header, report the visible conversation without sticky headers
and the composer, scroll it step by step and put it back afterwards.
- The toolbar icon, its shortcut and the context menu on the Panelize tab
  take a snapshot of that tab with captureVisibleTab, without Chrome's
  tab sharing dialog. The Share button tries the same first and falls
  back to getDisplayMedia, with a hint naming the shortcut.
- Snapshots follow the theme: layout A (light) and B (dark). The title is
  editable and prefilled with the prompt; the watermark setting drops the
  brand row, keeping the headline and frame.
- "Full answers" scrolls every panel in lock-step, captures one screen per
  step and stitches each conversation without sticky headers, the
  composer or overlap, up to 8000 px with a note when truncated. Progress
  shows over the input bar and can be cancelled.
- After the capture the dialog focuses its first button, and the mode
  button that started a capture once it is enabled again.
Chrome keeps the registered service worker of an unpacked extension while
its version is unchanged, so the live profile ran an old background
script. The live copy now gets a fresh fourth version part.
Scroll-to-bottom buttons and edge fades stay pinned over the conversation,
so they repeated on every stitched screen. Full-answer captures now hide
them with visibility (keeping layout) and drop edge masks, re-checking
after every scroll, and put the original inline styles back on restore.
A header's backdrop without pointer events was never hit when checking
which bars cover the conversation, so its fade repeated at the top of
every screen. Layers of a covering bar now count with it. Overlay
scrollbars, which show up after every scroll, are hidden while stitching.
Claude shows a usage notice as a plain card above the input, inside a
transparent dock stuck to the bottom of the conversation. It repeated on
every stitched screen. While stitching, hide what sits next to the input's
ancestors inside the pinned composer and overlaps the clip.

Also let the e2e polls on the preview height retry while a newer preview
replaces the one being decoded.
Sends a long prompt to a live comparison, then snapshots it silently from
the toolbar icon in both layouts and both modes, and saves the images,
the dialog on desktop and at 390 px, and what each panel reported.
PANELIZE_SHARE_PROVIDERS leaves providers out, e.g. one near a usage limit.
…es and the cursor out

Full answers now aligns every panel to the prompt, then the latest user
turn, then the top, and puts each scroll position back afterwards.
Mid-content sticky headings and table headers are un-stuck, gradient
pseudo-element fades and overlay scrollbars are hidden, and the pointer is
hidden in every frame. The current view hides floating controls, fades and
composer notices too, while keeping the composer.
The share dialog opens in Full answers by default and then in the mode
picked last, kept in chrome.storage.local. When a snapshot needs Chrome's
tab sharing, the stream is kept paused at a low frame rate between
snapshots, so Chrome asks once per page. It is released on Stop, when the
track ends or when the page goes away, and the dialog says the tab is
being shared. The stream asks for no cursor.
Asking for a capture larger than the tab made Chrome raise the tab's
device pixel ratio, and every later constraint change re-laid it out,
which clamped virtual lists such as DeepSeek's. Cap the capture at the
tab's own device pixels, and when restoring follow a short virtual
range until it grows back to the remembered position or end.
Tab capture sends a frame only when the page changes, so waiting for two
frame callbacks could time out and take a frame from before the latest
scroll, repeating one screen and dropping the next in Full answers.
Flip a nearly transparent pixel to make Chrome send a frame, and take the
first one whose capture time is after the request; fail rather than use
an old frame.
Each screen's place and height were rounded on their own, so at a
fractional scale a screen could end one pixel before or after the next
began. The gap showed the cell's fill color, a dark line across Gemini's
code blocks. Round where each screen starts and ends instead, in the
crop from the frame and in the stitched image.
Put a solid panelize.dev pill with a Free Chrome extension caption on the
brand row of both layouts, and move the provider names and date into one
subline under the headline so light and dark show the same details. The
subline wraps once on narrow captures and stays without the watermark.
…t out

Panels cut by the grid's horizontal scroll were captured as slivers and
listed in the subline. Snapshots now take the panels fully on screen,
crop to them, and a note names the open providers left out. A window
narrower than one column still captures its most visible panel.

Co-Authored-By: Claude <noreply@anthropic.com>
…s to resize

Some provider pages (Kimi, Zhipu and others) hold a pixel min-width, so
narrow panels such as 1x12 cut off their right side.

- Provider frames report the width their layout needs; a panel narrower
  than that lays the page out at that width and scales it down to fit
  (down to 0.4), so media queries and fixed elements keep working and
  share snapshots match what is on screen.
- Handles between columns resize panels by dragging or arrow keys;
  widths are remembered per layout and double-click resets them.
"Long answers truncated — showing the first 13 screens" did not say which
answer was cut or what a screen is. The capture now reports the panels
whose answer did not fit the image, and the note names them.
Chrome draws the pointer into shared-tab frames for 2s after it moves or
clicks, regardless of the page's CSS cursor, so stitched full-answer
snapshots showed one pointer per screen. While tab sharing captures, a
transparent shield covers the panels and each frame waits until the
pointer near them has been still past Chrome's idle timeout, asking the
user to hold the mouse still.
The toolbar icon e2e tests click the action through the CDP method
Extensions.triggerAction, which the Chrome 145 bundled with Playwright
1.58 lacks; CI failed with "'Extensions.triggerAction' wasn't found".
Manho added 15 commits October 22, 2026 15:28
The page measured only what reached past its viewport, so once laid out at
the width it needs it reported 0, the panel shrank it back and it asked
again: the fit flickered and, when slow, stayed off.
A heading in an answer that repeats the prompt verbatim could win the
match and start the Full answers capture partway down the answer. Matches
inside a marked user turn now rank first.
captureVisibleTab takes whatever tab is in front of the window, so
switching tabs mid-capture could stitch another tab into the snapshot.
The source now checks before and after every frame that its own tab is
still the active one (and was not hidden meanwhile), stops with a message
asking to keep the tab in front, and does not fall back to tab sharing.
Full answers planned the image height of a page scaled down to fit its
panel in the page's own CSS pixels, so a long answer was cut well before
the image was full. The budget is now planned in on-screen pixels and the
cap converted back to the page's.

Stitched screens of such pages also start between rows; rounding to the
nearest row picked up the page header's edge and left a line at every
seam. Screens are now cropped to rows wholly inside them.
A page that stopped answering partway (e.g. its session expired) was
kept as captured so far but reported as cut to fit the image, or not at
all. It is now listed separately and named in the dialog and the image
note.
The layout check covered the window and the grid only, so dragging a
panel divider during a silent capture kept cropping later screens with
the old panel boxes and stitched parts of one panel into its neighbour.
Panel and frame boxes are now part of the check, so the capture starts
over as it does when the window is resized.
A page that answered a scroll request but stayed where it was (e.g.
while putting back its own position) ended its Full answers capture as
if it had reached the end, with no note. It is now asked once more, and
if it still does not move it is named as not captured to the end. The
note now says that, which also fits a page that stopped answering.
A page that scrolled past the requested position left a blank hole in its
stitched answer and still counted as complete. Only a screen that joins up
with what is covered is used; otherwise the page is asked again, then it is
named as stopped partway with the part that joins up.
… prompt

A page that kept scrolling back to its bottom after the alignment retry
was still stitched from there, so Full answers showed only the end of the
answer as if complete. The preparation now reports that it could not
align, and the panel is shown as on screen and named as current view only.
Download and Copy used the last rendered image, so a title cleared or
changed just before exporting was shared as it was. Both now wait for the
pending render; Copy hands the clipboard a promise so the click still
counts.
Signed-in Qwen (Global) puts a 60px rail of icons beside a scrolling
content box that holds the app. The box did not start at the left edge,
so it was not taken for the app shell and its min-width was never seen:
the page was cut off instead of scaled to fit its panel.
A conversation inside a box scrolled sideways reaches under the side rail
next to it, so every stitched screen repeated part of the rail.
A panel frame that has not started loading has an empty URL, which made
new URL() throw and failed the focus protection tests now and then.
@Manho
Manho merged commit eb2c35c into main Oct 22, 2026
2 checks passed
@Manho Manho mentioned this pull request Oct 22, 2026
@Manho
Manho deleted the feature/share-snapshot branch October 22, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant