* fix(dashboard): open widget links through the host instead of sandbox popups
Widget frames granted allow-popups/allow-popups-to-escape-sandbox to every
rendered widget, including grantState "none" widgets that never reach an
operator approval card. CSP has no navigate-to, so a popup bypassed the
connect-src 'none' containment those widgets otherwise run under. Scripted
window.open was blocked only by the document guard's open lock, which is
emitted solely when blockDescendantFrames is set, so two unrelated knobs had
to agree for the boundary to hold.
Restore both sandbox strings and route user-clicked links through the existing
ticket-bound bridge: a trusted click on a ticketed board widget's http(s)
target="_blank" anchor forwards to the Control UI, which opens the tab with
noopener,noreferrer. Links keep working for every rendered widget, the host
owns the scheme filter instead of trusting widget-authored rel attributes, and
scripted popups are impossible by construction rather than by coincidence.
Also collapses the unreachable controlUiBaseUrl branch and corrects both docs
pages, including the claim that links opened for every rendered widget, which
was never true for MCP-App board widgets.
Follow-up to #127315; refs #127314.
* fix(dashboard): open widget links through the Control UI external-url owner
The widget bridge opened links with a local window.open helper, which the
lint:ui:no-raw-window-open boundary guard rejects. Delegate to
openExternalUrlSafe(), the Control UI owner for external navigation: it
applies noopener/noreferrer, detaches the opener handle, and keeps one
canonical open path. The bridge keeps a narrower absolute-http(s) gate,
since widget-supplied links must not reach blob: URLs that the shared
external-link policy permits.
* fix(dashboard): route middle-button widget links and honor canceled clicks
Address ClawSweeper review on #128125.
The wrapper only handled primary click, so with popup permission removed from
both sandbox layers a middle-button activation silently did nothing. It also
listened on capture, so a widget handler's preventDefault could not cancel the
link the wrapper then opened itself.
Match the Control UI activation contract in ui/src/app/native-link-routing.ts:
listen for click and auxclick, gate on !defaultPrevented && !shiftKey &&
!altKey && (click+button 0 || auxclick+button 1), and listen on bubble so
widget cancellation stays effective. A middle click now routes for any widget
link, not just target=_blank, since middle-click always means new tab and
leaving plain links unrouted would keep the same silent-no-op class.
Move the wrapped-document test into src/canvas/wrap.test.ts: it exercises
buildWidgetDocument, so it belongs with its owner, and the added assertions
would otherwise push widget-tool.test.ts past the max-lines cap that AGENTS.md
forbids suppressing.
* improve(ui): consolidate dashboard chat visibility into one Chat/Split/Dashboard toggle
The dashboard face spread chat visibility across three separate
affordances: a two-option face toggle, an icon-only dock dropdown among
the header actions, and a floating vertically-rotated "Show chat"
button on the board edge that users could not discover. One concept,
three controls.
The header now renders a single segmented control - Chat | Split |
Dashboard - where Split is dashboard plus docked chat and Dashboard is
board-only (the former "Hide chat"). A compact caret attached to the
pill picks the dock side (left/right/bottom) and only appears in Split
mode when dock mutation is allowed; read-only sessions keep the plain
two-option Chat | Dashboard toggle. The floating reopen button, the
standalone dock menu button, and their CSS (including the mobile
overrides) are deleted, so the header ends up with fewer buttons than
before.
Verified in the mock-gateway dev harness: mode mapping, caret dropdown,
unhide-to-remembered-side, and hidden dock rendering board-only.
* fix(ci): unexport board dock settings union and repair stale plugin-sdk baseline
BoardVisibleChatDock lost its last external consumer when the board
view switch consolidated dock controls; keep it file-local so the
production unused-export scan stays clean.
Also regenerate docs/.generated/plugin-sdk-api-baseline.sha256: the
session-catalog module hash was stale on main (check fails on clean
origin/main after the agent-attribution reverts), which blocks every
PR's check-plugin-sdk-api-baseline job.
* docs(web): describe the Chat/Split/Dashboard switch and dock-side picker
* docs(automation): rename scheduled-tasks feature wording to Automations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhJ8EiMXue6ADLmHfb7FL6
* docs: regenerate docs map and add Automations glossary entries
* docs(templates): follow renamed automations-vs-heartbeat anchor
* docs(automation): fix markdown formatting drift
* docs(automation): teach the canonical automations tool and sync the copied heartbeat default
Review follow-ups: normal instructions use the automations tool with cron as
an explicit compatibility alias; every verbatim copy of the default heartbeat
prompt matches the new shipped text from the strings PR.
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>