fix(mcp-apps): keep the sidebar visible in fullscreen, and give every app a way into it - #7481
Conversation
Adds an `open_in_fullscreen` column to `apps` and the matching optional field on `PATCH /api/apps/:appId`, so an app whose UI is the whole point can declare that it should fill its host surface on first render instead of sitting inline next to the conversation. The column seeds the first render only — the live display mode stays a per-view toggle owned by the host chrome and the app's own `ui/request-display-mode` — so an unrelated edit never resets it.
`McpAppCard` sized its fullscreen overlay to `window.innerWidth` / `window.innerHeight` at `(0, 0)` and painted it at `z-[100]`, so an app filling the screen covered the navigation as well — and the only way back to it was to leave fullscreen, since the sidebar's own collapse control sat underneath the overlay too. It now measures the app shell's `<main>` region instead (`MAIN_CONTENT_ID`, moved into `lib/app-shell-region` so the shell and the card share one id) and re-measures through a `ResizeObserver` on that element, which is what reports the sidebar's collapse/expand animation — no window resize fires for it. Chrome-less routes (`/a/…`, the browser preview, the recording renderer) render no shell, so the viewport stays the fallback there. Two z-index consequences fall out of the overlay no longer being viewport-wide: the card drops to `z-40`, above the page's own content (a `z-20` sticky divider is the tallest) but below the `z-50` portal layer, so a dialog opened from the app's chrome now renders over it rather than under it; and the sidebar's collapse pill moves to `z-50`, because it straddles the region's left edge and would otherwise be clipped in half by the app.
…n open-in-fullscreen default ## The gap Fullscreen was only ever reachable from inside the guest. `mcp-app-chrome` exported an exit button and nothing else, so the only way *in* was the app calling `ui/request-display-mode` over the ext-apps SDK. Apps shipped by an MCP server commonly do that; apps authored here generally do not — so the same display mode was available for one kind of app and not the other, with no host affordance to close the gap. ## The host owns the surface `McpAppFullscreenExitButton` becomes `McpAppFullscreenButton`, which renders Maximize2/"Enter fullscreen" or Minimize2/"Exit fullscreen" from an `isFullscreen` prop. `McpAppEntryContent` renders it unconditionally on both surfaces it chromes: the side panel's top bar (previously exit-only) and the chat-inline hover overlay (previously mounted only while fullscreen). Toggling from the host still flows back to the guest — the runtime already pushes `displayMode` into the bridge's host context. ## Opening fullscreen by default An app whose UI is the point — a dashboard opened to be looked at, not talked to — should not need the chat dismissed by hand on every open. `apps.open_in_fullscreen` (previous commit) is surfaced in App settings as an "Opens in" select, Inline or Fullscreen, alongside the existing status and modification controls. `McpAppEntryContent` seeds `displayMode` from it once, and only on a surface that is actually showing the app, so leaving fullscreen — or the app asking for inline over the SDK — is never undone underneath the viewer. The standalone run page (`/a/…`) is left alone: it is chrome-less and full-page already. The e2e case drives the whole path in a browser: an app saved with the default open filling the page, the sidebar still painted beside it, and the exit staying exited.
`handleShowInPanel` said the panel "is the app's frame — never fullscreen there", which stopped being true once the panel's top bar gained the way in. The reset it describes is still right, for a different reason: the panel instance owns its own display mode from that point, so leaving the inline one fullscreen would strand an invisible overlay behind the panel.
Eight one-off Playwright scripts I used to drive the local stack while verifying this change (and to seed the demo recording) were swept into the previous commit. They are scratch, not tests — the real coverage is the Playwright case in `tests/apps.spec.ts`.
Resolves the migration-number collision: main landed `0438_sudden_azazel` (chatops channel instructions) on the same index this branch had taken. Took main's `_journal.json` and `0438_snapshot.json`, dropped the branch's `0438_chilly_bulldozer.sql`, and regenerated it against main's snapshot as `0439_chilly_bulldozer.sql`. The regenerated DDL is byte-identical to the original — the migration is a single `ADD COLUMN` with no data-migration tail — so only its index and snapshot changed. `drizzle-kit check` and `check:migrations` both pass, and `pnpm codegen` reports no drift against the auto-merged API client.
Same migration-number collision as the previous merge, one index up: main landed `0439_harsh_matthew_murdock` on the index this branch had taken. Took main's `_journal.json` and `0439_snapshot.json`, dropped the branch's `0439_chilly_bulldozer.sql`, and regenerated it against main's snapshot as `0440_chilly_bulldozer.sql`. The regenerated DDL is byte-identical again (a single `ADD COLUMN`, no data tail), so only its index and snapshot moved. None of the 22 commits main picked up touch a file this branch changes. `drizzle-kit check` and `check:migrations` pass, `pnpm codegen` reports no drift, and the full `turbo check:commit` is green across all four workspaces.
Playwright test resultsDetails
Skipped testsidentity-providers › identity-providers.ee.spec.ts › Identity Provider SAML E2E Flow with Keycloak › should configure SAML provider, login via SSO, update, and delete |
|
E2E now covers this branch. I added the All five legs pass ( The branch is also merged up to current |
No description provided.