@agent-native/core 0.98.5 → 0.98.6
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 +12 -0
- package/corpus/core/docs/content/external-agents.mdx +62 -7
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +37 -7
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +37 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/handlers.ts +95 -21
- package/corpus/core/src/a2a/task-store.ts +65 -3
- package/corpus/core/src/integrations/adapters/slack.ts +123 -13
- package/corpus/core/src/integrations/identity-links-store.ts +210 -0
- package/corpus/core/src/integrations/identity.ts +132 -0
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/plugin.ts +58 -1
- package/corpus/core/src/integrations/types.ts +7 -0
- package/corpus/core/src/mcp/build-server.ts +127 -22
- package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
- package/corpus/core/src/mcp/index.ts +1 -0
- package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
- package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/corpus/templates/analytics/AGENTS.md +20 -5
- package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +0 -1
- package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
- package/corpus/templates/analytics/actions/list-error-issues.ts +12 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +48 -17
- package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -2
- package/corpus/templates/analytics/server/lib/error-capture.ts +47 -5
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +49 -17
- package/corpus/templates/analytics/server/lib/session-replay.ts +16 -7
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/analytics/server/plugins/db.ts +15 -0
- package/corpus/templates/forms/app/global.css +54 -8
- package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -17
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +8 -5
- package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
- package/corpus/templates/mail/AGENTS.md +3 -1
- package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +84 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/task-store.d.ts +20 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +57 -4
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +98 -13
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/identity-links-store.d.ts +23 -0
- package/dist/integrations/identity-links-store.d.ts.map +1 -0
- package/dist/integrations/identity-links-store.js +158 -0
- package/dist/integrations/identity-links-store.js.map +1 -0
- package/dist/integrations/identity.d.ts +11 -0
- package/dist/integrations/identity.d.ts.map +1 -0
- package/dist/integrations/identity.js +100 -0
- package/dist/integrations/identity.js.map +1 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +43 -1
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +6 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/mcp/build-server.d.ts +9 -2
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +97 -20
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/external-agent-policy.d.ts +19 -0
- package/dist/mcp/external-agent-policy.d.ts.map +1 -0
- package/dist/mcp/external-agent-policy.js +2 -0
- package/dist/mcp/external-agent-policy.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- 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-options.d.ts +11 -1
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
- package/dist/server/agent-chat/script-entries.js +16 -2
- package/dist/server/agent-chat/script-entries.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/docs/content/external-agents.mdx +62 -7
- package/docs/content/locales/ar-SA/external-agents.mdx +38 -7
- package/docs/content/locales/de-DE/external-agents.mdx +39 -7
- package/docs/content/locales/es-ES/external-agents.mdx +39 -7
- package/docs/content/locales/fr-FR/external-agents.mdx +39 -7
- package/docs/content/locales/hi-IN/external-agents.mdx +38 -7
- package/docs/content/locales/ja-JP/external-agents.mdx +38 -7
- package/docs/content/locales/ko-KR/external-agents.mdx +38 -7
- package/docs/content/locales/pt-BR/external-agents.mdx +39 -7
- package/docs/content/locales/zh-CN/external-agents.mdx +37 -7
- package/docs/content/locales/zh-TW/external-agents.mdx +37 -7
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
IconSettings,
|
|
22
22
|
} from "@tabler/icons-react";
|
|
23
23
|
import { useQuery } from "@tanstack/react-query";
|
|
24
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
24
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
25
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
|
26
26
|
import { toast } from "sonner";
|
|
27
27
|
|
|
@@ -169,6 +169,40 @@ type SessionRecordingDevice = Pick<SessionRecordingSummary, "metadata">;
|
|
|
169
169
|
const RANGE_OPTIONS: ReplayRange[] = ["24h", "7d", "30d", "90d", "all"];
|
|
170
170
|
const SESSION_QUERY_DEBOUNCE_MS = 250;
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Local input state for a URL-backed filter, debounced into the URL.
|
|
174
|
+
*
|
|
175
|
+
* `urlValue` only resyncs local state when it changes for a reason other
|
|
176
|
+
* than this hook's own debounced write (back/forward navigation, an agent
|
|
177
|
+
* driven URL change, etc). React Router commits `setSearchParams` inside a
|
|
178
|
+
* transition, so without this guard the echo of our own write can land
|
|
179
|
+
* after a newer keystroke and clobber it.
|
|
180
|
+
*/
|
|
181
|
+
export function useDebouncedUrlFilter(
|
|
182
|
+
urlValue: string,
|
|
183
|
+
onCommit: (value: string) => void,
|
|
184
|
+
): [string, (value: string) => void] {
|
|
185
|
+
const [input, setInput] = useState(urlValue);
|
|
186
|
+
const lastPushedRef = useRef(urlValue);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (urlValue === lastPushedRef.current) return;
|
|
190
|
+
lastPushedRef.current = urlValue;
|
|
191
|
+
setInput(urlValue);
|
|
192
|
+
}, [urlValue]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (input === urlValue) return;
|
|
196
|
+
const timeout = window.setTimeout(() => {
|
|
197
|
+
lastPushedRef.current = input;
|
|
198
|
+
onCommit(input);
|
|
199
|
+
}, SESSION_QUERY_DEBOUNCE_MS);
|
|
200
|
+
return () => window.clearTimeout(timeout);
|
|
201
|
+
}, [input, urlValue, onCommit]);
|
|
202
|
+
|
|
203
|
+
return [input, setInput];
|
|
204
|
+
}
|
|
205
|
+
|
|
172
206
|
export default function SessionsPage() {
|
|
173
207
|
const t = useT();
|
|
174
208
|
const navigate = useNavigate();
|
|
@@ -176,13 +210,8 @@ export default function SessionsPage() {
|
|
|
176
210
|
const range = readRange(searchParams.get("range"));
|
|
177
211
|
const app = searchParams.get("app") ?? "";
|
|
178
212
|
const query = searchParams.get("q") ?? "";
|
|
179
|
-
const [queryInput, setQueryInput] = useState(query);
|
|
180
213
|
const from = useMemo(() => rangeToFrom(range), [range]);
|
|
181
214
|
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
setQueryInput(query);
|
|
184
|
-
}, [query]);
|
|
185
|
-
|
|
186
215
|
const updateFilter = useCallback(
|
|
187
216
|
(key: string, value: string) => {
|
|
188
217
|
setSearchParams(
|
|
@@ -200,13 +229,17 @@ export default function SessionsPage() {
|
|
|
200
229
|
[setSearchParams],
|
|
201
230
|
);
|
|
202
231
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
232
|
+
const commitQuery = useCallback(
|
|
233
|
+
(value: string) => updateFilter("q", value),
|
|
234
|
+
[updateFilter],
|
|
235
|
+
);
|
|
236
|
+
const [queryInput, setQueryInput] = useDebouncedUrlFilter(query, commitQuery);
|
|
237
|
+
|
|
238
|
+
const commitApp = useCallback(
|
|
239
|
+
(value: string) => updateFilter("app", value),
|
|
240
|
+
[updateFilter],
|
|
241
|
+
);
|
|
242
|
+
const [appInput, setAppInput] = useDebouncedUrlFilter(app, commitApp);
|
|
210
243
|
|
|
211
244
|
const { data, isLoading, isFetching, refetch, error } = useActionQuery<
|
|
212
245
|
SessionRecordingSummary[]
|
|
@@ -291,10 +324,8 @@ export default function SessionsPage() {
|
|
|
291
324
|
{t("sessions.userFilters")}
|
|
292
325
|
</div>
|
|
293
326
|
<Input
|
|
294
|
-
value={
|
|
295
|
-
onChange={(event) =>
|
|
296
|
-
updateFilter("app", event.target.value)
|
|
297
|
-
}
|
|
327
|
+
value={appInput}
|
|
328
|
+
onChange={(event) => setAppInput(event.target.value)}
|
|
298
329
|
placeholder={t("sessions.appPlaceholder")}
|
|
299
330
|
className="h-9"
|
|
300
331
|
/>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* The deliberately small authenticated MCP surface for Analytics.
|
|
3
3
|
*
|
|
4
4
|
* Keep this list read-only and incident-focused. In particular, do not add
|
|
5
|
-
* replay-event/blob actions or dashboard/data mutation actions here.
|
|
5
|
+
* raw replay-event/blob actions or dashboard/data mutation actions here.
|
|
6
6
|
*/
|
|
7
7
|
export const ANALYTICS_CONNECTOR_CATALOG = [
|
|
8
8
|
"list-session-recordings",
|
|
9
9
|
"get-session-replay-summary",
|
|
10
|
-
"get-session-replay-
|
|
10
|
+
"get-session-replay-timeline",
|
|
11
11
|
"query-agent-native-analytics",
|
|
12
12
|
"list-error-issues",
|
|
13
13
|
"get-error-issue",
|
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
* reads go through `accessFilter` so an org-scoped key surfaces its issues to
|
|
19
19
|
* the whole org exactly like session recordings.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import { appStateGet } from "@agent-native/core/application-state";
|
|
22
22
|
import { notifyWithDelivery } from "@agent-native/core/notifications";
|
|
23
23
|
import { recordChange } from "@agent-native/core/server";
|
|
24
24
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
and,
|
|
27
27
|
desc,
|
|
28
28
|
eq,
|
|
29
|
+
exists,
|
|
29
30
|
inArray,
|
|
30
31
|
isNull,
|
|
31
32
|
notInArray,
|
|
@@ -1073,6 +1074,8 @@ export interface ListErrorIssuesFilters {
|
|
|
1073
1074
|
status?: IssueStatus | "all";
|
|
1074
1075
|
query?: string;
|
|
1075
1076
|
app?: string;
|
|
1077
|
+
sessionRecordingId?: string;
|
|
1078
|
+
userId?: string;
|
|
1076
1079
|
sort?: "lastSeen" | "eventCount" | "firstSeen";
|
|
1077
1080
|
limit?: number;
|
|
1078
1081
|
}
|
|
@@ -1106,10 +1109,14 @@ export function anonymizeErrorReportingEmails<T>(value: T): T {
|
|
|
1106
1109
|
return value;
|
|
1107
1110
|
}
|
|
1108
1111
|
|
|
1109
|
-
async function isErrorDemoModeEnabled(
|
|
1112
|
+
async function isErrorDemoModeEnabled(
|
|
1113
|
+
userEmail?: string | null,
|
|
1114
|
+
): Promise<boolean> {
|
|
1110
1115
|
if (process.env.DEMO_MODE === "true") return true;
|
|
1116
|
+
if (!userEmail) return false;
|
|
1111
1117
|
try {
|
|
1112
|
-
|
|
1118
|
+
// Keep demo state tied to the caller whose issues are being read.
|
|
1119
|
+
const state = await appStateGet(userEmail, "demo-mode");
|
|
1113
1120
|
return state?.enabled === true;
|
|
1114
1121
|
} catch {
|
|
1115
1122
|
return false;
|
|
@@ -1195,7 +1202,7 @@ export async function listErrorIssues(
|
|
|
1195
1202
|
scope: ErrorReadScope,
|
|
1196
1203
|
filters: ListErrorIssuesFilters = {},
|
|
1197
1204
|
): Promise<ErrorIssueSummary[]> {
|
|
1198
|
-
const demoModeEnabled = isErrorDemoModeEnabled();
|
|
1205
|
+
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1199
1206
|
const db = getDb() as any;
|
|
1200
1207
|
const limit = Math.min(
|
|
1201
1208
|
MAX_ISSUE_LIMIT,
|
|
@@ -1206,6 +1213,41 @@ export async function listErrorIssues(
|
|
|
1206
1213
|
conditions.push(eq(schema.errorIssues.status, filters.status));
|
|
1207
1214
|
}
|
|
1208
1215
|
if (filters.app) conditions.push(eq(schema.errorIssues.app, filters.app));
|
|
1216
|
+
const sessionRecordingId = filters.sessionRecordingId?.trim();
|
|
1217
|
+
const userId = filters.userId?.trim();
|
|
1218
|
+
if (sessionRecordingId || userId) {
|
|
1219
|
+
const occurrenceConditions: any[] = [
|
|
1220
|
+
eq(schema.errorEvents.issueId, schema.errorIssues.id),
|
|
1221
|
+
// Keep the child occurrence in the same tenant as its parent issue even
|
|
1222
|
+
// when the issue is visible through an org share.
|
|
1223
|
+
eq(schema.errorEvents.ownerEmail, schema.errorIssues.ownerEmail),
|
|
1224
|
+
or(
|
|
1225
|
+
eq(schema.errorEvents.orgId, schema.errorIssues.orgId),
|
|
1226
|
+
and(isNull(schema.errorEvents.orgId), isNull(schema.errorIssues.orgId)),
|
|
1227
|
+
),
|
|
1228
|
+
];
|
|
1229
|
+
if (sessionRecordingId) {
|
|
1230
|
+
occurrenceConditions.push(
|
|
1231
|
+
eq(schema.errorEvents.sessionRecordingId, sessionRecordingId),
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (userId) {
|
|
1235
|
+
occurrenceConditions.push(
|
|
1236
|
+
or(
|
|
1237
|
+
eq(schema.errorEvents.userId, userId),
|
|
1238
|
+
eq(schema.errorEvents.userKey, userId),
|
|
1239
|
+
),
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
conditions.push(
|
|
1243
|
+
exists(
|
|
1244
|
+
db
|
|
1245
|
+
.select({ id: schema.errorEvents.id })
|
|
1246
|
+
.from(schema.errorEvents)
|
|
1247
|
+
.where(and(...occurrenceConditions)),
|
|
1248
|
+
),
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1209
1251
|
const query = filters.query?.trim();
|
|
1210
1252
|
if (query) {
|
|
1211
1253
|
conditions.push(
|
|
@@ -1343,7 +1385,7 @@ export async function getErrorIssue(
|
|
|
1343
1385
|
issueId: string,
|
|
1344
1386
|
options: { eventsLimit?: number } = {},
|
|
1345
1387
|
): Promise<ErrorIssueDetail> {
|
|
1346
|
-
const demoModeEnabled = isErrorDemoModeEnabled();
|
|
1388
|
+
const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
|
|
1347
1389
|
const db = getDb() as any;
|
|
1348
1390
|
const [issueRow] = await db
|
|
1349
1391
|
.select()
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../../shared/session-replay-diagnostics.js";
|
|
23
23
|
import {
|
|
24
24
|
compactSessionRecordingSummary,
|
|
25
|
+
getSessionReplayEvents,
|
|
25
26
|
getSessionReplaySummary,
|
|
26
27
|
getSessionReplayTokenizedEvents,
|
|
27
28
|
getSessionReplayTokenizedSummary,
|
|
@@ -82,20 +83,16 @@ function replayStartedAt(events: AgentReplayEvent[]): number {
|
|
|
82
83
|
|
|
83
84
|
function pathLabel(href: string): string {
|
|
84
85
|
try {
|
|
85
|
-
const parsed = new URL(href);
|
|
86
|
-
return
|
|
86
|
+
const parsed = new URL(href, "https://placeholder.agent-native.local");
|
|
87
|
+
return boundedDiagnosticText(
|
|
88
|
+
parsed.pathname || parsed.hostname,
|
|
89
|
+
MAX_DIAGNOSTIC_URL_CHARS,
|
|
90
|
+
);
|
|
87
91
|
} catch {
|
|
88
|
-
return href;
|
|
92
|
+
return boundedDiagnosticText(href, MAX_DIAGNOSTIC_URL_CHARS);
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
function markerDetail(event: AgentReplayEvent): string | null {
|
|
93
|
-
const payload = event.data?.payload;
|
|
94
|
-
if (typeof payload?.message === "string") return payload.message;
|
|
95
|
-
if (typeof event.data?.href === "string") return event.data.href;
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
96
|
const TIMELINE_MARKER_CAP = 200;
|
|
100
97
|
const TIMELINE_ERROR_MARKER_RESERVE = 100;
|
|
101
98
|
const MAX_DIAGNOSTIC_MESSAGE_CHARS = 1_000;
|
|
@@ -528,10 +525,9 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
|
528
525
|
if (isFailedSessionReplayNetworkStatus(status)) {
|
|
529
526
|
const method =
|
|
530
527
|
boundedDiagnosticText(tagged.payload.method, 16) || "GET";
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
);
|
|
528
|
+
// External-agent timelines intentionally expose only a path label.
|
|
529
|
+
// Query strings commonly contain emails, tokens, and other secrets.
|
|
530
|
+
const url = pathLabel(String(tagged.payload.url ?? ""));
|
|
535
531
|
markers.push({
|
|
536
532
|
timestamp,
|
|
537
533
|
offsetMs: Math.max(0, timestamp - startedAt),
|
|
@@ -553,7 +549,7 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
|
553
549
|
offsetMs: Math.max(0, timestamp - startedAt),
|
|
554
550
|
kind: "navigation",
|
|
555
551
|
label: pathLabel(event.data.href),
|
|
556
|
-
detail: event.data.href,
|
|
552
|
+
detail: pathLabel(event.data.href),
|
|
557
553
|
});
|
|
558
554
|
} else if (
|
|
559
555
|
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
@@ -606,8 +602,14 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
|
606
602
|
timestamp,
|
|
607
603
|
offsetMs: Math.max(0, timestamp - startedAt),
|
|
608
604
|
kind: "custom",
|
|
609
|
-
label:
|
|
610
|
-
|
|
605
|
+
label: boundedDiagnosticText(
|
|
606
|
+
String(event.data?.tag ?? "Custom event"),
|
|
607
|
+
MAX_DIAGNOSTIC_MESSAGE_CHARS,
|
|
608
|
+
),
|
|
609
|
+
// Custom-event payloads are application-defined and can contain input
|
|
610
|
+
// values or secrets. The tag and timestamp are sufficient metadata for
|
|
611
|
+
// this sanitized external-agent timeline.
|
|
612
|
+
detail: null,
|
|
611
613
|
});
|
|
612
614
|
}
|
|
613
615
|
}
|
|
@@ -615,6 +617,36 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
|
615
617
|
return capReplayTimelineMarkers(markers);
|
|
616
618
|
}
|
|
617
619
|
|
|
620
|
+
export async function getSessionReplayTimeline(
|
|
621
|
+
recordingId: string,
|
|
622
|
+
scope: ReplayScope,
|
|
623
|
+
options: { eventLimit?: number } = {},
|
|
624
|
+
) {
|
|
625
|
+
const eventLimit = Math.min(
|
|
626
|
+
10_000,
|
|
627
|
+
Math.max(1, Math.floor(options.eventLimit ?? 10_000)),
|
|
628
|
+
);
|
|
629
|
+
const eventsResponse = await getSessionReplayEvents(recordingId, scope, {
|
|
630
|
+
limit: eventLimit,
|
|
631
|
+
});
|
|
632
|
+
const events = eventsResponse.chunks.flatMap((chunk) =>
|
|
633
|
+
chunk.events.filter(
|
|
634
|
+
(event): event is AgentReplayEvent =>
|
|
635
|
+
Boolean(event) && typeof event === "object",
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
const markers = buildReplayTimeline(events);
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
recording: compactSessionRecordingSummary(eventsResponse.recording),
|
|
642
|
+
markerCount: markers.length,
|
|
643
|
+
markers,
|
|
644
|
+
eventCount: eventsResponse.eventCount,
|
|
645
|
+
truncated: eventsResponse.truncated,
|
|
646
|
+
unavailableChunks: eventsResponse.unavailableChunks,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
618
650
|
export function verifySessionReplayAgentAccess(
|
|
619
651
|
recordingId: string,
|
|
620
652
|
token: string,
|
|
@@ -2,14 +2,18 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { appStateGet } from "@agent-native/core/application-state";
|
|
6
6
|
import {
|
|
7
7
|
deletePrivateBlob,
|
|
8
8
|
putPrivateBlob,
|
|
9
9
|
readPrivateBlob,
|
|
10
10
|
type PrivateBlobHandle,
|
|
11
11
|
} from "@agent-native/core/private-blob";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getRequestUserEmail,
|
|
14
|
+
recordChange,
|
|
15
|
+
runWithRequestContext,
|
|
16
|
+
} from "@agent-native/core/server";
|
|
13
17
|
import {
|
|
14
18
|
accessFilter,
|
|
15
19
|
resolveAccess,
|
|
@@ -301,10 +305,15 @@ function isBuilderEmail(value: unknown): boolean {
|
|
|
301
305
|
return normalizedEmail(value)?.endsWith("@builder.io") === true;
|
|
302
306
|
}
|
|
303
307
|
|
|
304
|
-
async function isSessionDemoModeEnabled(
|
|
308
|
+
async function isSessionDemoModeEnabled(
|
|
309
|
+
userEmail?: string | null,
|
|
310
|
+
): Promise<boolean> {
|
|
305
311
|
if (process.env.DEMO_MODE === "true") return true;
|
|
312
|
+
if (!userEmail) return false;
|
|
306
313
|
try {
|
|
307
|
-
|
|
314
|
+
// These reads also run from custom routes and actions, so use the
|
|
315
|
+
// authenticated scope explicitly instead of ambient request state.
|
|
316
|
+
const state = await appStateGet(userEmail, "demo-mode");
|
|
308
317
|
return state?.enabled === true;
|
|
309
318
|
} catch {
|
|
310
319
|
return false;
|
|
@@ -1677,7 +1686,7 @@ export async function listSessionRecordings(
|
|
|
1677
1686
|
filters: SessionReplayListFilters = {},
|
|
1678
1687
|
): Promise<SessionRecordingSummary[]> {
|
|
1679
1688
|
const db = getDb() as any;
|
|
1680
|
-
const demoMode = await isSessionDemoModeEnabled();
|
|
1689
|
+
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1681
1690
|
const limit = Math.min(
|
|
1682
1691
|
MAX_SESSION_RECORDINGS_LIMIT,
|
|
1683
1692
|
Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
|
|
@@ -1759,7 +1768,7 @@ export async function getSessionReplaySummary(
|
|
|
1759
1768
|
recordingId: string,
|
|
1760
1769
|
scope: SessionReplayScope,
|
|
1761
1770
|
): Promise<SessionRecordingSummary> {
|
|
1762
|
-
const demoMode = await isSessionDemoModeEnabled();
|
|
1771
|
+
const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
|
|
1763
1772
|
const access = await resolveAccess("session-recording", recordingId, {
|
|
1764
1773
|
userEmail: scope.userEmail,
|
|
1765
1774
|
orgId: scope.orgId ?? undefined,
|
|
@@ -1778,7 +1787,7 @@ export async function getSessionReplaySummary(
|
|
|
1778
1787
|
export async function getSessionReplayTokenizedSummary(
|
|
1779
1788
|
recordingId: string,
|
|
1780
1789
|
): Promise<SessionRecordingSummary> {
|
|
1781
|
-
const demoMode = await isSessionDemoModeEnabled();
|
|
1790
|
+
const demoMode = await isSessionDemoModeEnabled(getRequestUserEmail());
|
|
1782
1791
|
const db = getDb() as any;
|
|
1783
1792
|
// guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
|
|
1784
1793
|
const [row] = await db
|
|
@@ -258,6 +258,10 @@ export default createAgentChatPlugin({
|
|
|
258
258
|
// AGENT_PROD_CODE_EXECUTION=trusted to also enable bash/read/edit/write.
|
|
259
259
|
codeExecution: { production: "sandboxed" },
|
|
260
260
|
connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
|
|
261
|
+
externalAgents: {
|
|
262
|
+
authenticatedReads: "auto",
|
|
263
|
+
writes: "ask_app_only",
|
|
264
|
+
},
|
|
261
265
|
resolveOrgId: async (event) => {
|
|
262
266
|
const ctx = await getOrgContext(event);
|
|
263
267
|
return ctx.orgId;
|
|
@@ -1249,6 +1249,21 @@ const runAnalyticsMigrations = runMigrations(
|
|
|
1249
1249
|
sqlite: `ALTER TABLE monitor_incidents ADD COLUMN IF NOT EXISTS notification_delivered INTEGER NOT NULL DEFAULT 0`,
|
|
1250
1250
|
},
|
|
1251
1251
|
},
|
|
1252
|
+
{
|
|
1253
|
+
version: 118,
|
|
1254
|
+
name: "error-events-session-recording-filter-idx",
|
|
1255
|
+
sql: `CREATE INDEX IF NOT EXISTS error_events_session_recording_filter_idx ON error_events (session_recording_id, owner_email, org_id, issue_id)`,
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
version: 119,
|
|
1259
|
+
name: "error-events-user-id-filter-idx",
|
|
1260
|
+
sql: `CREATE INDEX IF NOT EXISTS error_events_user_id_filter_idx ON error_events (user_id, owner_email, org_id, issue_id)`,
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
version: 120,
|
|
1264
|
+
name: "error-events-user-key-filter-idx",
|
|
1265
|
+
sql: `CREATE INDEX IF NOT EXISTS error_events_user_key_filter_idx ON error_events (user_key, owner_email, org_id, issue_id)`,
|
|
1266
|
+
},
|
|
1252
1267
|
],
|
|
1253
1268
|
{ table: "analytics_migrations" },
|
|
1254
1269
|
);
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
|
|
182
182
|
.forms-ask-chat-panel [data-agent-empty-state="centered"] {
|
|
183
183
|
justify-content: center;
|
|
184
|
-
padding-bottom: clamp(
|
|
184
|
+
padding-bottom: clamp(2rem, 6vh, 4rem);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
.forms-ask-chat-panel [data-agent-empty-state="centered"] .agent-chat-scroll {
|
|
@@ -269,12 +269,28 @@
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
.forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro {
|
|
272
|
-
display:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
display: contents;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro h1,
|
|
276
|
+
.forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro p {
|
|
277
|
+
width: 100%;
|
|
277
278
|
text-align: center;
|
|
279
|
+
order: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro h1 {
|
|
283
|
+
margin: 0 0 0.5rem;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.forms-ask-chat-panel [data-agent-empty-state="centered"] .forms-chat-intro p {
|
|
287
|
+
margin: 0 auto 1rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.forms-ask-chat-panel
|
|
291
|
+
[data-agent-empty-state="centered"]
|
|
292
|
+
.agent-composer-stack {
|
|
293
|
+
order: 2;
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
.forms-chat-intro h1 {
|
|
@@ -297,10 +313,11 @@
|
|
|
297
313
|
flex-wrap: wrap;
|
|
298
314
|
justify-content: center;
|
|
299
315
|
gap: 0.5rem;
|
|
300
|
-
|
|
316
|
+
order: 3;
|
|
317
|
+
margin: 0.75rem auto 0;
|
|
301
318
|
}
|
|
302
319
|
|
|
303
|
-
.forms-chat-pill-row
|
|
320
|
+
.forms-chat-pill-row button {
|
|
304
321
|
display: inline-flex;
|
|
305
322
|
align-items: center;
|
|
306
323
|
gap: 0.375rem;
|
|
@@ -310,8 +327,37 @@
|
|
|
310
327
|
box-shadow: var(--forms-shadow-control);
|
|
311
328
|
padding: 0.375rem 0.625rem;
|
|
312
329
|
color: hsl(var(--muted-foreground));
|
|
330
|
+
cursor: pointer;
|
|
313
331
|
font-size: 0.75rem;
|
|
314
332
|
font-weight: 600;
|
|
333
|
+
transition-property: background-color, border-color, color, scale;
|
|
334
|
+
transition-duration: 150ms;
|
|
335
|
+
transition-timing-function: ease-out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.forms-chat-pill-row button:hover {
|
|
339
|
+
border-color: hsl(var(--foreground) / 0.2);
|
|
340
|
+
background: hsl(var(--background));
|
|
341
|
+
color: hsl(var(--foreground));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.forms-chat-pill-row button:focus-visible {
|
|
345
|
+
outline: none;
|
|
346
|
+
box-shadow: var(--forms-shadow-focus);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.forms-chat-pill-row button:active {
|
|
350
|
+
scale: 0.96;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@media (prefers-reduced-motion: reduce) {
|
|
354
|
+
.forms-chat-pill-row button {
|
|
355
|
+
transition: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.forms-chat-pill-row button:active {
|
|
359
|
+
scale: 1;
|
|
360
|
+
}
|
|
315
361
|
}
|
|
316
362
|
|
|
317
363
|
@media (min-width: 640px) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AgentChatSurface,
|
|
3
3
|
markAgentChatHomeHandoff,
|
|
4
|
+
sendToAgentChat,
|
|
4
5
|
useT,
|
|
5
6
|
} from "@agent-native/core/client";
|
|
6
7
|
import { IconChartBar, IconDatabase, IconSettings } from "@tabler/icons-react";
|
|
@@ -26,6 +27,28 @@ export function AskPage() {
|
|
|
26
27
|
};
|
|
27
28
|
}, []);
|
|
28
29
|
|
|
30
|
+
function prefillSuggestion(message: string) {
|
|
31
|
+
sendToAgentChat({ message, submit: false, chatTarget: "local" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const suggestions = [
|
|
35
|
+
{
|
|
36
|
+
label: t("home.pillForms"),
|
|
37
|
+
prompt: "@forms",
|
|
38
|
+
icon: IconDatabase,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: t("home.pillAnalytics"),
|
|
42
|
+
prompt: "analytics",
|
|
43
|
+
icon: IconChartBar,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: t("home.pillConfiguration"),
|
|
47
|
+
prompt: "configuration",
|
|
48
|
+
icon: IconSettings,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
29
52
|
return (
|
|
30
53
|
<div className="flex h-full min-h-0 flex-col bg-background">
|
|
31
54
|
<AgentChatSurface
|
|
@@ -48,19 +71,18 @@ export function AskPage() {
|
|
|
48
71
|
<div className="forms-chat-intro">
|
|
49
72
|
<h1>{t("home.heading")}</h1>
|
|
50
73
|
<p>{t("home.description")}</p>
|
|
51
|
-
<div className="forms-chat-pill-row"
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
</span>
|
|
74
|
+
<div className="forms-chat-pill-row">
|
|
75
|
+
{suggestions.map(({ icon: Icon, label, prompt }) => (
|
|
76
|
+
<button
|
|
77
|
+
key={prompt}
|
|
78
|
+
type="button"
|
|
79
|
+
className="forms-chat-pill"
|
|
80
|
+
onClick={() => prefillSuggestion(prompt)}
|
|
81
|
+
>
|
|
82
|
+
<Icon className="size-3.5" />
|
|
83
|
+
{label}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
64
86
|
</div>
|
|
65
87
|
</div>
|
|
66
88
|
}
|