Skip to content

Handle images from updateModelContext and setWidgetState#1531

Open
mikechao wants to merge 4 commits intoMCPJam:mainfrom
mikechao:handle-image
Open

Handle images from updateModelContext and setWidgetState#1531
mikechao wants to merge 4 commits intoMCPJam:mainfrom
mikechao:handle-image

Conversation

@mikechao
Copy link
Contributor

@mikechao mikechao commented Mar 4, 2026

This PR addresses handling of images from widgets that call updateModelContext and setWidgetState.

The issue

Current when a widget uses updateModelContext or setWidgetState with an image the image is treated as text.

Example of a call using updateModelContext

const contentWithImage: ContentBlock[] = [
        ...textOnlyContent,
        {
          type: 'image',
          data: imageData,
          mimeType,
        },
      ];

 await app.updateModelContext({ content: contentWithImage });

The payload it generates

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "ui/update-model-context",
  "params": {
    "content": [
      {
        "type": "text",
        "text": "User uploaded an image from the file upload widget."
      },
      {
        "type": "image",
        "data": "/9j/4AAQSkZJRgABAQAAAQABAAD/..truncated base64 string of image”,
        "mimeType": "image/jpeg"
      }
    ]
  }
}

Example of a setWidgetState call

      const uploadResult = await window.openai.uploadFile(file);
      const uploadedFileId = uploadResult.fileId;
      window.openai.setWidgetState({
          modelContent: resolvedAdditionalText,
          privateContent: {
            rawFileId: uploadedFileId,
          },
          imageIds: [uploadedFileId],
        });

The payload it generates:

{
  "type": "openai:setWidgetState",
  "toolId": "playground-7kZXNqf6UYa5QMDb",
  "state": {
    "modelContent": "User uploaded an image from the file upload widget.",
    "privateContent": {
      "rawFileId": "file_8a45d63b-79d4-4eff-a178-c0ce4078c7c6"
    },
    "imageIds": [
      "file_8a45d63b-79d4-4eff-a178-c0ce4078c7c6"
    ]
  }
}

The fix

Two new modules were introduced to handle image content properly in both paths.

openai-widget-state-messages.ts

  • buildWidgetStateParts now returns a mixed parts array: a text summary followed by one { type: "file" } part per image, rather than a single flat text string.
  • extractUploadedFileIds reads imageIds from widget state (the canonical Apps SDK field) and resolveFilePart fetches each ID from the local file endpoint, converting the blob to a base64 data URL so it can be attached as a file part.
  • buildWidgetStateText strips privateContent before serialising state to text, so UI-only data is never sent to the model.

model-context-messages.ts

  • contextToSyncParts iterates content[] and maps each image (or audio) ContentBlock to a { type: "file", mediaType, url } part instead of ignoring or stringifying it.
  • contextToParts handles the ChatGPT extension path: it resolves imageIds via resolveFilePart, and falls back to HTTP image URLs from structuredContent.privateContent.selectedImages only when no file IDs could be resolved (to avoid duplicates).
  • Messages whose resolved parts are empty (e.g. structuredContent containing only privateContent) are filtered out before being sent to the model.

Testing

I tested the fix by using my upload-mcp which just sending the image and a small bit of text to the model. A version of it is currently deploy at https://upload-mcp-def1fcb1.alpic.live/ if you need to test it out.

Tested with various models

Model Name Notes
Grok 4.1 Fast Works
GPT 5 Mini Works
Gemini 3 Flash Preview Returns empty message
Gemini 2.5 Flash Returns empty message
DeepSeek V3.2 No endpoint support image
Claude Haiku 4.5 Works
Kimi K2.5 Works

@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 4, 2026
@chelojimenez
Copy link
Contributor

chelojimenez commented Mar 4, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@dosubot dosubot bot added the bug Something isn't working label Mar 4, 2026
@mikechao
Copy link
Contributor Author

mikechao commented Mar 4, 2026

An example of chat before the fix

An example of chat after the fix


@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2f0b8ea-d6b2-48bc-8c87-89900bae5972

📥 Commits

Reviewing files that changed from the base of the PR and between c6d4b7b and 8d9d858.

📒 Files selected for processing (1)
  • mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts

Walkthrough

Adds a new useWidgetStateSync hook (exporting ModelContextItem and sync APIs) that serializes async widget-state updates, provides epoch-based reset/cancel, and exposes refs for in-flight work and model-context queueing. Introduces mcp-ui modules to build widget state parts and model-context messages (including file resolution and data-URL conversion). Refactors ChatTabV2 and PlaygroundMain to await widget-state conversion, enqueue updates via the hook, include built model-context messages and FileUIParts on submit, and surface submit errors with toast. Adds comprehensive tests and test mocks for the new flows.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts (1)

193-196: Resolve uploaded file IDs concurrently to reduce widget-sync latency.

Serial await in this loop scales linearly with the number of images and slows submit paths unnecessarily.

⚡ Suggested refactor
-  for (const fileId of extractUploadedFileIds(state)) {
-    const filePart = await resolveFilePart(fileId).catch(() => null);
-    if (filePart) parts.push(filePart);
-  }
+  const resolvedFileParts = await Promise.all(
+    extractUploadedFileIds(state).map((fileId) =>
+      resolveFilePart(fileId).catch(() => null),
+    ),
+  );
+  for (const filePart of resolvedFileParts) {
+    if (filePart) parts.push(filePart);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`
around lines 193 - 196, The loop that resolves uploaded file parts does serial
awaits and should be converted to concurrent resolution: map
extractUploadedFileIds(state) to an array of promises calling
resolveFilePart(fileId) with a catch that returns null, await Promise.all on
that array, then iterate the resulting array and push only non-null results into
parts; update the code around extractUploadedFileIds, resolveFilePart and parts
to use this concurrent pattern so widget-sync latency no longer scales linearly
with number of images.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts`:
- Around line 1-11: Remove the ad-hoc hoisted mock (buildWidgetStatePartsMock)
and the vi.mock call; instead import the project's shared client test presets
(mcpApiPresets and storePresets) from client/src/test/mocks and register/apply
them in the test setup (e.g., in beforeEach) using the repository's test mock
helper so the standard presets provide the buildWidgetStateParts mock and other
client mocks rather than the local vi.hoisted mock and vi.mock.

In `@mcpjam-inspector/client/src/hooks/use-widget-state-sync.ts`:
- Around line 160-165: The current useEffect clears widgetStateQueue
optimistically before async work finishes; update the effect so it calls
enqueueWidgetStateSync(widgetStateQueue) and only calls setWidgetStateQueue([])
after the returned promise resolves successfully, and on rejection preserve the
existing queue (or requeue/fall back and log) so failed syncs are not dropped;
adjust the effect that references status, widgetStateQueue,
enqueueWidgetStateSync, and setWidgetStateQueue to await the promise and handle
success vs error paths instead of clearing immediately.

In
`@mcpjam-inspector/client/src/lib/mcp-ui/__tests__/model-context-messages.test.ts`:
- Around line 1-9: Replace the ad-hoc local mocking in the test with the
repo-wide client mock presets: import and apply the shared mcpApiPresets and
storePresets (from client/src/test/mocks/) in the test's beforeEach setup
instead of the current manual vi.restoreAllMocks/spyOn block; then remove the
local-only mocking of resolveFilePart unless the presets do not already cover
it—if they don't, add a short supplemental mock for
widgetStateMessages.resolveFilePart.mockResolvedValue(null) alongside applying
mcpApiPresets and storePresets.

In
`@mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.hosted.test.ts`:
- Around line 19-24: Replace the ad-hoc vi.doMock calls that stub
"@/lib/session-token" and "@/lib/config" (the vi.doMock(...) lines) with the
shared test mock presets: import and apply mcpApiPresets and storePresets from
the test mocks and configure them to return authFetchMock and HOSTED_MODE: true
instead of directly mocking those modules; update the test setup to call the
preset helpers (mcpApiPresets, storePresets) so hosted-mode behavior and
authFetch are wired via the central client mocks rather than the inline
vi.doMock calls.

In
`@mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts`:
- Around line 1-13: This test defines bespoke mocks (authFetchMock and a local
HOSTED_MODE mock) instead of using the shared client mock presets; replace the
bespoke mocks by importing and applying the shared presets (mcpApiPresets and
storePresets) from the client test mocks and remove the local vi.hoisted
authFetchMock and the manual vi.mock("@/lib/config") call. Specifically, import
mcpApiPresets and storePresets at the top of
openai-widget-state-messages.test.ts and invoke/apply them (the shared presets
export will set up authFetch and config mocks for you) so the suite uses the
canonical client mocks rather than custom fixtures.

In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`:
- Around line 168-172: The current branch returns literal "undefined"/"null"
when modelContent exists but is unset; update the check in the isRecord(state)
&& "modelContent" in state branch to first guard against modelContent being null
or undefined (e.g., modelContent == null) before treating it as a string—only
return modelContent when typeof modelContent === "string" AND modelContent is
not null/undefined (and not the literal "undefined"/"null" if you prefer
stricter guarding); otherwise fall back to the sanitized model-visible state
path (use safeStringify or the existing fallback logic) so isRecord, state,
modelContent, and safeStringify are used to locate and fix this behavior.

---

Nitpick comments:
In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`:
- Around line 193-196: The loop that resolves uploaded file parts does serial
awaits and should be converted to concurrent resolution: map
extractUploadedFileIds(state) to an array of promises calling
resolveFilePart(fileId) with a catch that returns null, await Promise.all on
that array, then iterate the resulting array and push only non-null results into
parts; update the code around extractUploadedFileIds, resolveFilePart and parts
to use this concurrent pattern so widget-sync latency no longer scales linearly
with number of images.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f41c78c4-cb76-4c21-98db-a386ce8ae894

📥 Commits

Reviewing files that changed from the base of the PR and between 8aad4b9 and b6b9926.

📒 Files selected for processing (9)
  • mcpjam-inspector/client/src/components/ChatTabV2.tsx
  • mcpjam-inspector/client/src/components/ui-playground/PlaygroundMain.tsx
  • mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts
  • mcpjam-inspector/client/src/hooks/use-widget-state-sync.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/model-context-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.hosted.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/model-context-messages.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts

…sues

- switch widget-state-related tests to shared preset-driven client mocks
- clear widgetStateQueue only after successful async sync
- keep queued updates on sync failure
- guard nullish/invalid modelContent fallback behavior
- resolve uploaded file parts concurrently
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts (1)

136-163: Add a queue-flush failure test to lock retry semantics.

This suite validates successful flushes, but not the rejection path where queued items must remain for retry. A targeted failure-case test would protect this reliability contract.

Based on learnings, all changes should include tests using Vitest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts`
around lines 136 - 163, Add a new vitest test in use-widget-state-sync.test.ts
that simulates a failed flush when status transitions to "ready": mock or stub
the downstream send/enqueue used inside useWidgetStateSync (e.g., the function
that ultimately calls setMessages or the internal enqueue routine referenced by
widgetStateSyncRef) to reject, then transition from "streaming" to "ready" via
rerender and await the widgetStateSyncRef promise; assert that the queued item
added via setWidgetStateQueue remains in the queue (no messages pushed) and that
subsequent successful send will clear it, thus locking the retry semantics;
reference the hook useWidgetStateSync, methods setWidgetStateQueue and
widgetStateSyncRef, and the external setter setMessages when locating where to
inject the mock.
mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts (1)

194-203: Consider relocating resolveFilePart test to its own describe block.

This test exercises resolveFilePart directly, yet resides within the describe("buildWidgetStateParts") block. A dedicated sibling describe("resolveFilePart") would clarify the test organization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts`
around lines 194 - 203, The test exercising resolveFilePart is currently nested
inside the describe("buildWidgetStateParts") block; move that it("returns null
when all endpoint requests throw") into a new sibling
describe("resolveFilePart") block to improve organization and clarity, keeping
the same test body and mocks (e.g.,
clientRuntimeMocks.authFetchMock.mockRejectedValue and the resolveFilePart call)
and ensure any shared setup/teardown remains available or is duplicated/adjusted
within the new describe.
mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts (1)

198-202: Redundant but harmless defensive catch.

resolveFilePart already returns null on failure internally, making the outer .catch(() => null) technically redundant. However, this defensive layer is benign and guards against unforeseen changes to resolveFilePart's error handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`
around lines 198 - 202, Redundant defensive .catch: remove the outer .catch(()
=> null) from the Promise.map so the code becomes
Promise.all(extractUploadedFileIds(state).map((fileId) =>
resolveFilePart(fileId))) — this keeps behavior concise because resolveFilePart
already returns null on failure; reference resolveFilePart,
extractUploadedFileIds and the fileParts const and ensure tests still pass or
add a small unit test verifying fileParts contains nulls on failed resolutions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcpjam-inspector/client/src/test/mocks/client-runtime.ts`:
- Line 36: The test mock merge currently uses
Object.assign(clientRuntimeMocks.mcpApiMock, mcpApi) which mutates a long-lived
clientRuntimeMocks.mcpApiMock and allows keys from prior presets to leak; before
merging, reset or replace clientRuntimeMocks.mcpApiMock (e.g., set it to a fresh
empty object) and then merge the new mcpApi into that fresh object so only keys
present in the current preset remain (locate the call to Object.assign and
update it to reset clientRuntimeMocks.mcpApiMock first).

---

Nitpick comments:
In `@mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts`:
- Around line 136-163: Add a new vitest test in use-widget-state-sync.test.ts
that simulates a failed flush when status transitions to "ready": mock or stub
the downstream send/enqueue used inside useWidgetStateSync (e.g., the function
that ultimately calls setMessages or the internal enqueue routine referenced by
widgetStateSyncRef) to reject, then transition from "streaming" to "ready" via
rerender and await the widgetStateSyncRef promise; assert that the queued item
added via setWidgetStateQueue remains in the queue (no messages pushed) and that
subsequent successful send will clear it, thus locking the retry semantics;
reference the hook useWidgetStateSync, methods setWidgetStateQueue and
widgetStateSyncRef, and the external setter setMessages when locating where to
inject the mock.

In
`@mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts`:
- Around line 194-203: The test exercising resolveFilePart is currently nested
inside the describe("buildWidgetStateParts") block; move that it("returns null
when all endpoint requests throw") into a new sibling
describe("resolveFilePart") block to improve organization and clarity, keeping
the same test body and mocks (e.g.,
clientRuntimeMocks.authFetchMock.mockRejectedValue and the resolveFilePart call)
and ensure any shared setup/teardown remains available or is duplicated/adjusted
within the new describe.

In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`:
- Around line 198-202: Redundant defensive .catch: remove the outer .catch(() =>
null) from the Promise.map so the code becomes
Promise.all(extractUploadedFileIds(state).map((fileId) =>
resolveFilePart(fileId))) — this keeps behavior concise because resolveFilePart
already returns null on failure; reference resolveFilePart,
extractUploadedFileIds and the fileParts const and ensure tests still pass or
add a small unit test verifying fileParts contains nulls on failed resolutions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1f8c268-ef95-48eb-bdee-95d21ee06d59

📥 Commits

Reviewing files that changed from the base of the PR and between b6b9926 and 988130b.

📒 Files selected for processing (8)
  • mcpjam-inspector/client/src/hooks/__tests__/use-widget-state-sync.test.ts
  • mcpjam-inspector/client/src/hooks/use-widget-state-sync.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/model-context-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.hosted.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts
  • mcpjam-inspector/client/src/test/mocks/client-runtime.ts
  • mcpjam-inspector/client/src/test/mocks/widget-state-sync.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcpjam-inspector/client/src/lib/mcp-ui/tests/model-context-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/tests/openai-widget-state-messages.hosted.test.ts

… state parts

- reset client runtime mcpApi mock before applying each preset to prevent key leakage
- remove redundant catch in widget state file-part Promise.all path
- reorganize openai-widget-state-messages tests by moving resolveFilePart failure case into a dedicated describe block
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts (1)

198-200: Avoid unbounded parallel blob→dataURL conversions.

Lines 198-200 resolve every file concurrently; with many/large images this can cause avoidable memory pressure in the browser.

Proposed refactor (sequential, memory-friendlier)
-  const fileParts = await Promise.all(
-    extractUploadedFileIds(state).map((fileId) => resolveFilePart(fileId)),
-  );
-
-  for (const filePart of fileParts) {
-    if (filePart) parts.push(filePart);
-  }
+  for (const fileId of extractUploadedFileIds(state)) {
+    const filePart = await resolveFilePart(fileId);
+    if (filePart) parts.push(filePart);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`
around lines 198 - 200, The current code calls resolveFilePart for every ID
concurrently via Promise.all over extractUploadedFileIds(state), causing
unbounded memory use for many/large files; change this to a sequential (or
bounded-concurrency) approach by iterating the array from
extractUploadedFileIds(state) and awaiting resolveFilePart(fileId) for each
item, pushing results into the fileParts array (instead of using Promise.all).
Update the block that defines fileParts so it builds the array incrementally (or
uses a simple concurrency limiter) to avoid resolving all blobs at once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`:
- Around line 17-19: The isValidUploadedFileId type guard currently enforces the
wrong pattern; update the validator used in isValidUploadedFileId to accept
OpenAI-style file IDs (prefix "file-" plus opaque alphanumeric/URL-safe chars)
instead of "file_+hex". Replace the regex check in isValidUploadedFileId with
one that matches /^file-[A-Za-z0-9_-]+$/ (or a permissive /^file-[\w-]+$/) so
legitimate OpenAI file IDs are accepted while keeping the typeof value ===
"string" guard.

---

Nitpick comments:
In `@mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts`:
- Around line 198-200: The current code calls resolveFilePart for every ID
concurrently via Promise.all over extractUploadedFileIds(state), causing
unbounded memory use for many/large files; change this to a sequential (or
bounded-concurrency) approach by iterating the array from
extractUploadedFileIds(state) and awaiting resolveFilePart(fileId) for each
item, pushing results into the fileParts array (instead of using Promise.all).
Update the block that defines fileParts so it builds the array incrementally (or
uses a simple concurrency limiter) to avoid resolving all blobs at once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89deaa39-9593-47cf-88c8-a3af2330e46a

📥 Commits

Reviewing files that changed from the base of the PR and between 988130b and c6d4b7b.

📒 Files selected for processing (3)
  • mcpjam-inspector/client/src/lib/mcp-ui/__tests__/openai-widget-state-messages.test.ts
  • mcpjam-inspector/client/src/lib/mcp-ui/openai-widget-state-messages.ts
  • mcpjam-inspector/client/src/test/mocks/client-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcpjam-inspector/client/src/lib/mcp-ui/tests/openai-widget-state-messages.test.ts
  • mcpjam-inspector/client/src/test/mocks/client-runtime.ts

…part resolution

- accept non-empty string file IDs in isValidUploadedFileId to support current OpenAI file ID formats
- replace Promise.all file-part resolution with sequential resolveFilePart iteration to avoid unbounded concurrent blob loading
- keep existing widget-state message behavior and pass targeted mcp-ui/widget-sync test suites
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants