Repository: block/goose
File: ui/desktop/src/components/McpApps/McpAppRenderer.tsx (TypeScript)

Code before:
        case 'sampling/createMessage': {
          if (!sessionId || !apiHost || !secretKey) {
            throw new Error('Session not initialized for sampling request');
          }
          const { messages, systemPrompt, maxTokens } =
            params as McpMethodParams['sampling/createMessage'];
          const response = await fetch(`${apiHost}/sessions/${sessionId}/sampling/message`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'X-Secret-Key': secretKey,
            },
            body: JSON.stringify({
              messages: messages.map((m) => ({
                role: m.role,
                content: m.content,
              })),
              systemPrompt,
              maxTokens,
            }),
          });
          if (!response.ok) {
            throw new Error(`Sampling request failed: ${response.statusText}`);
          }
          return (await response.json()) as McpMethodResponse['sampling/createMessage'];
        }

        case 'ping':
          return {} satisfies McpMethodResponse['ping'];

        default:
          throw new Error(`Unknown method: ${method}`);
      }
    },
    [append, sessionId, extensionName, apiHost, secretKey]
  );

  const handleSizeChanged = useCallback((height: number, width?: number) => {
    const newHeight = Math.max(DEFAULT_IFRAME_HEIGHT, height);
    setIframeHeight(newHeight);
    setIframeWidth(width ?? null);
  }, []);

  const { iframeRef, proxyUrl } = useSandboxBridge({
    resourceHtml: resource.html || '',
    resourceCsp: resource.csp,
    resourcePermissions: resource.permissions,
    resourceUri,
    toolInput,
    toolInputPartial,
    toolResult,

Review comment (bug):
this is fantastic! 

FYI, @alexhancock  I'm working to integrate `<AppRenderer/>` from `@mcp-ui/client` over [here](https://github.com/block/goose/pull/7013/changes#diff-29ea4a5f599b916170bfa9de29bed5ee5048f973a360339e7175b9773cb87d68R434-R450). It has dedicated props for callbacks that map to a subset of  JSON RPC messages supported by the MCP Apps spec. Don't think `sampling/createMessage` is supported on that side of things yet. Maybe I'm wrong? 

@liady, how can we support `sampling/createMessage` while using `<AppRenderer />`?  

++ @idosal, @ochafik, @infoxicator for input as well

Code after:

  // Survive StrictMode remounts — replay cached results instead of re-fetching,
  // which prevents the iframe from being torn down and recreated (visible flicker).
  // Declared before useReducer so the lazy initializer can read them.
  const fetchedDataRef = useRef<{ html: string; meta: ResourceMeta } | null>(null);
  const sandboxUrlRef = useRef<{ url: string; csp: McpUiResourceCsp | null } | null>(null);

  const [state, dispatch] = useReducer(appReducer, undefined, (): AppState => {
    // On StrictMode remount, skip straight to ready if we have all cached data.
    if (fetchedDataRef.current && sandboxUrlRef.current) {
      return {
        status: 'ready',
        html: fetchedDataRef.current.html,
        meta: fetchedDataRef.current.meta,
        sandboxUrl: new URL(sandboxUrlRef.current.url),
        sandboxCsp: sandboxUrlRef.current.csp,
      };
    }
    if (cachedHtml) {
      return { status: 'loading_sandbox', html: cachedHtml, meta: DEFAULT_META };
    }
    return { status: 'idle' };
  });
  const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT);

  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState<number>(0);
  const [containerHeight, setContainerHeight] = useState<number>(0);
  const [apiHost, setApiHost] = useState<string | null>(null);
  const [secretKey, setSecretKey] = useState<string | null>(null);

  useEffect(() => {
    window.electron.getGoosedHostPort().then(setApiHost);
    window.electron.getSecretKey().then(setSecretKey);
  }, []);

  // Fetch the resource from the extension to get HTML and metadata (CSP, permissions, etc.).
  // If cachedHtml is provided we show it immediately; the fetch updates metadata and
  // replaces HTML only if the server returns different content.
  //
  // Retries with exponential backoff when the fetch fails (e.g. the extension hasn't
  // finished loading yet, causing a transient 500). Cached HTML skips retries since
  // the app can render immediately with the cached version.
  useEffect(() => {
    if (!sessionId) return;

    // On StrictMode remount, replay the cached result instead of re-fetching.
    if (fetchedDataRef.current) {
      const { html: cachedResult, meta: cachedMeta } = fetchedDataRef.current;
      dispatch({ type: 'RESOURCE_LOADED', html: cachedResult, meta: cachedMeta });
      return;