@agent-native/core 0.78.8 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/docs/content/actions.md +4 -2
- package/corpus/core/docs/content/agent-surfaces.md +9 -0
- package/corpus/core/docs/content/extensions.md +10 -0
- package/corpus/core/docs/content/generative-ui.md +217 -0
- package/corpus/core/docs/content/key-concepts.md +2 -2
- package/corpus/core/docs/content/mcp-apps.md +3 -5
- package/corpus/core/docs/content/native-chat-ui.md +9 -5
- package/corpus/core/docs/content/tracking.md +9 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action-ui.ts +1 -0
- package/corpus/core/src/client/analytics.ts +195 -12
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/tool-call-display.tsx +5 -2
- package/corpus/core/src/client/chat/widgets/InlineExtensionWidget.tsx +139 -0
- package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +27 -1
- package/corpus/core/src/client/conversation/AgentConversation.tsx +38 -0
- package/corpus/core/src/client/conversation/code-agent-transcript.ts +9 -0
- package/corpus/core/src/client/conversation/types.ts +4 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +27 -5
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +26 -5
- package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +654 -0
- package/corpus/core/src/client/extensions/iframe-bridge.ts +30 -0
- package/corpus/core/src/client/extensions/index.ts +5 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/session-replay.ts +89 -14
- package/corpus/core/src/code-agents/transcript-normalizer.ts +14 -0
- package/corpus/core/src/extensions/actions.ts +218 -1
- package/corpus/core/src/extensions/html-shell.ts +70 -3
- package/corpus/core/src/extensions/routes.ts +12 -4
- package/corpus/core/src/index.browser.ts +1 -0
- package/corpus/core/src/index.ts +1 -0
- package/corpus/core/src/mcp/build-server.ts +1 -1
- package/corpus/core/src/mcp/embed-app.ts +11 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +21 -9
- package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +198 -59
- package/corpus/templates/analytics/AGENTS.md +12 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1105 -0
- package/corpus/templates/analytics/actions/dashboard-panel-order.ts +169 -0
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +89 -6
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +333 -0
- package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +214 -0
- package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +48 -21
- package/corpus/templates/analytics/actions/update-dashboard.ts +193 -100
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/global.css +81 -10
- package/corpus/templates/analytics/app/i18n-data.ts +249 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +41 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +48 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +204 -70
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +1101 -227
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +160 -133
- package/corpus/templates/analytics/changelog/2026-06-26-agents-can-now-edit-dashboards-through-a-typed-mutation-scri.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-bar-chart-hovers-stay-subtle-in-dark-mode.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-chart-moves-are-now-handled-by-panel-id-making-sim.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-drag-smoothly-with-stable-cards-inactive-ch.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-show-a-refresh-spinner-while-keeping-existi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-edits-stay-stable-while-chart-order-and-deletions-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-email-reports-queue-immediate-sends.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-explorer-dashboards-now-drag-charts-with-a-stable-preview-an.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-playback-can-now-be-toggled-by-clicking-the-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-session-replays-now-open-from-the-full-row-and-play-with-tim.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +41 -17
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
- package/dist/action-ui.d.ts +1 -0
- package/dist/action-ui.d.ts.map +1 -1
- package/dist/action-ui.js +1 -0
- package/dist/action-ui.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +173 -12
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +3 -1
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat/widgets/InlineExtensionWidget.d.ts +17 -0
- package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -0
- package/dist/client/chat/widgets/InlineExtensionWidget.js +85 -0
- package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.js +15 -2
- package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
- package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
- package/dist/client/conversation/AgentConversation.js +33 -0
- package/dist/client/conversation/AgentConversation.js.map +1 -1
- package/dist/client/conversation/code-agent-transcript.js +8 -0
- package/dist/client/conversation/code-agent-transcript.js.map +1 -1
- package/dist/client/conversation/types.d.ts +4 -0
- package/dist/client/conversation/types.d.ts.map +1 -1
- package/dist/client/conversation/types.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +29 -1
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +28 -1
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/InlineExtensionFrame.d.ts +25 -0
- package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -0
- package/dist/client/extensions/InlineExtensionFrame.js +488 -0
- package/dist/client/extensions/InlineExtensionFrame.js.map +1 -0
- package/dist/client/extensions/iframe-bridge.d.ts +2 -0
- package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
- package/dist/client/extensions/iframe-bridge.js +23 -0
- package/dist/client/extensions/iframe-bridge.js.map +1 -1
- package/dist/client/extensions/index.d.ts +1 -0
- package/dist/client/extensions/index.d.ts.map +1 -1
- package/dist/client/extensions/index.js +1 -0
- package/dist/client/extensions/index.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +11 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +74 -14
- package/dist/client/session-replay.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.d.ts +2 -0
- package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +8 -0
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +200 -1
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +70 -3
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/routes.d.ts.map +1 -1
- package/dist/extensions/routes.js +11 -4
- package/dist/extensions/routes.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +11 -1
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +21 -9
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
- package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
- package/docs/content/actions.md +4 -2
- package/docs/content/agent-surfaces.md +9 -0
- package/docs/content/extensions.md +10 -0
- package/docs/content/generative-ui.md +217 -0
- package/docs/content/key-concepts.md +2 -2
- package/docs/content/mcp-apps.md +3 -5
- package/docs/content/native-chat-ui.md +9 -5
- package/docs/content/tracking.md +9 -3
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
- package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { buildExtensionHtml } from "../../extensions/html-shell.js";
|
|
4
|
+
import { getThemeVars } from "../../extensions/theme.js";
|
|
5
|
+
import { sendToAgentChat } from "../agent-chat.js";
|
|
6
|
+
import { agentNativePath } from "../api-path.js";
|
|
7
|
+
import {
|
|
8
|
+
isAllowedExtensionPath,
|
|
9
|
+
sanitizeExtensionRequestOptions,
|
|
10
|
+
checkBridgePolicy,
|
|
11
|
+
type BridgePolicyContext,
|
|
12
|
+
type ExtensionBridgeRole,
|
|
13
|
+
} from "./iframe-bridge.js";
|
|
14
|
+
|
|
15
|
+
const THEME_CSS_VARS = [
|
|
16
|
+
"--background",
|
|
17
|
+
"--foreground",
|
|
18
|
+
"--card",
|
|
19
|
+
"--card-foreground",
|
|
20
|
+
"--popover",
|
|
21
|
+
"--popover-foreground",
|
|
22
|
+
"--primary",
|
|
23
|
+
"--primary-foreground",
|
|
24
|
+
"--secondary",
|
|
25
|
+
"--secondary-foreground",
|
|
26
|
+
"--muted",
|
|
27
|
+
"--muted-foreground",
|
|
28
|
+
"--accent",
|
|
29
|
+
"--accent-foreground",
|
|
30
|
+
"--destructive",
|
|
31
|
+
"--destructive-foreground",
|
|
32
|
+
"--border",
|
|
33
|
+
"--input",
|
|
34
|
+
"--ring",
|
|
35
|
+
"--radius",
|
|
36
|
+
"--sidebar-background",
|
|
37
|
+
"--sidebar-foreground",
|
|
38
|
+
"--sidebar-primary",
|
|
39
|
+
"--sidebar-primary-foreground",
|
|
40
|
+
"--sidebar-accent",
|
|
41
|
+
"--sidebar-accent-foreground",
|
|
42
|
+
"--sidebar-border",
|
|
43
|
+
"--sidebar-ring",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
interface InlineExtensionSource {
|
|
47
|
+
mode?: "database" | "local-files";
|
|
48
|
+
permissions?: BridgePolicyContext["permissions"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface InlineExtensionDefinition {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
content?: string;
|
|
56
|
+
updatedAt?: string;
|
|
57
|
+
mode?: "transient" | "persisted";
|
|
58
|
+
source?: InlineExtensionSource;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface InlineExtensionFrameProps {
|
|
62
|
+
extensionId?: string;
|
|
63
|
+
extension?: InlineExtensionDefinition;
|
|
64
|
+
slotId?: string;
|
|
65
|
+
context?: Record<string, unknown> | null;
|
|
66
|
+
className?: string;
|
|
67
|
+
initialHeight?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getParentThemeVars(): Record<string, string> {
|
|
71
|
+
if (typeof document === "undefined") return {};
|
|
72
|
+
const computed = getComputedStyle(document.documentElement);
|
|
73
|
+
const vars: Record<string, string> = {};
|
|
74
|
+
for (const name of THEME_CSS_VARS) {
|
|
75
|
+
const val = computed.getPropertyValue(name).trim();
|
|
76
|
+
if (val) vars[name] = val;
|
|
77
|
+
}
|
|
78
|
+
return vars;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeRole(value: unknown): ExtensionBridgeRole {
|
|
82
|
+
return value === "owner" ||
|
|
83
|
+
value === "admin" ||
|
|
84
|
+
value === "editor" ||
|
|
85
|
+
value === "viewer"
|
|
86
|
+
? value
|
|
87
|
+
: "viewer";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function serializeChatValue(value: unknown): string | undefined {
|
|
91
|
+
if (value === undefined || value === null) return undefined;
|
|
92
|
+
if (typeof value === "string") return value;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.stringify(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function transientDataKey(
|
|
101
|
+
extensionId: string,
|
|
102
|
+
collection: string,
|
|
103
|
+
scope: string,
|
|
104
|
+
): string {
|
|
105
|
+
return `agent-native:inline-extension-data:${extensionId}:${scope}:${collection}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface TransientDataRow {
|
|
109
|
+
id: string;
|
|
110
|
+
extensionId: string;
|
|
111
|
+
toolId: string;
|
|
112
|
+
collection: string;
|
|
113
|
+
data: string;
|
|
114
|
+
ownerEmail: string;
|
|
115
|
+
scope: string;
|
|
116
|
+
orgId: string | null;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
updatedAt: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readTransientRows(
|
|
122
|
+
extensionId: string,
|
|
123
|
+
collection: string,
|
|
124
|
+
scope: string,
|
|
125
|
+
): TransientDataRow[] {
|
|
126
|
+
if (typeof localStorage === "undefined") return [];
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(
|
|
129
|
+
localStorage.getItem(transientDataKey(extensionId, collection, scope)) ??
|
|
130
|
+
"[]",
|
|
131
|
+
);
|
|
132
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeTransientRows(
|
|
139
|
+
extensionId: string,
|
|
140
|
+
collection: string,
|
|
141
|
+
scope: string,
|
|
142
|
+
rows: TransientDataRow[],
|
|
143
|
+
): void {
|
|
144
|
+
if (typeof localStorage === "undefined") return;
|
|
145
|
+
localStorage.setItem(
|
|
146
|
+
transientDataKey(extensionId, collection, scope),
|
|
147
|
+
JSON.stringify(rows),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseTransientDataPath(
|
|
152
|
+
extensionId: string,
|
|
153
|
+
path: string,
|
|
154
|
+
): { collection: string; itemId?: string; scope: string } | null {
|
|
155
|
+
try {
|
|
156
|
+
const url = new URL(path, "http://agent-native.local");
|
|
157
|
+
const parts = url.pathname.split("/");
|
|
158
|
+
if (
|
|
159
|
+
parts.length < 6 ||
|
|
160
|
+
parts.length > 7 ||
|
|
161
|
+
parts[1] !== "_agent-native" ||
|
|
162
|
+
parts[2] !== "extensions" ||
|
|
163
|
+
parts[3] !== "data" ||
|
|
164
|
+
decodeURIComponent(parts[4]) !== extensionId
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
collection: decodeURIComponent(parts[5] ?? ""),
|
|
170
|
+
itemId: parts[6] ? decodeURIComponent(parts[6]) : undefined,
|
|
171
|
+
scope: url.searchParams.get("scope") || "user",
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function handleTransientExtensionData(
|
|
179
|
+
extensionId: string,
|
|
180
|
+
path: string,
|
|
181
|
+
options: RequestInit,
|
|
182
|
+
): Promise<Record<string, unknown> | null> {
|
|
183
|
+
const parsed = parseTransientDataPath(extensionId, path);
|
|
184
|
+
if (!parsed?.collection) return null;
|
|
185
|
+
|
|
186
|
+
const method = String(options.method ?? "GET").toUpperCase();
|
|
187
|
+
if (method === "GET" || method === "HEAD") {
|
|
188
|
+
const rows = readTransientRows(
|
|
189
|
+
extensionId,
|
|
190
|
+
parsed.collection,
|
|
191
|
+
parsed.scope,
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
response: {
|
|
195
|
+
ok: true,
|
|
196
|
+
status: 200,
|
|
197
|
+
statusText: "OK",
|
|
198
|
+
body: rows,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (method === "POST") {
|
|
204
|
+
const bodyText = typeof options.body === "string" ? options.body : "{}";
|
|
205
|
+
let body: Record<string, unknown>;
|
|
206
|
+
try {
|
|
207
|
+
body = JSON.parse(bodyText);
|
|
208
|
+
} catch {
|
|
209
|
+
body = {};
|
|
210
|
+
}
|
|
211
|
+
if (body.data === undefined) {
|
|
212
|
+
return {
|
|
213
|
+
response: {
|
|
214
|
+
ok: false,
|
|
215
|
+
status: 400,
|
|
216
|
+
statusText: "Bad Request",
|
|
217
|
+
body: { error: "data is required" },
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const itemId =
|
|
222
|
+
typeof body.id === "string" && body.id.trim()
|
|
223
|
+
? body.id.trim()
|
|
224
|
+
: `item-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
225
|
+
const requestedScope =
|
|
226
|
+
body.scope === "org" || parsed.scope === "org" ? "org" : "user";
|
|
227
|
+
const now = new Date().toISOString();
|
|
228
|
+
const rows = readTransientRows(
|
|
229
|
+
extensionId,
|
|
230
|
+
parsed.collection,
|
|
231
|
+
requestedScope,
|
|
232
|
+
);
|
|
233
|
+
const existing = rows.find((row) => row.id === itemId);
|
|
234
|
+
const row: TransientDataRow = {
|
|
235
|
+
id: itemId,
|
|
236
|
+
extensionId,
|
|
237
|
+
toolId: extensionId,
|
|
238
|
+
collection: parsed.collection,
|
|
239
|
+
data:
|
|
240
|
+
typeof body.data === "string" ? body.data : JSON.stringify(body.data),
|
|
241
|
+
ownerEmail: "",
|
|
242
|
+
scope: requestedScope,
|
|
243
|
+
orgId: requestedScope === "org" ? "inline" : null,
|
|
244
|
+
createdAt: existing?.createdAt ?? now,
|
|
245
|
+
updatedAt: now,
|
|
246
|
+
};
|
|
247
|
+
writeTransientRows(extensionId, parsed.collection, requestedScope, [
|
|
248
|
+
row,
|
|
249
|
+
...rows.filter((item) => item.id !== itemId),
|
|
250
|
+
]);
|
|
251
|
+
return {
|
|
252
|
+
response: {
|
|
253
|
+
ok: true,
|
|
254
|
+
status: 200,
|
|
255
|
+
statusText: "OK",
|
|
256
|
+
body: row,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (method === "DELETE" && parsed.itemId) {
|
|
262
|
+
const rows = readTransientRows(
|
|
263
|
+
extensionId,
|
|
264
|
+
parsed.collection,
|
|
265
|
+
parsed.scope,
|
|
266
|
+
);
|
|
267
|
+
writeTransientRows(
|
|
268
|
+
extensionId,
|
|
269
|
+
parsed.collection,
|
|
270
|
+
parsed.scope,
|
|
271
|
+
rows.filter((row) => row.id !== parsed.itemId),
|
|
272
|
+
);
|
|
273
|
+
return {
|
|
274
|
+
response: {
|
|
275
|
+
ok: true,
|
|
276
|
+
status: 200,
|
|
277
|
+
statusText: "OK",
|
|
278
|
+
body: { ok: true },
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
response: {
|
|
285
|
+
ok: false,
|
|
286
|
+
status: 405,
|
|
287
|
+
statusText: "Method Not Allowed",
|
|
288
|
+
body: { error: "Unsupported transient extensionData request" },
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildTransientSrcDoc(
|
|
294
|
+
extension: InlineExtensionDefinition,
|
|
295
|
+
isDark: boolean,
|
|
296
|
+
): string {
|
|
297
|
+
return buildExtensionHtml(
|
|
298
|
+
extension.content ?? "",
|
|
299
|
+
getThemeVars(isDark),
|
|
300
|
+
isDark,
|
|
301
|
+
extension.id,
|
|
302
|
+
{
|
|
303
|
+
authorEmail: "",
|
|
304
|
+
viewerEmail: "",
|
|
305
|
+
isAuthor: true,
|
|
306
|
+
role: "owner",
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function InlineExtensionFrame({
|
|
312
|
+
extensionId,
|
|
313
|
+
extension: providedExtension,
|
|
314
|
+
slotId = "agent-chat.inline",
|
|
315
|
+
context,
|
|
316
|
+
className,
|
|
317
|
+
initialHeight = 260,
|
|
318
|
+
}: InlineExtensionFrameProps) {
|
|
319
|
+
const providedId = providedExtension?.id ?? extensionId ?? "";
|
|
320
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
321
|
+
const [height, setHeight] = useState<number>(initialHeight);
|
|
322
|
+
const [isDark, setIsDark] = useState(() =>
|
|
323
|
+
typeof document === "undefined"
|
|
324
|
+
? false
|
|
325
|
+
: document.documentElement.classList.contains("dark"),
|
|
326
|
+
);
|
|
327
|
+
const bridgeContextRef = useRef<BridgePolicyContext>({
|
|
328
|
+
role: providedExtension?.content ? "owner" : "viewer",
|
|
329
|
+
isAuthor: !!providedExtension?.content,
|
|
330
|
+
});
|
|
331
|
+
const bindingLatchedRef = useRef(false);
|
|
332
|
+
const [fetchedExtension, setFetchedExtension] =
|
|
333
|
+
useState<InlineExtensionDefinition | null>(null);
|
|
334
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
335
|
+
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
if (typeof document === "undefined") return;
|
|
338
|
+
setIsDark(document.documentElement.classList.contains("dark"));
|
|
339
|
+
const observer = new MutationObserver(() => {
|
|
340
|
+
setIsDark(document.documentElement.classList.contains("dark"));
|
|
341
|
+
});
|
|
342
|
+
observer.observe(document.documentElement, {
|
|
343
|
+
attributes: true,
|
|
344
|
+
attributeFilter: ["class"],
|
|
345
|
+
});
|
|
346
|
+
return () => observer.disconnect();
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (!providedId || providedExtension) {
|
|
351
|
+
setFetchedExtension(null);
|
|
352
|
+
setIsLoading(false);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let cancelled = false;
|
|
356
|
+
setIsLoading(true);
|
|
357
|
+
fetch(agentNativePath(`/_agent-native/extensions/${providedId}`))
|
|
358
|
+
.then(async (res) => {
|
|
359
|
+
if (!res.ok) return null;
|
|
360
|
+
return (await res.json()) as InlineExtensionDefinition;
|
|
361
|
+
})
|
|
362
|
+
.then((row) => {
|
|
363
|
+
if (!cancelled) setFetchedExtension(row);
|
|
364
|
+
})
|
|
365
|
+
.catch(() => {
|
|
366
|
+
if (!cancelled) setFetchedExtension(null);
|
|
367
|
+
})
|
|
368
|
+
.finally(() => {
|
|
369
|
+
if (!cancelled) setIsLoading(false);
|
|
370
|
+
});
|
|
371
|
+
return () => {
|
|
372
|
+
cancelled = true;
|
|
373
|
+
};
|
|
374
|
+
}, [providedExtension, providedId]);
|
|
375
|
+
|
|
376
|
+
const extension = providedExtension ?? fetchedExtension ?? null;
|
|
377
|
+
const resolvedId = extension?.id ?? providedId;
|
|
378
|
+
const isTransient =
|
|
379
|
+
extension?.mode === "transient" ||
|
|
380
|
+
(!!extension?.content && !extensionId && extension?.mode !== "persisted");
|
|
381
|
+
|
|
382
|
+
const initialDarkRef = useRef(isDark);
|
|
383
|
+
const iframeSrc = useMemo(() => {
|
|
384
|
+
if (!resolvedId || isTransient) return undefined;
|
|
385
|
+
const v = encodeURIComponent(extension?.updatedAt ?? "");
|
|
386
|
+
return agentNativePath(
|
|
387
|
+
`/_agent-native/extensions/${resolvedId}/render?slot=${encodeURIComponent(slotId)}&dark=${initialDarkRef.current}&v=${v}`,
|
|
388
|
+
);
|
|
389
|
+
}, [extension?.updatedAt, isTransient, resolvedId, slotId]);
|
|
390
|
+
|
|
391
|
+
const srcDoc = useMemo(() => {
|
|
392
|
+
if (!extension || !isTransient) return undefined;
|
|
393
|
+
return buildTransientSrcDoc(extension, initialDarkRef.current);
|
|
394
|
+
}, [extension, isTransient]);
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
bridgeContextRef.current = isTransient
|
|
398
|
+
? { role: "owner", isAuthor: true }
|
|
399
|
+
: { role: "viewer", isAuthor: false };
|
|
400
|
+
bindingLatchedRef.current = false;
|
|
401
|
+
setHeight(initialHeight);
|
|
402
|
+
}, [initialHeight, isTransient, resolvedId, extension?.updatedAt]);
|
|
403
|
+
|
|
404
|
+
const sendThemeToIframe = () => {
|
|
405
|
+
const win = iframeRef.current?.contentWindow;
|
|
406
|
+
if (!win) return;
|
|
407
|
+
win.postMessage(
|
|
408
|
+
{
|
|
409
|
+
type: "agent-native-theme-update",
|
|
410
|
+
isDark,
|
|
411
|
+
vars: getParentThemeVars(),
|
|
412
|
+
},
|
|
413
|
+
"*",
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
sendThemeToIframe();
|
|
419
|
+
}, [isDark, srcDoc, iframeSrc]);
|
|
420
|
+
|
|
421
|
+
const contextJson = JSON.stringify(context ?? {});
|
|
422
|
+
const sendContextToIframe = () => {
|
|
423
|
+
const win = iframeRef.current?.contentWindow;
|
|
424
|
+
if (!win) return;
|
|
425
|
+
win.postMessage(
|
|
426
|
+
{ type: "agent-native-slot-context", context: context ?? {} },
|
|
427
|
+
"*",
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
sendContextToIframe();
|
|
433
|
+
}, [contextJson, srcDoc, iframeSrc]);
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
const handleMessage = async (event: MessageEvent) => {
|
|
437
|
+
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
438
|
+
const message = event.data;
|
|
439
|
+
if (!message || typeof message !== "object") return;
|
|
440
|
+
|
|
441
|
+
if (message.type === "agent-native-extension-binding") {
|
|
442
|
+
if (bindingLatchedRef.current) return;
|
|
443
|
+
bindingLatchedRef.current = true;
|
|
444
|
+
const binding = (message as any).binding ?? {};
|
|
445
|
+
const role = normalizeRole(binding.role);
|
|
446
|
+
bridgeContextRef.current = {
|
|
447
|
+
role,
|
|
448
|
+
isAuthor: !!binding.isAuthor,
|
|
449
|
+
source: binding.source === "local-files" ? "local-files" : "database",
|
|
450
|
+
permissions:
|
|
451
|
+
binding && typeof binding.permissions === "object"
|
|
452
|
+
? binding.permissions
|
|
453
|
+
: undefined,
|
|
454
|
+
};
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (message.type === "agent-native-extension-resize") {
|
|
459
|
+
const h = Number((message as any).height);
|
|
460
|
+
if (Number.isFinite(h) && h > 0) {
|
|
461
|
+
setHeight(Math.ceil(Math.min(Math.max(h, 96), 1000)));
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (message.type === "agent-native-extension-keydown") {
|
|
467
|
+
document.dispatchEvent(
|
|
468
|
+
new KeyboardEvent("keydown", {
|
|
469
|
+
key: (message as any).key,
|
|
470
|
+
code: (message as any).code,
|
|
471
|
+
metaKey: !!(message as any).metaKey,
|
|
472
|
+
ctrlKey: !!(message as any).ctrlKey,
|
|
473
|
+
shiftKey: !!(message as any).shiftKey,
|
|
474
|
+
altKey: !!(message as any).altKey,
|
|
475
|
+
bubbles: true,
|
|
476
|
+
cancelable: true,
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (message.type === "agent-native-send-to-chat") {
|
|
483
|
+
const text = serializeChatValue((message as any).message);
|
|
484
|
+
if (!text?.trim()) return;
|
|
485
|
+
sendToAgentChat({
|
|
486
|
+
message: text,
|
|
487
|
+
context: serializeChatValue((message as any).context),
|
|
488
|
+
submit: (message as any).submit !== false,
|
|
489
|
+
openSidebar: (message as any).openSidebar !== false,
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (message.type === "agent-native-ui-output") {
|
|
495
|
+
window.dispatchEvent(
|
|
496
|
+
new CustomEvent("agentNative.inlineUiOutput", {
|
|
497
|
+
detail: {
|
|
498
|
+
extensionId: resolvedId,
|
|
499
|
+
key:
|
|
500
|
+
typeof (message as any).key === "string"
|
|
501
|
+
? (message as any).key
|
|
502
|
+
: undefined,
|
|
503
|
+
value: (message as any).value,
|
|
504
|
+
output: (message as any).output,
|
|
505
|
+
},
|
|
506
|
+
}),
|
|
507
|
+
);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (message.type === "agent-native-extension-error-fix") {
|
|
512
|
+
const errors: string[] = Array.isArray((message as any).errors)
|
|
513
|
+
? (message as any).errors
|
|
514
|
+
: [];
|
|
515
|
+
sendToAgentChat({
|
|
516
|
+
message: `Fix runtime errors in the inline extension "${extension?.name ?? resolvedId}".\n\nErrors:\n${errors.join("\n")}`,
|
|
517
|
+
context: [
|
|
518
|
+
`The user is viewing an inline generated extension named "${extension?.name ?? resolvedId}".`,
|
|
519
|
+
isTransient
|
|
520
|
+
? "This is a transient chat-only extension. Re-render it with render-inline-extension instead of update-extension."
|
|
521
|
+
: `This is saved as extension id ${resolvedId}. Use get-extension/update-extension for durable edits.`,
|
|
522
|
+
extension?.content
|
|
523
|
+
? `\nCurrent inline content:\n\`\`\`html\n${extension.content}\n\`\`\``
|
|
524
|
+
: "",
|
|
525
|
+
]
|
|
526
|
+
.filter(Boolean)
|
|
527
|
+
.join("\n"),
|
|
528
|
+
submit: true,
|
|
529
|
+
openSidebar: true,
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (message.type !== "agent-native-extension-request") return;
|
|
535
|
+
|
|
536
|
+
const requestId = String((message as any).requestId ?? "");
|
|
537
|
+
const path = String((message as any).path ?? "");
|
|
538
|
+
const respond = (payload: Record<string, unknown>) => {
|
|
539
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
540
|
+
{
|
|
541
|
+
type: "agent-native-extension-response",
|
|
542
|
+
requestId,
|
|
543
|
+
...payload,
|
|
544
|
+
},
|
|
545
|
+
"*",
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (
|
|
550
|
+
!requestId ||
|
|
551
|
+
!resolvedId ||
|
|
552
|
+
!isAllowedExtensionPath(path, resolvedId)
|
|
553
|
+
) {
|
|
554
|
+
respond({ error: "Extension request path is not allowed" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const options = sanitizeExtensionRequestOptions(
|
|
560
|
+
(message as any).options,
|
|
561
|
+
);
|
|
562
|
+
const policy = checkBridgePolicy(path, options.method ?? "GET", {
|
|
563
|
+
...bridgeContextRef.current,
|
|
564
|
+
extensionId: resolvedId,
|
|
565
|
+
});
|
|
566
|
+
if (!policy.ok) {
|
|
567
|
+
respond({
|
|
568
|
+
response: {
|
|
569
|
+
ok: false,
|
|
570
|
+
status: 403,
|
|
571
|
+
statusText: "Forbidden",
|
|
572
|
+
body: { error: policy.error },
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isTransient) {
|
|
579
|
+
const localData = await handleTransientExtensionData(
|
|
580
|
+
resolvedId,
|
|
581
|
+
path,
|
|
582
|
+
options,
|
|
583
|
+
);
|
|
584
|
+
if (localData) {
|
|
585
|
+
respond(localData);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const finalHeaders = new Headers(options.headers ?? undefined);
|
|
591
|
+
finalHeaders.set("X-Agent-Native-Extension-Bridge", "1");
|
|
592
|
+
finalHeaders.set("X-Agent-Native-Extension-Id", resolvedId);
|
|
593
|
+
finalHeaders.set("X-Agent-Native-Tool-Bridge", "1");
|
|
594
|
+
finalHeaders.set("X-Agent-Native-Tool-Id", resolvedId);
|
|
595
|
+
const res = await fetch(agentNativePath(path), {
|
|
596
|
+
...options,
|
|
597
|
+
headers: finalHeaders,
|
|
598
|
+
credentials: "same-origin",
|
|
599
|
+
});
|
|
600
|
+
const text = await res.text();
|
|
601
|
+
let body: unknown = text;
|
|
602
|
+
if (text) {
|
|
603
|
+
try {
|
|
604
|
+
body = JSON.parse(text);
|
|
605
|
+
} catch {
|
|
606
|
+
body = text;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
respond({
|
|
610
|
+
response: {
|
|
611
|
+
ok: res.ok,
|
|
612
|
+
status: res.status,
|
|
613
|
+
statusText: res.statusText,
|
|
614
|
+
body,
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
} catch (err: any) {
|
|
618
|
+
respond({ error: err?.message ?? "Extension host request failed" });
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
window.addEventListener("message", handleMessage);
|
|
623
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
624
|
+
}, [extension, isTransient, resolvedId]);
|
|
625
|
+
|
|
626
|
+
if (!extension) {
|
|
627
|
+
if (!isLoading) return null;
|
|
628
|
+
return (
|
|
629
|
+
<div
|
|
630
|
+
className={className}
|
|
631
|
+
style={{ height: initialHeight }}
|
|
632
|
+
aria-busy="true"
|
|
633
|
+
/>
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return (
|
|
638
|
+
<div className={`relative ${className ?? ""}`}>
|
|
639
|
+
<iframe
|
|
640
|
+
ref={iframeRef}
|
|
641
|
+
key={`${resolvedId}-${extension.updatedAt ?? "inline"}-${isTransient ? "transient" : "persisted"}`}
|
|
642
|
+
src={iframeSrc}
|
|
643
|
+
srcDoc={srcDoc}
|
|
644
|
+
title={extension.name}
|
|
645
|
+
sandbox="allow-scripts allow-forms"
|
|
646
|
+
style={{ width: "100%", border: 0, height, display: "block" }}
|
|
647
|
+
onLoad={() => {
|
|
648
|
+
sendThemeToIframe();
|
|
649
|
+
sendContextToIframe();
|
|
650
|
+
}}
|
|
651
|
+
/>
|
|
652
|
+
</div>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
@@ -164,6 +164,8 @@ export type ExtensionBridgeRole = "owner" | "admin" | "editor" | "viewer";
|
|
|
164
164
|
const READ_METHODS = new Set(["GET", "HEAD"]);
|
|
165
165
|
|
|
166
166
|
export interface BridgePolicyContext {
|
|
167
|
+
/** Extension id currently owning the iframe. Used for narrow helper-specific exceptions. */
|
|
168
|
+
extensionId?: string;
|
|
167
169
|
/** Resolved role of the viewer on this extension. */
|
|
168
170
|
role: ExtensionBridgeRole;
|
|
169
171
|
/** True when viewer is the extension's owner_email — equivalent to role "owner"
|
|
@@ -267,6 +269,12 @@ export function checkBridgePolicy(
|
|
|
267
269
|
// application-state — viewers can read but not write.
|
|
268
270
|
if (path.startsWith("/_agent-native/application-state/")) {
|
|
269
271
|
if (READ_METHODS.has(upperMethod)) return { ok: true };
|
|
272
|
+
if (
|
|
273
|
+
upperMethod === "PUT" &&
|
|
274
|
+
isInlineUiOutputApplicationStatePath(path, ctx.extensionId)
|
|
275
|
+
) {
|
|
276
|
+
return { ok: true };
|
|
277
|
+
}
|
|
270
278
|
return {
|
|
271
279
|
ok: false,
|
|
272
280
|
error: deniedMessage("appFetch (mutation)", ctx.role),
|
|
@@ -339,9 +347,31 @@ function checkLocalFileBridgePolicy(
|
|
|
339
347
|
|
|
340
348
|
if (path.startsWith("/_agent-native/application-state/")) {
|
|
341
349
|
if (READ_METHODS.has(upperMethod)) return { ok: true };
|
|
350
|
+
if (
|
|
351
|
+
upperMethod === "PUT" &&
|
|
352
|
+
isInlineUiOutputApplicationStatePath(path, ctx.extensionId)
|
|
353
|
+
) {
|
|
354
|
+
return { ok: true };
|
|
355
|
+
}
|
|
342
356
|
return { ok: false, error: localDeniedMessage("applicationState") };
|
|
343
357
|
}
|
|
344
358
|
|
|
345
359
|
if (READ_METHODS.has(upperMethod)) return { ok: true };
|
|
346
360
|
return { ok: false, error: localDeniedMessage("appFetch") };
|
|
347
361
|
}
|
|
362
|
+
|
|
363
|
+
function isInlineUiOutputApplicationStatePath(
|
|
364
|
+
path: string,
|
|
365
|
+
extensionId?: string,
|
|
366
|
+
): boolean {
|
|
367
|
+
if (!extensionId) return false;
|
|
368
|
+
try {
|
|
369
|
+
const pathname = new URL(path, "http://agent-native.local").pathname;
|
|
370
|
+
const prefix = "/_agent-native/application-state/";
|
|
371
|
+
if (!pathname.startsWith(prefix)) return false;
|
|
372
|
+
const key = decodeURIComponent(pathname.slice(prefix.length));
|
|
373
|
+
return key === `inline-ui:${extensionId}:output`;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -13,6 +13,11 @@ export {
|
|
|
13
13
|
EmbeddedExtension,
|
|
14
14
|
type EmbeddedExtensionProps,
|
|
15
15
|
} from "./EmbeddedExtension.js";
|
|
16
|
+
export {
|
|
17
|
+
InlineExtensionFrame,
|
|
18
|
+
type InlineExtensionDefinition,
|
|
19
|
+
type InlineExtensionFrameProps,
|
|
20
|
+
} from "./InlineExtensionFrame.js";
|
|
16
21
|
export { ExtensionSlot, type ExtensionSlotProps } from "./ExtensionSlot.js";
|
|
17
22
|
export {
|
|
18
23
|
deleteOrHideExtension,
|