Skip to content

Previews: show a header Preview button and side panel only while a window has something to preview - #590

Merged
ZacxDev merged 8 commits into
mainfrom
feat/previews-ui-live-only
Nov 11, 2026
Merged

ZacxDev merged 8 commits into
mainfrom
feat/previews-ui-live-only

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner

What

The previews control on a mirrored window's session views is now shown only while that window has something to preview, and it lives in the header, not in a row above the chat.

  • Protocol (no version bump, compatible both ways): muster-mirror -preview sends a new previews host to muster message on its existing mirror connection, a whole map of tmux window id to {offered, open} (counts only, no port or process). It is sent after the window list and again whenever it changes, and an empty map clears it. It follows the existing work/colors per-window-message convention: an older muster's read loop drops the unknown type, and an older host never sends one, so its windows show no button.
  • Host source: previewhost.Host.Summary(). offered counts listener reports with no open preview on that port (the same set the gateway's session page lists as offers), and open counts open previews. It returns nil while no gateway tunnel serves the offer stream or while previews are off.
  • Server state: in memory on the hub's per-host state (HostView.Previews, copied to SessionView.Previews). It is never kept for an offline host and never stored. A change nudges the owner's views (agent.changed), the same as work counts.
  • UI:
    • A header Preview button with a count badge, beside the Rendered | Terminal switch. Below sm it is a 44x44 icon with the badge on its corner.
    • The button opens a side panel with the gateway session page in the sandboxed frame. From sm up the panel sits over the right of the page; on phones it takes the whole screen. Close or Escape unloads it.
    • The panel's header has a New tab hand-off, with the same tab-before-mint and noopener behaviour as before, plus a picker when there is more than one viewer plugin.
    • The sidebar row gets a small browser-window glyph. It is not a dot, because the waiting mark is already an accent dot.
  • Gates: viewerPlugins() stays the first gate (no slot, no poll, no frame-src without a live viewer plugin). The counts only narrow it: the button is drawn only while offered + open > 0. frame-src follows the viewer plugins alone, so a button that appears after load can still frame its panel.
  • Live updates: the button's slot polls GET /ui/mirror/preview-button every 10 s while the tab is visible. The session views hold no event stream, and this keeps it that way. A zero-count poll does not read the plugin store, and a revoked token removes the button at the next poll. The sidebar refreshes on its existing triggers.

Tests

  • Go:
    • internal/mirror/previews_test.go: normalise, pinned wire bytes, hub arrival/nudge/clear/offline.
    • internal/mirrorhost/previews_test.go: send after the windows, re-send only on change, none without -preview, worst case fits one frame.
    • internal/previewhost/summary_test.go: offers minus open ports, nil without a tunnel or while off.
    • internal/api/plugin_chip_test.go: button only while counts > 0 on both views, the poll route's gates, the sidebar mark, the existing CSP/sandbox/gate tests adapted.
    • cmd/muster-mirror: the shipped build cannot take counts from the e2e file.
  • Mutation sweep over the gates: 16 of 17 mutants killed, each by the named test. The one survivor, dropping the ok check in NormalizePreviews, is equivalent, because iteration is over listed windows and a zero entry is dropped anyway. A no-op control mutant survived, as it should.
  • e2e:
    • previews.spec.ts drives the real chain through the button and panel, and adds a hidden-when-nothing case: a quiet @2 window on the real host's report gets no button and no sidebar mark, with @1 as the control.
    • plugin-chip.spec.ts plays the counts via MUSTER_MIRROR_E2E_PREVIEWS_FILE and covers the button appearing and disappearing live on an open page.
    • cls.spec.ts measures the views with the button.

e2e was not run locally; CI is the authority.

🤖 Generated with Claude Code

ZacxDev and others added 8 commits November 10, 2026 20:45
…indow has something to preview

The mirror reports per-window preview counts (offered, open) on a new
previews message, the hub keeps them with the window's live state, and the
session views draw a header Preview button (count badge) that opens the
gateway's session page in a side panel, with New tab as the secondary hand-off.
The sidebar marks windows with something to preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n-nothing; docs describe the built behaviour

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rincipal; document MUSTER_MIRROR_E2E_PREVIEWS_FILE

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bject route (its answer never tells a window from a ghost)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ond row, so the title keeps its width

Below sm the button is a 20px pill at the end of the status line (laid out at
the line's own height, a 44px tap target above the title's hit area); from sm
up it stays beside the Rendered | Terminal switch. One slot per place, each
polled only at its width. e2e asserts the title, header and conversation do
not move when the button appears at 390px, and the desktop placement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and full-screen focus defer properly

- The sidebar's preview mark now has the Preview button's own gate: without a
  live viewer plugin (revoked, no viewer, MUSTER_PLUGIN_ORIGINS unset) its
  counts are zeroed, read once per list and not at all with the seam off.
- Escape closes the panel only when no other dialog or popover is open and no
  handler took it.
- Full screen (below sm), everything behind the panel is inert while it is
  open, restored on close.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements & deletion), recorded late from the round's report: verdict proceed to the checklist, no open premises, one deletion candidate.

D1 — the panel's multi-plugin <select> picker (internal/ui/plugin_chip.go:257-265): only reachable with ≥2 live viewer plugins; the deployed setup has one — keep only if multi-viewer owners are a real case

@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (nine-axis checklist) at 5a18cc1b: no 🔴, so the ladder ends here. One 🟡 and one 🟢 were fixed in bd8d22c5 and not re-audited. ef5877e6 only merges main, which brings in #591.

1. 🟡 the sidebar preview icon is drawn only when the owner has a live viewer plugin, the same gate as the view's Preview button
2. 🟢 Escape closes the preview panel only when it is not meant for another open dialog
3. 🟢 on phones, the full-screen panel keeps focus inside it: the content behind it is inert while it is open

@ZacxDev
ZacxDev marked this pull request as ready for review November 11, 2026 07:18
@ZacxDev
ZacxDev merged commit ca4e62d into main Nov 11, 2026
19 checks passed
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