Complete "Persisting widget state" pattern documentation
Replaces the TODO stub with complete server-side and client-side examples based on the `pdf-server` implementation pattern: - Server: shows returning `widgetUUID` via `CallToolResult._meta` - Client: shows receiving UUID via `ontoolresult`, using it as localStorage key with generic `saveState<T>()` / `loadState<T>()` helpers Also clarifies the distinction between widget state persistence (UI state surviving page reloads) and `updateModelContext()` (model-visible state). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a0265f92d2
commit
05e07a76e7
2 changed files with 78 additions and 12 deletions
|
|
@ -54,13 +54,20 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged onhostco
|
|||
|
||||
{@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode}
|
||||
|
||||
## [TODO] Persist data (incl. widget state)
|
||||
## Persisting widget state
|
||||
|
||||
- Note: OAI's `window.openai.setWidgetState({modelContent, privateContent, imageIds})` has only a partial equivalent in the MCP Apps spec (for now!): `App.updateModelContext({content, structuredContent})`
|
||||
- For data persistence / to reload when conversation is reloaded, you must use localStorage / IndexedDb with `hostInfo.toolInfo.id` as key returned `CallToolResult._meta.widgetUUID = randomUUID()`
|
||||
To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server.
|
||||
|
||||
**Server-side**: Tool handler generates a unique `widgetUUID` and returns it in `CallToolResult._meta.widgetUUID`:
|
||||
|
||||
{@includeCode ./patterns.tsx#persistDataServer}
|
||||
|
||||
**Client-side**: Receive the UUID in {@link app!App.ontoolresult ontoolresult} and use it as the storage key:
|
||||
|
||||
{@includeCode ./patterns.tsx#persistData}
|
||||
|
||||
> **Note:** For model-visible state (informing the LLM about what the user is viewing), use {@link app!App.updateModelContext updateModelContext} instead. Widget state persistence is for UI state that should survive page reloads but doesn't need to be seen by the model.
|
||||
|
||||
## Lowering perceived latency
|
||||
|
||||
Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import {
|
|||
applyHostFonts,
|
||||
applyHostStyleVariables,
|
||||
} from "../src/styles.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { McpUiHostContext } from "../src/types.js";
|
||||
import { useApp, useHostStyles } from "../src/react/index.js";
|
||||
|
||||
|
|
@ -87,16 +89,72 @@ function hostStylingReact() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Example: Persist data (incl. widget state)
|
||||
* Example: Persisting widget state (server-side)
|
||||
*/
|
||||
function persistData(app: App) {
|
||||
function persistWidgetStateServer(
|
||||
url: string,
|
||||
title: string,
|
||||
pageCount: number,
|
||||
) {
|
||||
function toolCallback(): CallToolResult {
|
||||
//#region persistDataServer
|
||||
// In your tool callback, include widgetUUID in the result metadata.
|
||||
return {
|
||||
content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }],
|
||||
structuredContent: { url, title, pageCount, initialPage: 1 },
|
||||
_meta: {
|
||||
widgetUUID: randomUUID(),
|
||||
},
|
||||
};
|
||||
//#endregion persistDataServer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Persisting widget state (client-side)
|
||||
*/
|
||||
function persistWidgetState(app: App) {
|
||||
//#region persistData
|
||||
// Note: OAI's window.openai.setWidgetState({modelContent, privateContent, imageIds})
|
||||
// has only a partial equivalent in MCP Apps: App.updateModelContext({content, structuredContent})
|
||||
// For data persistence / to reload when conversation is reloaded,
|
||||
// use localStorage / IndexedDb with hostInfo.toolInfo.id as key
|
||||
// returned CallToolResult._meta.widgetUUID = randomUUID()
|
||||
// TODO: Complete implementation
|
||||
// Store the widgetUUID received from the server
|
||||
let widgetUUID: string | undefined;
|
||||
|
||||
// Helper to save state to localStorage
|
||||
function saveState<T>(state: T): void {
|
||||
if (!widgetUUID) return;
|
||||
try {
|
||||
localStorage.setItem(widgetUUID, JSON.stringify(state));
|
||||
} catch (err) {
|
||||
console.error("Failed to save widget state:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to load state from localStorage
|
||||
function loadState<T>(): T | null {
|
||||
if (!widgetUUID) return null;
|
||||
try {
|
||||
const saved = localStorage.getItem(widgetUUID);
|
||||
return saved ? (JSON.parse(saved) as T) : null;
|
||||
} catch (err) {
|
||||
console.error("Failed to load widget state:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Receive widgetUUID from the tool result
|
||||
app.ontoolresult = (result) => {
|
||||
widgetUUID = result._meta?.widgetUUID
|
||||
? String(result._meta.widgetUUID)
|
||||
: undefined;
|
||||
|
||||
// Restore any previously saved state
|
||||
const savedState = loadState<{ currentPage: number }>();
|
||||
if (savedState) {
|
||||
// Apply restored state to your UI...
|
||||
}
|
||||
};
|
||||
|
||||
// Call saveState() whenever your widget state changes
|
||||
// e.g., saveState({ currentPage: 5 });
|
||||
//#endregion persistData
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +181,7 @@ function migrateFromOpenai() {
|
|||
void authenticatedCalls;
|
||||
void hostStylingVanillaJs;
|
||||
void hostStylingReact;
|
||||
void persistData;
|
||||
void persistWidgetStateServer;
|
||||
void persistWidgetState;
|
||||
void iframeAndMcpApps;
|
||||
void migrateFromOpenai;
|
||||
|
|
|
|||
Loading…
Reference in a new issue