feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459] - #87
Open
piyushsarin-sib wants to merge 7 commits into
Open
feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459]#87piyushsarin-sib wants to merge 7 commits into
piyushsarin-sib wants to merge 7 commits into
Conversation
This was referenced Sep 2, 2026
…nsions, private-only [BEX-281] The iframe-extension launch, CLI side: - The UI-app integration-type prompt offers Iframe (Embeds your page in a modal) next to Link, on a private app only — iframe extensions are private-only in v1. The choice is hidden (not disabled) on a public app: every layer refuses the combination. - The iframe branch asks for the embed URL and writes it to each entry's modal_iframe_url instead of redirect_link; the example context-URL lines in the created-app box read either destination. - Private-only enforced locally too: uiAppType.validateConfig refuses an iframeExtension block on a public app before any round trip (APP_UI_IFRAME_PRIVATE_ONLY), mirroring bo-be's 400. - Smoke: yarn smoke --suite=ui grows an iframe leg (create → upload no-op → install → uninstall → delete) that skips on a build without the choice or an environment without the registry flip; its new prompt pins are covered by en.test.ts like the existing ones. - Docs updated in the same PR (repo rule): agent-context/SKILL.md, agent-context/AGENTS.md, CLAUDE.md. One changeset (minor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and size seeding Rebase fallout from #92 (registry default_size seeding) and #97 (non-interactive actionLink UI creation) landing on main first: - resolveUiAppOrRedirectUris threads the distribution answer through to resolveUiApp, which gates the Iframe choice (private-only). The non-interactive path deliberately doesn't take it — that route is actionLink-only by design, so its buildSurfacePointList call pins urlField: 'redirect_link'. - buildSurfacePointList carries both #92's sizeFor and this branch's urlField/url; its unit tests updated to the merged signature. - Agent docs merged: the non-interactive flags (#97) and the Iframe choice documented side by side, both noting the non-interactive routes stay actionLink-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
piyushsarin-sib
force-pushed
the
feat/iframe-extension-authoring
branch
from
September 3, 2026 07:38
5b957d8 to
7c31953
Compare
An iframeExtension entry on a widget slot can author layout: "inline" (the page embedded directly in the card body) vs "modal" (the default — never written, so layout-less configs stay byte-identical). - create asks the question only for an iframe WIDGET placement (an action slot's menu entry is a modal by definition) and writes the key only for inline. - validateUiApp refuses layout on actionLink entries and pins the vocabulary; the widget-only rule stays server-side (registry fact). - fields.ts gains the layout: row, so the upload diff, created-app box and install summary all show it. - Docs (SKILL/AGENTS) + changeset extended; 1437 tests green. Requires bo-be with per-entry layout support (same-branch PR #404) — an older server 400s the key as unknown, the standard sequencing rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[BEX-467] bo-be stamps a root `sandbox` onto the stored snapshot — the iframe policy the platform decides, not the partner. Left in the wire-only list's blind spot, the first successful upload writes it into app-config.json and every upload after that reports drift on a field nobody can edit. `stripUiAppWireOnlyKeys` is the single reader and strips at every depth, so naming the key in `uiAppType.wireOnlyKeys` is the whole change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…[BEX-459] Four rules that finish per-entry iframe authoring, all of them about a value the CLI would otherwise write and something downstream would then ignore or reject. `modal_size` — `small` / `medium` / `large`, asked for every iframe entry that actually OPENS a modal. That is deliberately a different set of placements from the layout question's: layout is widget-only (an action slot's menu entry has one presentation), while the menu entry opens a modal too, and a widget card answered `inline` opens none. Large is the default, is pre-selected rather than listed first, and writes nothing — so a default answer leaves app-config.json byte-identical to one authored before the question existed, the same contract `layout: "modal"` has. `validateUiApp` pins the vocabulary and refuses the field on an `actionLink`; the `modal size:` row is one more `VALUE_ROWS` entry, so the upload diff, the created-app box and `app install`'s summary all gain it at once. `buildSurfacePointList` now refuses a `layout` on a row that renders no card, naming the entry the way the validator does. The prompt never asks for one there, so this cannot fire from the interactive flow — but the builder is what stamps the field and it stamps whatever row it is handed, so an unchecked caller would author a block the upload endpoint rejects and the partner would meet the rule one round trip later, phrased by the server. `--ui-config` refuses `layout` and `modal_size` by name instead of dropping them. The file's key set is fixed and everything else is discarded, so a silently ignored presentation field means the created app renders differently from the file that asked for it, with nothing said anywhere. `app upload` translates the platform's own layout refusal — narrowly, exactly like `isPublicDistributionRefusal`: only a 400 that mentions `layout` is relabelled, so an unrelated 400 keeps the server's text. No local slot list: whether a slot renders a card is a registry fact and the CLI holds no copy of the registry, so the server stays the authority and this only says what to edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ui suite's iframe leg went URL -> output directory. Two questions can render in between and neither is guaranteed: the layout list appears only when the first iframe-enabled registry row on the picked page is a widget, and the modal-size list only when a modal actually opens. Which row the registry serves first is not this suite's business, so the leg cannot know in advance, and either question is also missing from an older build under --against=published. The pty runner gains `optional` on an exchange: a prompt that hasn't appeared is skipped once a LATER one has — proof the flow moved past it, rather than that it is merely slow — and trailing optionals left unmatched at exit are not a broken flow. Distinct from the existing `send: () => null` abort, which says the prompt rendered without the choice this leg needs and skips the whole leg. Both new questions are answered with their default (Enter), and the leg now asserts neither wrote a key — the only part of the omit-the-default contract a real run can prove. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y [BEX-459] Both agent docs gain `modal_size` next to `layout`, and say plainly that the two are gated differently — an agent that assumes they cover the same placements will author a size on an inline card, or omit one on a menu entry. Also: `--ui-config` refuses either field by name, and `sandbox` joins the server-stamped keys that must never be written into app-config.json. Appended to the branch's existing changeset rather than adding a second file; the bump level stays minor, since this is all new surface on an unreleased feature. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Jira: BEX-459 · Epic: BEX-211
What
Phase 1 (authoring release) of the iframe-extension launch, CLI side. The validator, the upload diff (
modal URL:row), the wire-only strip and the write-back already spokeiframeExtension— only the prompt withheld it (removed in63bc9eewhile authoring wasn't ready).modal_iframe_urlinstead ofredirect_link. Nosizeprompt (decided): the modal's dimensions are hand-authored per entry inapp-config.json.uiAppType.validateConfigrefuses a hand-authorediframeExtensionblock on a public app before any round trip, with the same wording as bo-be's 400. (readProjectConfigdefaults an absentdistribution_typetoprivate, so only an explicitpublicreaches it.)yarn smoke --suite=uigrows an iframe leg — create (pty) → upload no-op → install → uninstall → delete — that skips rather than fails on a build whose prompt has no Iframe choice or an environment whose registry has no slot enabled foriframeExtensionyet (pre-flip). New prompt patterns are pinned againstmessagesinen.test.tslike the existing ones (the PR fix: harmonize app-create prompt copy (distribution & UI-app fields) #73 pty-timeout precedent).agent-context/SKILL.md,agent-context/AGENTS.md,CLAUDE.md. One changeset,minor.Tests
modal_iframe_url(neverredirect_link/link_target) and narrows both registry reads byiframeExtension; iframe-specific URL prompt copy.scripts/tsconfig.json.Sequencing
The staging registry flip lands before this releases (bo-be PR DTSL/app-store-bo-be#404 carries the SQL): on an un-flipped environment the restored choice dead-ends in
APP_CREATE_UI_POINTS_NONE_FOR_TYPE, which reads as a CLI bug and isn't. The prod flip follows this CLI release.Part of the iframe-extension Phase 1 (epic BEX-281). Counterparts: bo-be DTSL/app-store-bo-be#404 (registry flip + server-side private-only rule); kit PR (sandbox + modal size) to follow.
🤖 Generated with Claude Code
Update 2026-09-03 — rebased onto main (#92 + #97)
Main gained the registry default_size seeding (#92, the cherry-picked successor of #91, released in 2.2.1) and the non-interactive actionLink UI creation (#97, released in 2.2.2). Reconciliation on this branch:
resolveUiAppOrRedirectUris(from feat(app-create): non-interactive actionLink UI app creation #97) now threads thedistributionanswer through toresolveUiApp, which gates the Iframe choice (private-only). The non-interactive path deliberately doesn't take it —--ui-app/--ui-configstayactionLink-only by design, so that route'sbuildSurfacePointListcall pinsurlField: 'redirect_link'.buildSurfacePointListcarries both feat(app-create): seed placement size from the slot's registry default [BEX-461] #92'ssizeForand this branch'surlField/url; its unit tests updated to the merged signature. An iframe entry therefore also picks up a slot's registry default size at create — the one-knob-two-surfaces note from feat(app-create): seed placement size from the slot's registry default [BEX-461] #91/feat(app-create): seed placement size from the slot's registry default [BEX-461] #92 applies (a seeded height also drives the modal body).actionLink-only.1434 tests green, lint + prettier clean, smoke scripts typecheck.
Update 2026-09-03 (2) — inline iframe cards
An
iframeExtensionentry on a widget slot can now authorlayout:"inline"(page embedded directly in the card body) vs"modal"(the default — never written, so layout-less configs are byte-identical).app createasks the question only for iframe widget placements;validateUiApprefuses the field onactionLinkentries; the diff/box/install summary gain alayout:row viaVALUE_ROWS. Counterparts: bo-be #404 (validation + storage, widget-only rule from the registry), app-store-backend #757 (servesappConfigs.layout,cacheSchemaVersionv9→v10), kit #317 (inline routing). Deploy order: bo-be → backend → kit → CLI.Update 2026-09-04 —
layoutis widget-only, and what still lands in this PRRule.
layout: "inline"is only meaningful on the 6 widget slots (contactDetails|dealDetails|companyDetails×overviewMain.widget|overviewSidebar.widget), the registry rows whosecomponent_typeiswidgetand therefore render a card. The 3 header-menu slots (*.headerMenu.action,component_type: "action") yield a menu entry that must open something, so there is nothing to be inline in. The slot kind is judged from the registry row, never from a local list (CLAUDE.md: no registry mirror in the CLI).What this PR does today
promptIframeLayout(src/app-types/ui/authoring.ts) asks How should it appear on the page? only when the picked placement's row hascomponent_type === 'widget'. A header-menu placement never sees the question and its entry is written withoutlayout.buildSurfacePointListcan only ever writeinline;modalis the default and is never stamped.validateUiApppins the vocabulary ("<value>" is not supported — use "modal" or "inline") and refuseslayouton anyactionLinkentry. It deliberately has no local slot-kind check; the platform'svalidateLayoutPerEntryanswers that on create and upload with:400 ui_app.surface_point_list authors layout "inline" on slot(s) that render no card: <slug> (component_type "action")--ui-app/--ui-configstayactionLink-only;layoutis not part of their input.Landing before merge (same PR)
layoutfor a row whosecomponent_typeis notwidget, throw locally naming the entry (ui_app.surface_point_list["<slug>"].layout: "inline" needs a widget slot; <slug> renders a menu entry). Same outcome as the skip today, but a future multi-select or a newcomponent_typevalue can no longer author inline silently.en.tsmessage sobrevo app uploadof a hand-editedinlineon a header-menu slot reads like every other per-entry refusal.actionLinkrefusal (none exist yet),en.test.tspins for the three layout prompt strings, and a create test that the question is absent for a header-menu placement (exists) plus one that a widget placement gets it.--ui-configrefuses alayoutkey explicitly instead of dropping it.Planned in this PR: modal size presets. For an iframe entry that opens a modal (widget slot with modal layout, or any header-menu slot)
app createasks Modal size — Small / Medium / Large, default Large. Large is the platform's current behaviour (the kit'sModalDialogis hard-codedsize="lg"), so an unanswered or Large answer writes nothing and layout-less configs stay byte-identical. Wire: per-entrymodal_size("small" | "medium" | "large",iframeExtensiononly, refused onactionLink), validated by bo-be, served asappConfigs.modalSizeinside the same v9→v10 cache bump aslayout, mapped by the kit onto the dialog size. This is a preset for the dialog, distinct from the widget card'ssize, which is still seeded from the slot's registry default (BEX-461) and hand-edited in px/%; the CLI still asks no px/% question. Counterparts to follow in bo-be #404, backend #757, kit #317. Tracked under BEX-211 / BEX-467.Sandbox stored in the snapshot, never authorable (2026-09-04). bo-be now stamps
sandboxinto the storedui_appsnapshot at the root with the defaultallow-scripts allow-same-origin allow-forms allow-popups, server-side only. The CLI API neither accepts it (an authoredsandboxinapp-config.jsonis a 400unknown key) nor echoes it. This PR addssandboxtouiAppType.wireOnlyKeysso a value can never reachapp-config.jsoneven if it were echoed, exactly likelink_targetandextension_point_name. In M1 the value is store-only: the manifest does not serve it and the kit keeps its constant.Update 2026-09-04 (3) — plan sync
ui.showConfirmis no longer dropped from v1; it moved to Phase 2c (host confirm dialogs for inline cards, M2c 2026-11-13). No change to this PR.app-config.json(Phase 1, sibling change).createwill write an app-type field (OAuth vs UI) so the file states what it is instead of relying on the presence ofui_app;uploadchecks it agrees with theui_app/authblock and refuses a mismatch with a migration hint;isUiAppConfig()stays the single reader; files without the field keep working. Open: wire (bo-be reflected key sets + openapi) vs file-only key stripped before upload. Lands in its own PR on top of this branch;CLAUDE.md's "there is no appType key" note changes with it.Plan of record: https://app.notion.com/p/sendinblue/Iframe-Embed-timelines-3d1449002dcb80fc8c3ff1a6a0c7c0b0