@agent-native/core 0.78.9 → 0.79.1
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 +17 -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/agent/production-agent.ts +8 -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/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/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -0
- package/dist/agent/production-agent.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 +1 -1
- 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/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/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- 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/server/transcribe-voice.d.ts +1 -1
- 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
|
@@ -38,7 +38,8 @@ interface SessionReplayState {
|
|
|
38
38
|
active: boolean;
|
|
39
39
|
replayId: string | null;
|
|
40
40
|
sequence: number;
|
|
41
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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(
|
|
551
|
+
return JSON.stringify(event, scrubReplayReplacer);
|
|
500
552
|
} catch {
|
|
501
|
-
return
|
|
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
|
|
511
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
564
|
-
events,
|
|
623
|
+
properties,
|
|
565
624
|
};
|
|
566
625
|
state.sequence += 1;
|
|
567
626
|
persistReplaySequence(sessionId, state.replayId, state.sequence);
|
|
568
|
-
|
|
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
|
|
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
|
|
576
|
-
//
|
|
577
|
-
|
|
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 };
|