@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.
Files changed (181) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/docs/content/actions.md +4 -2
  4. package/corpus/core/docs/content/agent-surfaces.md +9 -0
  5. package/corpus/core/docs/content/extensions.md +10 -0
  6. package/corpus/core/docs/content/generative-ui.md +217 -0
  7. package/corpus/core/docs/content/key-concepts.md +2 -2
  8. package/corpus/core/docs/content/mcp-apps.md +3 -5
  9. package/corpus/core/docs/content/native-chat-ui.md +9 -5
  10. package/corpus/core/docs/content/tracking.md +9 -3
  11. package/corpus/core/package.json +1 -1
  12. package/corpus/core/src/action-ui.ts +1 -0
  13. package/corpus/core/src/client/analytics.ts +195 -12
  14. package/corpus/core/src/client/chat/index.ts +1 -0
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +5 -2
  16. package/corpus/core/src/client/chat/widgets/InlineExtensionWidget.tsx +139 -0
  17. package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +27 -1
  18. package/corpus/core/src/client/conversation/AgentConversation.tsx +38 -0
  19. package/corpus/core/src/client/conversation/code-agent-transcript.ts +9 -0
  20. package/corpus/core/src/client/conversation/types.ts +4 -0
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +27 -5
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +26 -5
  23. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +654 -0
  24. package/corpus/core/src/client/extensions/iframe-bridge.ts +30 -0
  25. package/corpus/core/src/client/extensions/index.ts +5 -0
  26. package/corpus/core/src/client/index.ts +1 -0
  27. package/corpus/core/src/client/session-replay.ts +89 -14
  28. package/corpus/core/src/code-agents/transcript-normalizer.ts +14 -0
  29. package/corpus/core/src/extensions/actions.ts +218 -1
  30. package/corpus/core/src/extensions/html-shell.ts +70 -3
  31. package/corpus/core/src/extensions/routes.ts +12 -4
  32. package/corpus/core/src/index.browser.ts +1 -0
  33. package/corpus/core/src/index.ts +1 -0
  34. package/corpus/core/src/mcp/build-server.ts +1 -1
  35. package/corpus/core/src/mcp/embed-app.ts +11 -1
  36. package/corpus/core/src/server/agent-chat-plugin.ts +21 -9
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  39. package/corpus/core/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  40. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +198 -59
  41. package/corpus/templates/analytics/AGENTS.md +12 -0
  42. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1105 -0
  43. package/corpus/templates/analytics/actions/dashboard-panel-order.ts +169 -0
  44. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +89 -6
  45. package/corpus/templates/analytics/actions/mutate-dashboard.ts +333 -0
  46. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +214 -0
  47. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +48 -21
  48. package/corpus/templates/analytics/actions/update-dashboard.ts +193 -100
  49. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -0
  50. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  51. package/corpus/templates/analytics/app/global.css +81 -10
  52. package/corpus/templates/analytics/app/i18n-data.ts +249 -4
  53. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +3 -3
  54. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +41 -1
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +1 -1
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -4
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +48 -18
  58. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +32 -0
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +204 -70
  60. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +1101 -227
  61. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +160 -133
  62. package/corpus/templates/analytics/changelog/2026-06-26-agents-can-now-edit-dashboards-through-a-typed-mutation-scri.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-bar-chart-hovers-stay-subtle-in-dark-mode.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-chart-moves-are-now-handled-by-panel-id-making-sim.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-drag-smoothly-with-stable-cards-inactive-ch.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-show-a-refresh-spinner-while-keeping-existi.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-edits-stay-stable-while-chart-order-and-deletions-.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-email-reports-queue-immediate-sends.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-06-26-explorer-dashboards-now-drag-charts-with-a-stable-preview-an.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-playback-can-now-be-toggled-by-clicking-the-r.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-06-26-session-replays-now-open-from-the-full-row-and-play-with-tim.md +6 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +21 -0
  73. package/corpus/templates/analytics/server/lib/dashboard-report.ts +41 -17
  74. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  75. package/dist/action-ui.d.ts +1 -0
  76. package/dist/action-ui.d.ts.map +1 -1
  77. package/dist/action-ui.js +1 -0
  78. package/dist/action-ui.js.map +1 -1
  79. package/dist/client/analytics.d.ts.map +1 -1
  80. package/dist/client/analytics.js +173 -12
  81. package/dist/client/analytics.js.map +1 -1
  82. package/dist/client/chat/index.d.ts +1 -1
  83. package/dist/client/chat/index.d.ts.map +1 -1
  84. package/dist/client/chat/index.js +1 -1
  85. package/dist/client/chat/index.js.map +1 -1
  86. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  87. package/dist/client/chat/tool-call-display.js +3 -1
  88. package/dist/client/chat/tool-call-display.js.map +1 -1
  89. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts +17 -0
  90. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -0
  91. package/dist/client/chat/widgets/InlineExtensionWidget.js +85 -0
  92. package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -0
  93. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  94. package/dist/client/chat/widgets/builtin-tool-renderers.js +15 -2
  95. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  96. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  97. package/dist/client/conversation/AgentConversation.js +33 -0
  98. package/dist/client/conversation/AgentConversation.js.map +1 -1
  99. package/dist/client/conversation/code-agent-transcript.js +8 -0
  100. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  101. package/dist/client/conversation/types.d.ts +4 -0
  102. package/dist/client/conversation/types.d.ts.map +1 -1
  103. package/dist/client/conversation/types.js.map +1 -1
  104. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  105. package/dist/client/extensions/EmbeddedExtension.js +29 -1
  106. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  107. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  108. package/dist/client/extensions/ExtensionViewer.js +28 -1
  109. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  110. package/dist/client/extensions/InlineExtensionFrame.d.ts +25 -0
  111. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -0
  112. package/dist/client/extensions/InlineExtensionFrame.js +488 -0
  113. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -0
  114. package/dist/client/extensions/iframe-bridge.d.ts +2 -0
  115. package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
  116. package/dist/client/extensions/iframe-bridge.js +23 -0
  117. package/dist/client/extensions/iframe-bridge.js.map +1 -1
  118. package/dist/client/extensions/index.d.ts +1 -0
  119. package/dist/client/extensions/index.d.ts.map +1 -1
  120. package/dist/client/extensions/index.js +1 -0
  121. package/dist/client/extensions/index.js.map +1 -1
  122. package/dist/client/index.d.ts +1 -1
  123. package/dist/client/index.d.ts.map +1 -1
  124. package/dist/client/index.js +1 -1
  125. package/dist/client/index.js.map +1 -1
  126. package/dist/client/session-replay.d.ts +11 -1
  127. package/dist/client/session-replay.d.ts.map +1 -1
  128. package/dist/client/session-replay.js +74 -14
  129. package/dist/client/session-replay.js.map +1 -1
  130. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  131. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  132. package/dist/code-agents/transcript-normalizer.js +8 -0
  133. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  134. package/dist/collab/awareness.d.ts +2 -2
  135. package/dist/collab/awareness.d.ts.map +1 -1
  136. package/dist/collab/routes.d.ts +2 -2
  137. package/dist/extensions/actions.d.ts.map +1 -1
  138. package/dist/extensions/actions.js +200 -1
  139. package/dist/extensions/actions.js.map +1 -1
  140. package/dist/extensions/html-shell.d.ts.map +1 -1
  141. package/dist/extensions/html-shell.js +70 -3
  142. package/dist/extensions/html-shell.js.map +1 -1
  143. package/dist/extensions/routes.d.ts.map +1 -1
  144. package/dist/extensions/routes.js +11 -4
  145. package/dist/extensions/routes.js.map +1 -1
  146. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  147. package/dist/index.browser.d.ts +1 -1
  148. package/dist/index.browser.d.ts.map +1 -1
  149. package/dist/index.browser.js +1 -1
  150. package/dist/index.browser.js.map +1 -1
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.d.ts.map +1 -1
  153. package/dist/index.js +1 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/mcp/build-server.js +1 -1
  156. package/dist/mcp/build-server.js.map +1 -1
  157. package/dist/mcp/embed-app.d.ts.map +1 -1
  158. package/dist/mcp/embed-app.js +11 -1
  159. package/dist/mcp/embed-app.js.map +1 -1
  160. package/dist/observability/routes.d.ts +7 -7
  161. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  162. package/dist/resources/handlers.d.ts +2 -2
  163. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  164. package/dist/server/agent-chat-plugin.js +21 -9
  165. package/dist/server/agent-chat-plugin.js.map +1 -1
  166. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  167. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  168. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  169. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  170. package/docs/content/actions.md +4 -2
  171. package/docs/content/agent-surfaces.md +9 -0
  172. package/docs/content/extensions.md +10 -0
  173. package/docs/content/generative-ui.md +217 -0
  174. package/docs/content/key-concepts.md +2 -2
  175. package/docs/content/mcp-apps.md +3 -5
  176. package/docs/content/native-chat-ui.md +9 -5
  177. package/docs/content/tracking.md +9 -3
  178. package/package.json +1 -1
  179. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  180. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  181. 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,