@agent-native/core 0.78.9 → 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 (176) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +11 -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/server/agent-chat-plugin.ts +21 -9
  35. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  38. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +198 -59
  39. package/corpus/templates/analytics/AGENTS.md +12 -0
  40. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1105 -0
  41. package/corpus/templates/analytics/actions/dashboard-panel-order.ts +169 -0
  42. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +89 -6
  43. package/corpus/templates/analytics/actions/mutate-dashboard.ts +333 -0
  44. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +214 -0
  45. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +48 -21
  46. package/corpus/templates/analytics/actions/update-dashboard.ts +193 -100
  47. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -0
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  49. package/corpus/templates/analytics/app/global.css +81 -10
  50. package/corpus/templates/analytics/app/i18n-data.ts +249 -4
  51. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +3 -3
  52. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +41 -1
  53. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -4
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +48 -18
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +32 -0
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +204 -70
  58. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +1101 -227
  59. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +160 -133
  60. package/corpus/templates/analytics/changelog/2026-06-26-agents-can-now-edit-dashboards-through-a-typed-mutation-scri.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-bar-chart-hovers-stay-subtle-in-dark-mode.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-chart-moves-are-now-handled-by-panel-id-making-sim.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-drag-smoothly-with-stable-cards-inactive-ch.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-show-a-refresh-spinner-while-keeping-existi.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-edits-stay-stable-while-chart-order-and-deletions-.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-email-reports-queue-immediate-sends.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-26-explorer-dashboards-now-drag-charts-with-a-stable-preview-an.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-playback-can-now-be-toggled-by-clicking-the-r.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-06-26-session-replays-now-open-from-the-full-row-and-play-with-tim.md +6 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +21 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report.ts +41 -17
  72. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  73. package/dist/action-ui.d.ts +1 -0
  74. package/dist/action-ui.d.ts.map +1 -1
  75. package/dist/action-ui.js +1 -0
  76. package/dist/action-ui.js.map +1 -1
  77. package/dist/client/analytics.d.ts.map +1 -1
  78. package/dist/client/analytics.js +173 -12
  79. package/dist/client/analytics.js.map +1 -1
  80. package/dist/client/chat/index.d.ts +1 -1
  81. package/dist/client/chat/index.d.ts.map +1 -1
  82. package/dist/client/chat/index.js +1 -1
  83. package/dist/client/chat/index.js.map +1 -1
  84. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  85. package/dist/client/chat/tool-call-display.js +3 -1
  86. package/dist/client/chat/tool-call-display.js.map +1 -1
  87. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts +17 -0
  88. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -0
  89. package/dist/client/chat/widgets/InlineExtensionWidget.js +85 -0
  90. package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -0
  91. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  92. package/dist/client/chat/widgets/builtin-tool-renderers.js +15 -2
  93. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  94. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  95. package/dist/client/conversation/AgentConversation.js +33 -0
  96. package/dist/client/conversation/AgentConversation.js.map +1 -1
  97. package/dist/client/conversation/code-agent-transcript.js +8 -0
  98. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  99. package/dist/client/conversation/types.d.ts +4 -0
  100. package/dist/client/conversation/types.d.ts.map +1 -1
  101. package/dist/client/conversation/types.js.map +1 -1
  102. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  103. package/dist/client/extensions/EmbeddedExtension.js +29 -1
  104. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  105. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  106. package/dist/client/extensions/ExtensionViewer.js +28 -1
  107. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  108. package/dist/client/extensions/InlineExtensionFrame.d.ts +25 -0
  109. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -0
  110. package/dist/client/extensions/InlineExtensionFrame.js +488 -0
  111. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -0
  112. package/dist/client/extensions/iframe-bridge.d.ts +2 -0
  113. package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
  114. package/dist/client/extensions/iframe-bridge.js +23 -0
  115. package/dist/client/extensions/iframe-bridge.js.map +1 -1
  116. package/dist/client/extensions/index.d.ts +1 -0
  117. package/dist/client/extensions/index.d.ts.map +1 -1
  118. package/dist/client/extensions/index.js +1 -0
  119. package/dist/client/extensions/index.js.map +1 -1
  120. package/dist/client/index.d.ts +1 -1
  121. package/dist/client/index.d.ts.map +1 -1
  122. package/dist/client/index.js +1 -1
  123. package/dist/client/index.js.map +1 -1
  124. package/dist/client/session-replay.d.ts +11 -1
  125. package/dist/client/session-replay.d.ts.map +1 -1
  126. package/dist/client/session-replay.js +74 -14
  127. package/dist/client/session-replay.js.map +1 -1
  128. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  129. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  130. package/dist/code-agents/transcript-normalizer.js +8 -0
  131. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  132. package/dist/collab/awareness.d.ts +2 -2
  133. package/dist/collab/awareness.d.ts.map +1 -1
  134. package/dist/collab/routes.d.ts +1 -1
  135. package/dist/extensions/actions.d.ts.map +1 -1
  136. package/dist/extensions/actions.js +200 -1
  137. package/dist/extensions/actions.js.map +1 -1
  138. package/dist/extensions/html-shell.d.ts.map +1 -1
  139. package/dist/extensions/html-shell.js +70 -3
  140. package/dist/extensions/html-shell.js.map +1 -1
  141. package/dist/extensions/routes.d.ts.map +1 -1
  142. package/dist/extensions/routes.js +11 -4
  143. package/dist/extensions/routes.js.map +1 -1
  144. package/dist/index.browser.d.ts +1 -1
  145. package/dist/index.browser.d.ts.map +1 -1
  146. package/dist/index.browser.js +1 -1
  147. package/dist/index.browser.js.map +1 -1
  148. package/dist/index.d.ts +1 -1
  149. package/dist/index.d.ts.map +1 -1
  150. package/dist/index.js +1 -1
  151. package/dist/index.js.map +1 -1
  152. package/dist/notifications/routes.d.ts +2 -2
  153. package/dist/observability/routes.d.ts +3 -3
  154. package/dist/progress/routes.d.ts +1 -1
  155. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  156. package/dist/resources/handlers.d.ts +3 -3
  157. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  158. package/dist/server/agent-chat-plugin.js +21 -9
  159. package/dist/server/agent-chat-plugin.js.map +1 -1
  160. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  161. package/dist/server/transcribe-voice.d.ts +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  163. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  164. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  165. package/docs/content/actions.md +4 -2
  166. package/docs/content/agent-surfaces.md +9 -0
  167. package/docs/content/extensions.md +10 -0
  168. package/docs/content/generative-ui.md +217 -0
  169. package/docs/content/key-concepts.md +2 -2
  170. package/docs/content/mcp-apps.md +3 -5
  171. package/docs/content/native-chat-ui.md +9 -5
  172. package/docs/content/tracking.md +9 -3
  173. package/package.json +1 -1
  174. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  175. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  176. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
@@ -573,6 +573,7 @@ export {
573
573
  ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER,
574
574
  ACTION_CHAT_UI_DATA_TABLE_RENDERER,
575
575
  ACTION_CHAT_UI_DATA_WIDGET_RENDERER,
576
+ ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
576
577
  type ActionChatUIConfig,
577
578
  } from "../action-ui.js";
578
579
  export {
@@ -38,7 +38,8 @@ interface SessionReplayState {
38
38
  active: boolean;
39
39
  replayId: string | null;
40
40
  sequence: number;
41
- queue: ReplayEvent[];
41
+ /** Pre-serialized + scrubbed event JSON strings, ready to splice at flush. */
42
+ queue: string[];
42
43
  queuedBytes: number;
43
44
  flushTimer: number | null;
44
45
  maxDurationTimer: number | null;
@@ -55,10 +56,14 @@ interface StoredReplaySession {
55
56
  sequence?: number;
56
57
  }
57
58
 
59
+ /** rrweb `sampling` shape (mousemove/scroll/media throttles, input strategy). */
60
+ export type ReplayEventSampling = Record<string, unknown>;
61
+
58
62
  export interface SessionReplayOptions {
59
63
  enabled?: boolean;
60
64
  publicKey?: string;
61
65
  endpoint?: string;
66
+ requireSignedInUser?: boolean;
62
67
  sampleRate?: number;
63
68
  samplingSalt?: string;
64
69
  allowUrls?: SessionReplayUrlMatcher[];
@@ -78,6 +83,13 @@ export interface SessionReplayOptions {
78
83
  recordCrossOriginIframes?: boolean;
79
84
  collectFonts?: boolean;
80
85
  inlineImages?: boolean;
86
+ /**
87
+ * rrweb per-event throttling (distinct from `sampleRate`, which decides
88
+ * whether a whole session records). Throttles high-frequency event types
89
+ * (mousemove/scroll/input) to keep the recorded page responsive and the
90
+ * payloads small. Passed straight through to `rrweb.record({ sampling })`.
91
+ */
92
+ eventSampling?: ReplayEventSampling;
81
93
  extraProperties?:
82
94
  | Record<string, unknown>
83
95
  | (() => Record<string, unknown> | undefined);
@@ -90,6 +102,7 @@ export interface SessionReplayStartResult {
90
102
  | "not-browser"
91
103
  | "missing-public-key"
92
104
  | "missing-session-id"
105
+ | "missing-user-id"
93
106
  | "sampled-out"
94
107
  | "url-blocked"
95
108
  | "already-active"
@@ -103,6 +116,7 @@ export interface SessionReplayStartResult {
103
116
  interface NormalizedSessionReplayOptions {
104
117
  publicKey: string;
105
118
  endpoint: string;
119
+ requireSignedInUser: boolean;
106
120
  sampleRate: number;
107
121
  samplingSalt: string;
108
122
  allowUrls: SessionReplayUrlMatcher[];
@@ -122,11 +136,25 @@ interface NormalizedSessionReplayOptions {
122
136
  recordCrossOriginIframes: boolean;
123
137
  collectFonts: boolean;
124
138
  inlineImages: boolean;
139
+ eventSampling: ReplayEventSampling;
125
140
  extraProperties?: SessionReplayOptions["extraProperties"];
126
141
  }
127
142
 
128
143
  const DEFAULT_REPLAY_PATH = "/api/analytics/replay";
129
144
  const DEFAULT_SAMPLING_SALT = "agent-native-session-replay";
145
+
146
+ /**
147
+ * Default rrweb event throttling. Without this, rrweb captures every
148
+ * mousemove/scroll which dominates event volume and main-thread cost on
149
+ * interactive pages. These caps keep replays faithful while staying light.
150
+ */
151
+ const DEFAULT_EVENT_SAMPLING: ReplayEventSampling = {
152
+ mousemove: 50,
153
+ mouseInteraction: true,
154
+ scroll: 100,
155
+ media: 800,
156
+ input: "last",
157
+ };
130
158
  const SESSION_REPLAY_STATE_KEY = Symbol.for(
131
159
  "agent-native.client.sessionReplay",
132
160
  );
@@ -278,6 +306,14 @@ function readEnvNumber(key: string): number | undefined {
278
306
  return Number.isFinite(value) ? value : undefined;
279
307
  }
280
308
 
309
+ function readEnvBoolean(key: string): boolean | undefined {
310
+ const raw = readEnvString(key);
311
+ if (!raw) return undefined;
312
+ if (/^(1|true|yes|on)$/i.test(raw)) return true;
313
+ if (/^(0|false|no|off)$/i.test(raw)) return false;
314
+ return undefined;
315
+ }
316
+
281
317
  function readFirstEnvString(keys: string[]): string | undefined {
282
318
  for (const key of keys) {
283
319
  const value = readEnvString(key);
@@ -399,6 +435,11 @@ function normalizeOptions(
399
435
  return {
400
436
  publicKey,
401
437
  endpoint,
438
+ requireSignedInUser:
439
+ options.requireSignedInUser ??
440
+ readEnvBoolean("VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH") ??
441
+ readEnvBoolean("VITE_SESSION_REPLAY_REQUIRE_AUTH") ??
442
+ false,
402
443
  sampleRate: clampSamplingRate(
403
444
  options.sampleRate ??
404
445
  readFirstEnvNumber([
@@ -451,6 +492,7 @@ function normalizeOptions(
451
492
  recordCrossOriginIframes: options.recordCrossOriginIframes ?? false,
452
493
  collectFonts: options.collectFonts ?? false,
453
494
  inlineImages: options.inlineImages ?? false,
495
+ eventSampling: options.eventSampling ?? DEFAULT_EVENT_SAMPLING,
454
496
  extraProperties: options.extraProperties,
455
497
  };
456
498
  }
@@ -490,15 +532,25 @@ function scrubReplayValue(
490
532
  return out;
491
533
  }
492
534
 
493
- function scrubReplayEvent(event: ReplayEvent): ReplayEvent {
494
- return scrubReplayValue(event) as ReplayEvent;
535
+ /**
536
+ * JSON.stringify replacer that scrubs URL-like string values inline. Folding
537
+ * the scrub into the single serialization pass avoids a separate deep-clone of
538
+ * every emitted event (FullSnapshots are large DOM trees) on the hot path.
539
+ */
540
+ function scrubReplayReplacer(key: string, value: unknown): unknown {
541
+ return typeof value === "string" ? scrubStringValue(key, value) : value;
495
542
  }
496
543
 
497
- function estimateJsonBytes(value: unknown): number {
544
+ /**
545
+ * Serialize + scrub one event in a single pass. The resulting string is stored
546
+ * directly on the queue and reused verbatim at flush, so each event is
547
+ * stringified exactly once (was: deep-clone + size-stringify + flush-stringify).
548
+ */
549
+ function serializeReplayEvent(event: ReplayEvent): string {
498
550
  try {
499
- return JSON.stringify(value).length;
551
+ return JSON.stringify(event, scrubReplayReplacer);
500
552
  } catch {
501
- return 0;
553
+ return "";
502
554
  }
503
555
  }
504
556
 
@@ -507,15 +559,16 @@ function enqueueReplayEvent(
507
559
  event: ReplayEvent,
508
560
  ): void {
509
561
  if (!state.options) return;
510
- const sanitized = scrubReplayEvent(event);
511
- const estimatedBytes = estimateJsonBytes(sanitized);
562
+ const serialized = serializeReplayEvent(event);
563
+ if (!serialized) return;
564
+ const estimatedBytes = serialized.length;
512
565
  if (
513
566
  state.queue.length > 0 &&
514
567
  state.queuedBytes + estimatedBytes > state.options.maxBatchBytes
515
568
  ) {
516
569
  void flushSessionReplay("max-bytes");
517
570
  }
518
- state.queue.push(sanitized);
571
+ state.queue.push(serialized);
519
572
  state.queuedBytes += estimatedBytes;
520
573
  if (state.queue.length >= state.options.maxEventsPerBatch) {
521
574
  void flushSessionReplay("max-events");
@@ -536,20 +589,27 @@ function replayExtraProperties(
536
589
  }
537
590
  }
538
591
 
592
+ function replayString(value: unknown): string | undefined {
593
+ return typeof value === "string" && value.trim() ? value : undefined;
594
+ }
595
+
539
596
  function buildReplayBody(
540
597
  state: SessionReplayState,
541
598
  reason: string,
542
- events: ReplayEvent[],
599
+ eventJsonParts: string[],
543
600
  ): string | null {
544
601
  const options = state.options;
545
602
  if (!options || !state.replayId) return null;
546
603
  const sessionId = getAnalyticsSessionId();
547
604
  if (!sessionId) return null;
548
- const body = {
605
+ const properties = replayExtraProperties(options);
606
+ const userId = replayString(properties?.userId ?? properties?.user_id);
607
+ const envelope = {
549
608
  publicKey: options.publicKey,
550
609
  type: "session_replay",
551
610
  replayId: state.replayId,
552
611
  sessionId,
612
+ ...(userId ? { userId } : {}),
553
613
  anonymousId: getAnalyticsAnonymousId(),
554
614
  sequence: state.sequence,
555
615
  reason,
@@ -560,12 +620,14 @@ function buildReplayBody(
560
620
  ? scrubUrl(window.location.href)
561
621
  : undefined,
562
622
  timestamp: new Date().toISOString(),
563
- properties: replayExtraProperties(options),
564
- events,
623
+ properties,
565
624
  };
566
625
  state.sequence += 1;
567
626
  persistReplaySequence(sessionId, state.replayId, state.sequence);
568
- return JSON.stringify(body);
627
+ // Events are already serialized+scrubbed JSON strings; splice them into the
628
+ // envelope without re-serializing the (potentially large) events array.
629
+ const envelopeJson = JSON.stringify(envelope);
630
+ return `${envelopeJson.slice(0, -1)},"events":[${eventJsonParts.join(",")}]}`;
569
631
  }
570
632
 
571
633
  function isFinalFlushReason(reason: string): boolean {
@@ -673,6 +735,18 @@ export async function startSessionReplay(
673
735
  if (!sampled) {
674
736
  return { started: false, reason: "sampled-out", sessionId, sampled };
675
737
  }
738
+ if (normalized.requireSignedInUser) {
739
+ const properties = replayExtraProperties(normalized);
740
+ const userId = replayString(properties?.userId ?? properties?.user_id);
741
+ if (!userId) {
742
+ return {
743
+ started: false,
744
+ reason: "missing-user-id",
745
+ sessionId,
746
+ sampled,
747
+ };
748
+ }
749
+ }
676
750
  if (!isUrlRecordable(window.location.href, normalized)) {
677
751
  return { started: false, reason: "url-blocked", sessionId, sampled };
678
752
  }
@@ -707,6 +781,7 @@ export async function startSessionReplay(
707
781
  try {
708
782
  const stopRecorder = rrweb.record({
709
783
  emit: (event) => enqueueReplayEvent(state, event),
784
+ sampling: normalized.eventSampling,
710
785
  checkoutEveryNth: normalized.checkoutEveryNth,
711
786
  checkoutEveryNms: normalized.checkoutEveryNms,
712
787
  blockSelector: normalized.blockSelector,
@@ -1,3 +1,7 @@
1
+ import {
2
+ normalizeActionChatUIConfig,
3
+ type ActionChatUIConfig,
4
+ } from "../action-ui.js";
1
5
  import type { CodeAgentTranscriptEvent } from "../cli/code-agent-runs.js";
2
6
  import type { AgentMcpAppPayload } from "../mcp-client/app-result.js";
3
7
 
@@ -47,6 +51,7 @@ export interface NormalizedCodeAgentToolEvent extends NormalizedCodeAgentTranscr
47
51
  input?: unknown;
48
52
  result?: unknown;
49
53
  mcpApp?: AgentMcpAppPayload;
54
+ chatUI?: ActionChatUIConfig;
50
55
  activities: string[];
51
56
  startedAt?: string;
52
57
  completedAt?: string;
@@ -297,6 +302,8 @@ function appendToolEvent(
297
302
  }
298
303
  const mcpApp = mcpAppMetadata(event.metadata);
299
304
  if (mcpApp) item.mcpApp = mcpApp;
305
+ const chatUI = chatUIMetadata(event.metadata);
306
+ if (chatUI) item.chatUI = chatUI;
300
307
  const doneMeta = structuredMetadata(event.metadata);
301
308
  if (doneMeta) item.structuredMeta = doneMeta;
302
309
  }
@@ -322,6 +329,7 @@ function createToolEvent(
322
329
  input: hasMetadataKey(metadata, "input") ? metadata?.input : undefined,
323
330
  result: hasMetadataKey(metadata, "result") ? metadata?.result : undefined,
324
331
  mcpApp: mcpAppMetadata(metadata),
332
+ chatUI: chatUIMetadata(metadata),
325
333
  activities: toolType === "activity" ? [event.message] : [],
326
334
  startedAt: toolType === "tool_start" ? event.createdAt : undefined,
327
335
  completedAt: toolType === "tool_done" ? event.createdAt : undefined,
@@ -627,3 +635,9 @@ function mcpAppMetadata(
627
635
  }
628
636
  return candidate as AgentMcpAppPayload;
629
637
  }
638
+
639
+ function chatUIMetadata(
640
+ metadata: Record<string, unknown> | undefined,
641
+ ): ActionChatUIConfig | undefined {
642
+ return normalizeActionChatUIConfig(metadata?.chatUI);
643
+ }
@@ -1,3 +1,4 @@
1
+ import { ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER } from "../action-ui.js";
1
2
  import type { ActionRunContext } from "../action.js";
2
3
  import type { ActionEntry } from "../agent/production-agent.js";
3
4
  import type { AgentChatAttachment } from "../agent/types.js";
@@ -256,10 +257,196 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
256
257
  readOnly: true,
257
258
  },
258
259
 
260
+ "render-inline-extension": {
261
+ tool: {
262
+ description:
263
+ "Render a one-time, transient sandboxed Alpine.js mini-app directly inside the chat. Use this for generated UI that should answer the current turn inline without saving anything to the Extensions view: calculators, adjustable controls, knobs, pickers, visualizers, temporary dashboards, and interactive results. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<inline extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. For transient UIs, extensionData is browser-local throwaway state; use application_state/appFetch, appAction, ui.output, or chat.send for anything the agent or app must observe. Use semantic Tailwind colors (bg-background, text-foreground, bg-primary, etc.) so it inherits the parent app theme. Use create-extension instead when the user wants the UI saved or reusable.",
264
+ parameters: {
265
+ type: "object",
266
+ properties: {
267
+ name: {
268
+ type: "string",
269
+ description: "Short display name for the inline UI.",
270
+ },
271
+ description: {
272
+ type: "string",
273
+ description: "One-sentence summary of what the inline UI does.",
274
+ },
275
+ content: {
276
+ type: "string",
277
+ description:
278
+ "Self-contained Alpine.js HTML body snippet. Do not include a full app build, React code, or source files. Required unless contentFromAttachment is set.",
279
+ },
280
+ contentFromAttachment: {
281
+ type: "string",
282
+ description:
283
+ 'Render a pasted/attached HTML file verbatim without re-typing it. Set to an attachment name or "latest".',
284
+ },
285
+ context: {
286
+ type: "string",
287
+ description:
288
+ "Optional JSON object passed to the iframe as slotContext for initial inputs from chat.",
289
+ },
290
+ initialHeight: {
291
+ type: "number",
292
+ description:
293
+ "Optional initial iframe height in pixels before auto-resize reports. Defaults to 260.",
294
+ },
295
+ },
296
+ required: ["name"],
297
+ },
298
+ },
299
+ chatUI: {
300
+ renderer: ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
301
+ title: "Inline extension",
302
+ },
303
+ maxResultChars: 220_000,
304
+ readOnly: true,
305
+ run: async (args, ctx) => {
306
+ const name = String(args?.name ?? "").trim();
307
+ if (!name) return "Error: name is required.";
308
+ const resolved = resolveExtensionContent(args, ctx);
309
+ if ("error" in resolved) return resolved.error;
310
+ const content = resolved.content.trim();
311
+ if (!content) return "Error: content is required.";
312
+ const description = String(args?.description ?? "").trim();
313
+ const id = `inline-${Date.now().toString(36)}-${Math.random()
314
+ .toString(36)
315
+ .slice(2, 8)}`;
316
+ return {
317
+ ok: true,
318
+ inlineExtension: {
319
+ mode: "transient",
320
+ id,
321
+ name,
322
+ description,
323
+ content,
324
+ context: parseInlineContext(args?.context),
325
+ initialHeight: coerceInlineHeight(args?.initialHeight),
326
+ },
327
+ next: "Rendered inline in chat only. It is not saved in the Extensions view.",
328
+ };
329
+ },
330
+ },
331
+
332
+ "show-extension-inline": {
333
+ tool: {
334
+ description:
335
+ "Render an existing saved extension inline in the chat. Use this when the user asks to load, reopen, reuse, or show a saved extension/widget/dashboard/calculator/mini-app in the conversation. Inline extensions can expose passive current values through agentNative.ui.output(value, opts?), which writes application state at inline-ui:<extension id>:output for the agent to read later with readAppState. Pass id when known; otherwise pass a search string and the action will use the best visible extension match.",
336
+ parameters: {
337
+ type: "object",
338
+ properties: {
339
+ id: {
340
+ type: "string",
341
+ description: "Extension id to render inline.",
342
+ },
343
+ search: {
344
+ type: "string",
345
+ description:
346
+ "Fallback search matched against id, name, description, and owner email when id is unknown.",
347
+ },
348
+ context: {
349
+ type: "string",
350
+ description:
351
+ "Optional JSON object passed to the iframe as slotContext for chat-provided inputs.",
352
+ },
353
+ initialHeight: {
354
+ type: "number",
355
+ description:
356
+ "Optional initial iframe height in pixels before auto-resize reports. Defaults to 260.",
357
+ },
358
+ },
359
+ },
360
+ },
361
+ chatUI: {
362
+ renderer: ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
363
+ title: "Inline extension",
364
+ },
365
+ readOnly: true,
366
+ run: async (args) => {
367
+ let id = String(args?.id ?? "").trim();
368
+ const search = String(args?.search ?? "")
369
+ .trim()
370
+ .toLowerCase();
371
+
372
+ if (!id && search) {
373
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
374
+ const rows: Array<ExtensionRow | LocalExtensionRow> = [
375
+ ...(await listExtensions({
376
+ includeHidden: false,
377
+ includeGloballyHidden: false,
378
+ })),
379
+ ...(await listLocalExtensions()),
380
+ ];
381
+ const match = rows.find((row) =>
382
+ [row.id, row.name, row.description, row.ownerEmail]
383
+ .join("\n")
384
+ .toLowerCase()
385
+ .includes(search),
386
+ );
387
+ if (match) {
388
+ id = match.id;
389
+ } else {
390
+ return {
391
+ ok: false,
392
+ error: `No extension matched "${args?.search}".`,
393
+ available: await Promise.all(
394
+ rows
395
+ .slice(0, 10)
396
+ .map((row) => summarizeExtension(row, hiddenIds, false)),
397
+ ),
398
+ };
399
+ }
400
+ }
401
+
402
+ if (!id) return "Error: provide id or search.";
403
+
404
+ const localExtension = await getLocalExtension(id);
405
+ const hiddenIds = await getHiddenExtensionIdsForCurrentUser();
406
+ if (localExtension) {
407
+ const summary = await summarizeExtension(
408
+ localExtension,
409
+ hiddenIds,
410
+ false,
411
+ );
412
+ return {
413
+ ok: true,
414
+ inlineExtension: {
415
+ mode: "persisted",
416
+ id: summary.id,
417
+ name: summary.name,
418
+ description: summary.description,
419
+ path: summary.path,
420
+ updatedAt: summary.updatedAt,
421
+ context: parseInlineContext(args?.context),
422
+ initialHeight: coerceInlineHeight(args?.initialHeight),
423
+ },
424
+ };
425
+ }
426
+
427
+ const extension = await getExtension(id);
428
+ if (!extension) return `Error: extension not found: ${id}`;
429
+ const summary = await summarizeExtension(extension, hiddenIds, false);
430
+ return {
431
+ ok: true,
432
+ inlineExtension: {
433
+ mode: "persisted",
434
+ id: summary.id,
435
+ name: summary.name,
436
+ description: summary.description,
437
+ path: summary.path,
438
+ updatedAt: summary.updatedAt,
439
+ context: parseInlineContext(args?.context),
440
+ initialHeight: coerceInlineHeight(args?.initialHeight),
441
+ },
442
+ };
443
+ },
444
+ },
445
+
259
446
  "create-extension": {
260
447
  tool: {
261
448
  description:
262
- 'Create a sandboxed Alpine.js mini-app extension. Use this when the user asks to create, build, or make an extension/widget/dashboard/calculator. Call this action exactly once per requested extension. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), and extensionData. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery()/dbExec() only for known existing SQL tables. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
449
+ 'Create a persisted sandboxed Alpine.js mini-app extension and render it inline in the chat. Use this when the user wants generated UI that should be saved, reusable, or visible in the Extensions view: extensions, widgets, dashboards, calculators, mini-apps, and reusable interactive utilities. For one-time chat-only UI, use render-inline-extension instead. The content must be a self-contained Alpine.js HTML body snippet that can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send()/sendToAgentChat(). Use agentNative.ui.output for passive current values from knobs, sliders, and selections; it writes application state at inline-ui:<extension id>:output, which the agent can read later with readAppState when the user says to use that value. Use agentNative.chat.send for visible submit/apply actions. Persist reusable user-edited state with extensionData: if the extension has checkboxes, todos, notes, filters, preferences, or any control whose value should survive reload/reopen, load that state on init and save changes with extensionData, usually at user scope, instead of keeping it only in Alpine state. IMPORTANT — hosting a pasted file: if the user pasted a large HTML/Alpine file (it appears in your context as an <attachment name="pasted-text-…"> block) and asked you to host it as-is, do NOT copy that file into `content`. Instead leave `content` empty and pass `contentFromAttachment` set to that attachment\'s name (or the literal "latest" for the most recent pasted block) — the server reads the file verbatim. Re-emitting a large pasted file as `content` regularly gets cut off mid-stream and stalls the turn. Prefer appAction(name, params) for app data and actions, including read actions mounted as GET; do not call template /api/* routes from appFetch because the extension bridge only allows framework /_agent-native/* paths. Parse JSON string action results before aggregating; use dbQuery()/dbExec() only for known existing SQL tables. Keep the initial create-extension payload compact and working; for complex extensions, create a useful v1 first, then use focused update-extension edits for refinements rather than assembling one enormous initial tool input. For any non-trivial component (more than a couple of state fields, any methods, any string formatting, any branching) put the component in a <script> block via Alpine.data(\'name\', () => ({...})) and reference it with x-data="name" — do NOT cram methods, template literals, or branching logic into an inline x-data="{...}" attribute (HTML parser pitfalls cause ReferenceError failures). Define every variable referenced from x-text/x-show/x-if/x-for on the data object\'s initial state. If the extension\'s value depends on an LLM call, require a real key via \\${keys.OPENAI_API_KEY}/\\${keys.ANTHROPIC_API_KEY} (and tell the user to add it in the Dispatch Vault, or in app Settings → API Keys & Connections for standalone apps, if missing) or route the AI work to the agent chat — never ship a stubbed analysis step that renders a placeholder/boolean as the result.',
263
450
  parameters: {
264
451
  type: "object",
265
452
  properties: {
@@ -290,6 +477,10 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
290
477
  required: ["name"],
291
478
  },
292
479
  },
480
+ chatUI: {
481
+ renderer: ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
482
+ title: "Extension",
483
+ },
293
484
  run: async (args, ctx) => {
294
485
  const name = String(args?.name ?? "").trim();
295
486
  if (!name) return "Error: name is required.";
@@ -1010,6 +1201,32 @@ function coerceBoolean(value: unknown): boolean {
1010
1201
  return value === true || value === "true";
1011
1202
  }
1012
1203
 
1204
+ function parseInlineContext(
1205
+ value: unknown,
1206
+ ): Record<string, unknown> | undefined {
1207
+ if (value === undefined || value === null || value === "") return undefined;
1208
+ const parsed =
1209
+ typeof value === "string"
1210
+ ? (() => {
1211
+ try {
1212
+ return JSON.parse(value);
1213
+ } catch {
1214
+ return undefined;
1215
+ }
1216
+ })()
1217
+ : value;
1218
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
1219
+ ? (parsed as Record<string, unknown>)
1220
+ : undefined;
1221
+ }
1222
+
1223
+ function coerceInlineHeight(value: unknown): number | undefined {
1224
+ if (value === undefined || value === null || value === "") return undefined;
1225
+ const height = Number(value);
1226
+ if (!Number.isFinite(height) || height <= 0) return undefined;
1227
+ return Math.min(Math.max(Math.round(height), 120), 1000);
1228
+ }
1229
+
1013
1230
  function coerceLimit(value: unknown): number {
1014
1231
  const limit = Number(value ?? 100);
1015
1232
  if (!Number.isFinite(limit)) return 100;
@@ -425,6 +425,53 @@ export function buildExtensionHtml(
425
425
  return res.body;
426
426
  }
427
427
 
428
+ function sendToChat(message, options) {
429
+ options = options || {};
430
+ var text = typeof message === 'string' ? message : JSON.stringify(message);
431
+ window.parent.postMessage({
432
+ type: 'agent-native-send-to-chat',
433
+ message: text,
434
+ context: options.context,
435
+ submit: options.submit !== false,
436
+ openSidebar: options.openSidebar !== false,
437
+ }, '*');
438
+ return { ok: true };
439
+ }
440
+
441
+ function inlineUiOutputKey() {
442
+ var safeId = String(_extensionId || 'unknown').replace(/[^A-Za-z0-9_:-]/g, '') || 'unknown';
443
+ return 'inline-ui:' + safeId + ':output';
444
+ }
445
+
446
+ async function outputToUi(value, options) {
447
+ options = options || {};
448
+ var key = inlineUiOutputKey();
449
+ var payload = {
450
+ value: value,
451
+ updatedAt: new Date().toISOString(),
452
+ extensionId: _extensionId,
453
+ source: 'inline-ui',
454
+ };
455
+ if (options.label !== undefined) payload.label = options.label;
456
+ if (options.context !== undefined) payload.context = options.context;
457
+ if (options.meta !== undefined) payload.meta = options.meta;
458
+ var output = await appFetch('/_agent-native/application-state/' + key, {
459
+ method: 'PUT',
460
+ headers: { 'X-Request-Source': 'inline-ui' },
461
+ body: JSON.stringify(payload),
462
+ });
463
+ try {
464
+ window.parent.postMessage({
465
+ type: 'agent-native-ui-output',
466
+ extensionId: _extensionId,
467
+ key: key,
468
+ value: value,
469
+ output: output,
470
+ }, '*');
471
+ } catch (_) {}
472
+ return { ok: true, key: key, output: output };
473
+ }
474
+
428
475
  async function dbQuery(sql, args) {
429
476
  var body = { sql: sql };
430
477
  if (args) body.args = args;
@@ -519,6 +566,25 @@ export function buildExtensionHtml(
519
566
  var toolFetch = extensionFetch;
520
567
  var toolData = extensionData;
521
568
  var _toolId = _extensionId;
569
+ window.agentNative = Object.assign(window.agentNative || {}, {
570
+ extensionId: _extensionId,
571
+ extensionBinding: _extensionBinding,
572
+ appAction: appAction,
573
+ appFetch: appFetch,
574
+ dbQuery: dbQuery,
575
+ dbExec: dbExec,
576
+ extensionFetch: extensionFetch,
577
+ extensionData: extensionData,
578
+ data: extensionData,
579
+ sendToChat: sendToChat,
580
+ chat: Object.assign({}, (window.agentNative && window.agentNative.chat) || {}, {
581
+ send: sendToChat,
582
+ }),
583
+ ui: Object.assign({}, (window.agentNative && window.agentNative.ui) || {}, {
584
+ output: outputToUi,
585
+ }),
586
+ });
587
+ window.sendToAgentChat = sendToChat;
522
588
  </script>
523
589
  <style>
524
590
  #__extension-error-toast {
@@ -572,9 +638,10 @@ export function buildExtensionHtml(
572
638
  });
573
639
  });
574
640
 
575
- // Auto-resize the iframe to its content when running in slot mode. The
576
- // host listens for agent-native-extension-resize and adjusts the iframe height.
577
- if (new URLSearchParams(location.search).get('slot')) {
641
+ // Auto-resize iframe renders. Persisted extension slots include ?slot=;
642
+ // transient inline chat UI uses srcdoc, so detect that by parent frame.
643
+ // The host listens for agent-native-extension-resize and adjusts height.
644
+ if (new URLSearchParams(location.search).get('slot') || window.parent !== window) {
578
645
  var _lastH = 0;
579
646
  var _reportHeight = function() {
580
647
  var h = Math.max(
@@ -85,7 +85,11 @@ export function createExtensionsHandler() {
85
85
  }
86
86
 
87
87
  const orgCtx = await getOrgContext(event).catch(() => null);
88
- const userEmail = session.email;
88
+ const userEmail = normalizeExtensionUserEmail(session.email);
89
+ if (!userEmail) {
90
+ setResponseStatus(event, 401);
91
+ return { error: "Authentication required" };
92
+ }
89
93
  const orgId = orgCtx?.orgId ?? session.orgId ?? undefined;
90
94
 
91
95
  try {
@@ -103,6 +107,10 @@ export function createExtensionsHandler() {
103
107
  });
104
108
  }
105
109
 
110
+ function normalizeExtensionUserEmail(email: string): string {
111
+ return email.trim().toLowerCase();
112
+ }
113
+
106
114
  async function dispatch(
107
115
  event: H3Event,
108
116
  method: string,
@@ -499,7 +507,7 @@ async function handleExtensionDataList(
499
507
  sql: `SELECT COALESCE(item_id, id) AS id, tool_id, collection, data, owner_email, scope, org_id, created_at, updated_at
500
508
  FROM tool_data
501
509
  WHERE tool_id = ? AND collection = ?
502
- AND ((scope = 'user' AND owner_email = ?) OR (scope = 'org' AND org_id = ?))
510
+ AND ((scope = 'user' AND lower(owner_email) = ?) OR (scope = 'org' AND org_id = ?))
503
511
  ORDER BY created_at DESC
504
512
  LIMIT ?`,
505
513
  args: [extensionId, collection, userEmail, orgId ?? "", limit],
@@ -510,7 +518,7 @@ async function handleExtensionDataList(
510
518
  const result = await client.execute({
511
519
  sql: `SELECT COALESCE(item_id, id) AS id, tool_id, collection, data, owner_email, scope, org_id, created_at, updated_at
512
520
  FROM tool_data
513
- WHERE tool_id = ? AND collection = ? AND scope = 'user' AND owner_email = ?
521
+ WHERE tool_id = ? AND collection = ? AND scope = 'user' AND lower(owner_email) = ?
514
522
  ORDER BY updated_at DESC
515
523
  LIMIT ?`,
516
524
  args: [extensionId, collection, userEmail, limit],
@@ -614,7 +622,7 @@ async function handleExtensionDataDelete(
614
622
  }
615
623
 
616
624
  await client.execute({
617
- sql: `DELETE FROM tool_data WHERE COALESCE(item_id, id) = ? AND tool_id = ? AND collection = ? AND scope = 'user' AND owner_email = ?`,
625
+ sql: `DELETE FROM tool_data WHERE COALESCE(item_id, id) = ? AND tool_id = ? AND collection = ? AND scope = 'user' AND lower(owner_email) = ?`,
618
626
  args: [itemId, extensionId, collection, userEmail],
619
627
  });
620
628
  return { ok: true };