mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ui): preserve MCP App previews across reloads (#105728)
* fix(mcp): harden app preview lifecycle * fix(ui): defer MCP app initialization payload * test(mcp): align current policy expectations * fix(ui): preserve MCP app previews * fix(mcp): reject disabled app restoration * fix(mcp): preserve reconstructed app state * fix(mcp): bind reconstructable app data * fix(ui): deduplicate MCP app preview copies * style(ui): format MCP app preview test * fix(mcp): satisfy metadata access lint * style(mcp): format metadata access
This commit is contained in:
@@ -14,10 +14,27 @@ describe("extractCanvasFromText", () => {
|
||||
kind: "canvas",
|
||||
view: { id: "cv_app" },
|
||||
presentation: { target: "assistant_message", sandbox: "scripts" },
|
||||
mcpApp: { viewId: "cv_app" },
|
||||
mcpApp: {
|
||||
viewId: "cv_app",
|
||||
serverName: "demo",
|
||||
toolName: "show",
|
||||
uiResourceUri: "ui://demo/app",
|
||||
toolCallId: "call-1",
|
||||
resultMetaState: "unavailable",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ viewId: "cv_app", mcpApp: { viewId: "cv_app" } });
|
||||
).toMatchObject({
|
||||
viewId: "cv_app",
|
||||
mcpApp: {
|
||||
viewId: "cv_app",
|
||||
serverName: "demo",
|
||||
toolName: "show",
|
||||
uiResourceUri: "ui://demo/app",
|
||||
toolCallId: "call-1",
|
||||
resultMetaState: "unavailable",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps MCP App previews opaque while preserving model-visible results", () => {
|
||||
|
||||
@@ -9,6 +9,15 @@ import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js";
|
||||
type CanvasSurface = "assistant_message";
|
||||
type CanvasSandbox = "strict" | "scripts";
|
||||
|
||||
export type McpAppPreviewDescriptor = {
|
||||
viewId: string;
|
||||
serverName?: string;
|
||||
toolName?: string;
|
||||
uiResourceUri?: string;
|
||||
toolCallId?: string;
|
||||
resultMetaState?: "unavailable";
|
||||
};
|
||||
|
||||
type CanvasPreview = {
|
||||
kind: "canvas";
|
||||
surface: CanvasSurface;
|
||||
@@ -20,7 +29,7 @@ type CanvasPreview = {
|
||||
className?: string;
|
||||
style?: string;
|
||||
sandbox?: CanvasSandbox;
|
||||
mcpApp?: { viewId: string };
|
||||
mcpApp?: McpAppPreviewDescriptor;
|
||||
};
|
||||
|
||||
function getRecordStringField(
|
||||
@@ -47,6 +56,40 @@ function getNestedRecord(
|
||||
return asOptionalRecord(value);
|
||||
}
|
||||
|
||||
function coerceMcpAppDescriptor(
|
||||
record: Record<string, unknown> | undefined,
|
||||
): McpAppPreviewDescriptor | undefined {
|
||||
const viewId = getRecordStringField(record, "viewId");
|
||||
if (!viewId || viewId.length > 128) {
|
||||
return undefined;
|
||||
}
|
||||
const serverName = getRecordStringField(record, "serverName");
|
||||
const toolName = getRecordStringField(record, "toolName");
|
||||
const uiResourceUri = getRecordStringField(record, "uiResourceUri");
|
||||
const toolCallId = getRecordStringField(record, "toolCallId");
|
||||
const resultMetaState = record?.resultMetaState === "unavailable" ? "unavailable" : undefined;
|
||||
const hasCompleteDescriptor = Boolean(
|
||||
serverName &&
|
||||
serverName.length <= 256 &&
|
||||
toolName &&
|
||||
toolName.length <= 256 &&
|
||||
uiResourceUri?.startsWith("ui://") &&
|
||||
uiResourceUri.length <= 2048 &&
|
||||
toolCallId &&
|
||||
toolCallId.length <= 512,
|
||||
);
|
||||
return hasCompleteDescriptor
|
||||
? {
|
||||
viewId,
|
||||
serverName,
|
||||
toolName,
|
||||
uiResourceUri,
|
||||
toolCallId,
|
||||
...(resultMetaState ? { resultMetaState } : {}),
|
||||
}
|
||||
: { viewId };
|
||||
}
|
||||
|
||||
function normalizeSurface(value: string | undefined): CanvasSurface | undefined {
|
||||
return value === "assistant_message" ? value : undefined;
|
||||
}
|
||||
@@ -75,7 +118,8 @@ function coerceCanvasPreview(
|
||||
const view = getNestedRecord(record, "view");
|
||||
const source = getNestedRecord(record, "source");
|
||||
const mcpAppRecord = getNestedRecord(record, "mcpApp");
|
||||
const mcpAppViewId = getRecordStringField(mcpAppRecord, "viewId");
|
||||
const mcpApp = coerceMcpAppDescriptor(mcpAppRecord);
|
||||
const mcpAppViewId = mcpApp?.viewId;
|
||||
const requestedSurface =
|
||||
getRecordStringField(presentation, "target") ?? getRecordStringField(record, "target");
|
||||
const surface = requestedSurface ? normalizeSurface(requestedSurface) : "assistant_message";
|
||||
@@ -105,7 +149,7 @@ function coerceCanvasPreview(
|
||||
...(title ? { title } : {}),
|
||||
...(preferredHeight ? { preferredHeight } : {}),
|
||||
...(sandbox ? { sandbox } : {}),
|
||||
mcpApp: { viewId: mcpAppViewId },
|
||||
mcpApp,
|
||||
};
|
||||
}
|
||||
if (viewUrl) {
|
||||
@@ -120,7 +164,7 @@ function coerceCanvasPreview(
|
||||
...(className ? { className } : {}),
|
||||
...(style ? { style } : {}),
|
||||
...(sandbox ? { sandbox } : {}),
|
||||
...(mcpAppViewId ? { mcpApp: { viewId: mcpAppViewId } } : {}),
|
||||
...(mcpApp ? { mcpApp } : {}),
|
||||
};
|
||||
}
|
||||
const sourceType = getRecordStringField(source, "type")?.trim().toLowerCase();
|
||||
@@ -139,7 +183,7 @@ function coerceCanvasPreview(
|
||||
...(className ? { className } : {}),
|
||||
...(style ? { style } : {}),
|
||||
...(sandbox ? { sandbox } : {}),
|
||||
...(mcpAppViewId ? { mcpApp: { viewId: mcpAppViewId } } : {}),
|
||||
...(mcpApp ? { mcpApp } : {}),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user