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