| ... | @@ -38,15 +38,10 @@ import { useEnvironmentQuery } from "../../state/query"; | ... | @@ -38,15 +38,10 @@ import { useEnvironmentQuery } from "../../state/query"; |
| 38 | import { enqueueThreadOutboxMessage } from "../../state/thread-outbox"; | 38 | import { enqueueThreadOutboxMessage } from "../../state/thread-outbox"; |
| 39 | import { useAtomCommand } from "../../state/use-atom-command"; | 39 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 40 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 40 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| | 41 | import { MCP_APP_INLINE_MAX_HEIGHT, mcpAppInlineHeight } from "./mcpAppSizing"; |
| 41 | | 42 | |
| 42 | /** The feed reserves a fixed box for an app; a taller app scrolls inside it. */ | | |
| 43 | const MCP_APP_ROW_HEIGHT = 420; | | |
| 44 | const ROW_BOTTOM_MARGIN = 8; | 43 | const ROW_BOTTOM_MARGIN = 8; |
| 45 | | 44 | |
| 46 | export function mcpAppRowHeight() { | | |
| 47 | return MCP_APP_ROW_HEIGHT + ROW_BOTTOM_MARGIN; | | |
| 48 | } | | |
| 49 | | | |
| 50 | // The WebView loads a tiny outer page that hosts the app in a real | 45 | // The WebView loads a tiny outer page that hosts the app in a real |
| 51 | // opaque-origin iframe, as web does, so the app's `window.parent` and the | 46 | // opaque-origin iframe, as web does, so the app's `window.parent` and the |
| 52 | // `event.source` of host replies are a real window, which the MCP Apps SDK | 47 | // `event.source` of host replies are a real window, which the MCP Apps SDK |
| ... | @@ -104,7 +99,7 @@ const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024; | ... | @@ -104,7 +99,7 @@ const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024; |
| 104 | | 99 | |
| 105 | /** | 100 | /** |
| 106 | * A captured MCP App, hosted in a WebView over the MCP Apps bridge: inline as | 101 | * A captured MCP App, hosted in a WebView over the MCP Apps bridge: inline as |
| 107 | * a fixed row of the thread feed, or full screen in its own modal screen. A | 102 | * a content-sized row of the thread feed, or full screen in its own modal screen. A |
| 108 | * WebView cannot move between the two without reloading, so each is its own | 103 | * WebView cannot move between the two without reloading, so each is its own |
| 109 | * view of the app: the inline one is torn down before full screen opens, and | 104 | * view of the app: the inline one is torn down before full screen opens, and |
| 110 | * comes back when it closes. | 105 | * comes back when it closes. |
| ... | @@ -119,7 +114,7 @@ export function ThreadMcpApp(props: { | ... | @@ -119,7 +114,7 @@ export function ThreadMcpApp(props: { |
| 119 | readonly revision: string; | 114 | readonly revision: string; |
| 120 | readonly app: McpAppReference; | 115 | readonly app: McpAppReference; |
| 121 | readonly width: number; | 116 | readonly width: number; |
| 122 | /** Full screen fills its screen; inline is the feed's fixed row. */ | 117 | /** Full screen fills its screen; inline follows the app's reported height. */ |
| 123 | readonly displayMode?: "inline" | "fullscreen"; | 118 | readonly displayMode?: "inline" | "fullscreen"; |
| 124 | /** Full screen only: leaves it, back to the inline row. */ | 119 | /** Full screen only: leaves it, back to the inline row. */ |
| 125 | readonly onExitFullscreen?: () => void; | 120 | readonly onExitFullscreen?: () => void; |
| ... | @@ -128,8 +123,12 @@ export function ThreadMcpApp(props: { | ... | @@ -128,8 +123,12 @@ export function ThreadMcpApp(props: { |
| 128 | }) { | 123 | }) { |
| 129 | // One reference per app: a new object with the same content (a refetch, a | 124 | // One reference per app: a new object with the same content (a refetch, a |
| 130 | // rerender) must not rebuild the host of a document that is already live. | 125 | // rerender) must not rebuild the host of a document that is already live. |
| | 126 | const [height, setHeight] = useState(MCP_APP_INLINE_MAX_HEIGHT); |
| 131 | const [app, setApp] = useState(props.app); | 127 | const [app, setApp] = useState(props.app); |
| 132 | if (!mcpAppReferencesEqual(app, props.app)) setApp(props.app); | 128 | if (!mcpAppReferencesEqual(app, props.app)) { |
| | 129 | setApp(props.app); |
| | 130 | setHeight(MCP_APP_INLINE_MAX_HEIGHT); |
| | 131 | } |
| 133 | const fullscreen = props.displayMode === "fullscreen"; | 132 | const fullscreen = props.displayMode === "fullscreen"; |
| 134 | const navigation = useNavigation(); | 133 | const navigation = useNavigation(); |
| 135 | const insets = useSafeAreaInsets(); | 134 | const insets = useSafeAreaInsets(); |
| ... | @@ -183,6 +182,7 @@ export function ThreadMcpApp(props: { | ... | @@ -183,6 +182,7 @@ export function ThreadMcpApp(props: { |
| 183 | return; | 182 | return; |
| 184 | } | 183 | } |
| 185 | setLoaded(false); | 184 | setLoaded(false); |
| | 185 | setHeight(MCP_APP_INLINE_MAX_HEIGHT); |
| 186 | // The first URL's token may have expired by now; the asset query keeps | 186 | // The first URL's token may have expired by now; the asset query keeps |
| 187 | // a refreshed one, which this new view can load. | 187 | // a refreshed one, which this new view can load. |
| 188 | if (asset._tag === "Success") setUri(asset.url); | 188 | if (asset._tag === "Success") setUri(asset.url); |
| ... | @@ -194,6 +194,7 @@ export function ThreadMcpApp(props: { | ... | @@ -194,6 +194,7 @@ export function ThreadMcpApp(props: { |
| 194 | // loads the asset query's current URL: the first one's token may be gone. | 194 | // loads the asset query's current URL: the first one's token may be gone. |
| 195 | const setDocumentKey = (next: (value: number) => number) => { | 195 | const setDocumentKey = (next: (value: number) => number) => { |
| 196 | setLoaded(false); | 196 | setLoaded(false); |
| | 197 | setHeight(MCP_APP_INLINE_MAX_HEIGHT); |
| 197 | setNavigatedAway(false); | 198 | setNavigatedAway(false); |
| 198 | if (asset._tag === "Success") setUri(asset.url); | 199 | if (asset._tag === "Success") setUri(asset.url); |
| 199 | setDocumentKeyState(next); | 200 | setDocumentKeyState(next); |
| ... | @@ -296,11 +297,12 @@ export function ThreadMcpApp(props: { | ... | @@ -296,11 +297,12 @@ export function ThreadMcpApp(props: { |
| 296 | styles: { variables: mcpAppStyleVariables(current.theme.variables) }, | 297 | styles: { variables: mcpAppStyleVariables(current.theme.variables) }, |
| 297 | displayMode: isFullscreen ? "fullscreen" : "inline", | 298 | displayMode: isFullscreen ? "fullscreen" : "inline", |
| 298 | availableDisplayModes: ["inline", "fullscreen"], | 299 | availableDisplayModes: ["inline", "fullscreen"], |
| 299 | // Both are fixed boxes, so the app is told its exact size. | 300 | containerDimensions: isFullscreen |
| 300 | containerDimensions: { | 301 | ? { |
| 301 | width: current.props.width, | 302 | width: current.props.width, |
| 302 | height: isFullscreen ? (current.props.height ?? MCP_APP_ROW_HEIGHT) : MCP_APP_ROW_HEIGHT, | 303 | height: current.props.height ?? MCP_APP_INLINE_MAX_HEIGHT, |
| 303 | }, | 304 | } |
| | 305 | : { width: current.props.width, maxHeight: MCP_APP_INLINE_MAX_HEIGHT }, |
| 304 | platform: "mobile", | 306 | platform: "mobile", |
| 305 | locale: Intl.DateTimeFormat().resolvedOptions().locale, | 307 | locale: Intl.DateTimeFormat().resolvedOptions().locale, |
| 306 | timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, | 308 | timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, |
| ... | @@ -475,9 +477,11 @@ export function ThreadMcpApp(props: { | ... | @@ -475,9 +477,11 @@ export function ThreadMcpApp(props: { |
| 475 | void hostRef.current?.teardown().then(() => setClosed(true)); | 477 | void hostRef.current?.teardown().then(() => setClosed(true)); |
| 476 | } | 478 | } |
| 477 | }, | 479 | }, |
| 478 | // Both modes are fixed boxes, so the app's own height only decides | 480 | onSizeChanged: (size) => { |
| 479 | // whether it scrolls inside one. | 481 | if (latest.current.props.displayMode === "fullscreen") return; |
| 480 | onSizeChanged: () => undefined, | 482 | const nextHeight = mcpAppInlineHeight(size.height); |
| | 483 | if (nextHeight !== undefined) setHeight(nextHeight); |
| | 484 | }, |
| 481 | }); | 485 | }); |
| 482 | hostRef.current = next; | 486 | hostRef.current = next; |
| 483 | return () => { | 487 | return () => { |
| ... | @@ -510,7 +514,7 @@ export function ThreadMcpApp(props: { | ... | @@ -510,7 +514,7 @@ export function ThreadMcpApp(props: { |
| 510 | if (presentedFullscreen) { | 514 | if (presentedFullscreen) { |
| 511 | return ( | 515 | return ( |
| 512 | <View | 516 | <View |
| 513 | style={{ height: MCP_APP_ROW_HEIGHT, marginBottom: ROW_BOTTOM_MARGIN }} | 517 | style={{ height, marginBottom: ROW_BOTTOM_MARGIN }} |
| 514 | className="items-center justify-center rounded-lg border border-border" | 518 | className="items-center justify-center rounded-lg border border-border" |
| 515 | > | 519 | > |
| 516 | <Text className="text-sm text-foreground-muted"> | 520 | <Text className="text-sm text-foreground-muted"> |
| ... | @@ -523,7 +527,7 @@ export function ThreadMcpApp(props: { | ... | @@ -523,7 +527,7 @@ export function ThreadMcpApp(props: { |
| 523 | if (closed) { | 527 | if (closed) { |
| 524 | return ( | 528 | return ( |
| 525 | <View | 529 | <View |
| 526 | style={{ height: MCP_APP_ROW_HEIGHT, marginBottom: ROW_BOTTOM_MARGIN }} | 530 | style={{ height, marginBottom: ROW_BOTTOM_MARGIN }} |
| 527 | className="items-center justify-center gap-2 rounded-lg border border-border" | 531 | className="items-center justify-center gap-2 rounded-lg border border-border" |
| 528 | > | 532 | > |
| 529 | <Text className="text-sm text-foreground-muted">The {app.server} app was closed</Text> | 533 | <Text className="text-sm text-foreground-muted">The {app.server} app was closed</Text> |
| ... | @@ -541,11 +545,7 @@ export function ThreadMcpApp(props: { | ... | @@ -541,11 +545,7 @@ export function ThreadMcpApp(props: { |
| 541 | } | 545 | } |
| 542 | | 546 | |
| 543 | return ( | 547 | return ( |
| 544 | <View | 548 | <View style={fullscreen ? { flex: 1 } : { height, marginBottom: ROW_BOTTOM_MARGIN }}> |
| 545 | style={ | | |
| 546 | fullscreen ? { flex: 1 } : { height: MCP_APP_ROW_HEIGHT, marginBottom: ROW_BOTTOM_MARGIN } | | |
| 547 | } | | |
| 548 | > | | |
| 549 | {navigatedAway ? ( | 549 | {navigatedAway ? ( |
| 550 | <View className="flex-1 items-center justify-center"> | 550 | <View className="flex-1 items-center justify-center"> |
| 551 | <Text className="text-sm text-foreground-muted"> | 551 | <Text className="text-sm text-foreground-muted"> |