@agent-native/core 0.101.4 → 0.101.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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +13 -0
  4. package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +2 -2
  5. package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +2 -2
  6. package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +2 -2
  7. package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +2 -2
  8. package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +2 -2
  9. package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +2 -2
  10. package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +2 -2
  11. package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +2 -2
  12. package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +2 -2
  13. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +2 -2
  14. package/corpus/core/docs/content/pure-agent-apps.mdx +58 -14
  15. package/corpus/core/package.json +1 -1
  16. package/corpus/core/src/action.ts +15 -0
  17. package/corpus/core/src/agent/production-agent.ts +215 -19
  18. package/corpus/core/src/coding-tools/run-code.ts +11 -5
  19. package/corpus/core/src/extensions/actions.ts +15 -0
  20. package/corpus/core/src/extensions/url-safety.ts +11 -1
  21. package/corpus/core/src/notifications/channels.ts +61 -30
  22. package/corpus/core/src/server/action-discovery.ts +4 -1
  23. package/corpus/core/src/server/agent-chat/context-tools.ts +4 -0
  24. package/corpus/templates/analytics/AGENTS.md +6 -0
  25. package/corpus/templates/analytics/README.md +1 -1
  26. package/corpus/templates/analytics/actions/data-source-status.ts +19 -1
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +5 -1
  28. package/corpus/templates/analytics/app/routes/_index.tsx +2 -2
  29. package/corpus/templates/analytics/changelog/2026-07-14-analytics-chat-guides-you-to-connect-a-data-source-with-a-di.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-07-14-analytics-landing-pages-now-clearly-position-the-app-as-an-o.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-07-15-daily-dashboard-emails-now-include-the-complete-dashboard-sc.md +6 -0
  32. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +8 -1
  33. package/corpus/templates/analytics/server/lib/dashboard-report.ts +64 -12
  34. package/corpus/templates/analytics/server/lib/real-data-actions.ts +15 -3
  35. package/corpus/templates/analytics/server/plugins/agent-chat.ts +364 -4
  36. package/corpus/templates/clips/app/components/dictate/vocabulary-section.tsx +1 -0
  37. package/corpus/templates/clips/app/components/library/folder-tree.tsx +12 -9
  38. package/corpus/templates/clips/app/components/library/library-layout.tsx +19 -16
  39. package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +13 -9
  40. package/corpus/templates/clips/changelog/2026-07-15-folders-and-sidebar-links-are-clickable-again-immediately-af.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-15-hovering-a-truncated-folder-name-now-shows-the-full-name.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-15-space-creation-controls-are-now-shown-only-to-organization-a.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-15-vocabulary-terms-can-now-be-deleted-from-the-dictation-dicti.md +6 -0
  44. package/corpus/templates/clips/desktop/src/styles.css +23 -1
  45. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +49 -7
  46. package/corpus/templates/clips/server/lib/recordings.ts +5 -2
  47. package/dist/action.d.ts +12 -0
  48. package/dist/action.d.ts.map +1 -1
  49. package/dist/action.js +2 -0
  50. package/dist/action.js.map +1 -1
  51. package/dist/agent/production-agent.d.ts +24 -0
  52. package/dist/agent/production-agent.d.ts.map +1 -1
  53. package/dist/agent/production-agent.js +175 -18
  54. package/dist/agent/production-agent.js.map +1 -1
  55. package/dist/coding-tools/run-code.d.ts +5 -2
  56. package/dist/coding-tools/run-code.d.ts.map +1 -1
  57. package/dist/coding-tools/run-code.js +11 -5
  58. package/dist/coding-tools/run-code.js.map +1 -1
  59. package/dist/extensions/actions.d.ts.map +1 -1
  60. package/dist/extensions/actions.js +14 -0
  61. package/dist/extensions/actions.js.map +1 -1
  62. package/dist/extensions/url-safety.d.ts +6 -0
  63. package/dist/extensions/url-safety.d.ts.map +1 -1
  64. package/dist/extensions/url-safety.js +6 -0
  65. package/dist/extensions/url-safety.js.map +1 -1
  66. package/dist/notifications/channels.d.ts.map +1 -1
  67. package/dist/notifications/channels.js +38 -22
  68. package/dist/notifications/channels.js.map +1 -1
  69. package/dist/progress/routes.d.ts +1 -1
  70. package/dist/server/action-discovery.d.ts.map +1 -1
  71. package/dist/server/action-discovery.js +4 -1
  72. package/dist/server/action-discovery.js.map +1 -1
  73. package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
  74. package/dist/server/agent-chat/context-tools.js +4 -0
  75. package/dist/server/agent-chat/context-tools.js.map +1 -1
  76. package/docs/content/locales/ar-SA/pure-agent-apps.mdx +2 -2
  77. package/docs/content/locales/de-DE/pure-agent-apps.mdx +2 -2
  78. package/docs/content/locales/es-ES/pure-agent-apps.mdx +2 -2
  79. package/docs/content/locales/fr-FR/pure-agent-apps.mdx +2 -2
  80. package/docs/content/locales/hi-IN/pure-agent-apps.mdx +2 -2
  81. package/docs/content/locales/ja-JP/pure-agent-apps.mdx +2 -2
  82. package/docs/content/locales/ko-KR/pure-agent-apps.mdx +2 -2
  83. package/docs/content/locales/pt-BR/pure-agent-apps.mdx +2 -2
  84. package/docs/content/locales/zh-CN/pure-agent-apps.mdx +2 -2
  85. package/docs/content/locales/zh-TW/pure-agent-apps.mdx +2 -2
  86. package/docs/content/pure-agent-apps.mdx +58 -14
  87. package/package.json +1 -1
@@ -175,7 +175,7 @@ function looksLikeWorkflowOrAutomationRequest(lower: string): boolean {
175
175
  }
176
176
 
177
177
  const ANALYTICS_RESULT_TERMS =
178
- /\b(conversion|conversions|funnel|revenue|traffic|pageviews?|signups?|events?|active users?|sessions?|retention|churn|pipeline|deals?|calls?|transcripts?|sentiment|themes?|objections?|cohorts?|segments?|accounts?|customers?|tickets?|issues?|leads?|opportunities|mrr|arr|ctr|cvr|cac|ltv)\b/;
178
+ /\b(conversion|conversions|funnel|revenue|payment|payments|traffic|pageviews?|signups?|events?|active users?|sessions?|retention|churn|pipeline|deals?|calls?|transcripts?|sentiment|themes?|objections?|cohorts?|segments?|accounts?|customers?|tickets?|issues?|leads?|opportunities|mrr|arr|ctr|cvr|cac|ltv)\b/;
179
179
 
180
180
  const ANALYTICS_INTENT_TERMS =
181
181
  /\b(analy[sz]e|measure|calculate|query|report|summari[sz]e|break ?down|compare|rank|segment|forecast|trend|count|total|average|median|percent(?:age)?|rate|top|bottom|highest|lowest|how many|how much|what (?:is|are|was|were)|which|why)\b/;
@@ -183,6 +183,11 @@ const ANALYTICS_INTENT_TERMS =
183
183
  const SOURCE_SEARCH_INTENT_TERMS =
184
184
  /\b(find|surface|search|scan|grep|review|inspect|check|look through|go find)\b/;
185
185
 
186
+ const SETUP_REQUEST_TERMS =
187
+ /\b(connect|configure|configuration|settings?|setup|set up|credentials?|authenticate|authorization)\b/;
188
+ const SETUP_REQUEST_FRAMING =
189
+ /\b(?:how (?:do|can) i|can you|help me|where can i|show me how)\b/;
190
+
186
191
  const ARTIFACT_TERMS = /\b(analysis|dashboard|panel|chart|metric|metrics)\b/;
187
192
 
188
193
  const ARTIFACT_DATA_INTENT =
@@ -199,6 +204,12 @@ export function looksLikeAnalyticsDataRequest(text: string): boolean {
199
204
  const lower = requestText.toLowerCase();
200
205
  if (!lower) return false;
201
206
  if (lower.includes(REAL_DATA_REQUIRED_MARKER.toLowerCase())) return true;
207
+ if (
208
+ SETUP_REQUEST_TERMS.test(lower) &&
209
+ (SETUP_REQUEST_FRAMING.test(lower) || /\bsettings?\b/.test(lower))
210
+ ) {
211
+ return false;
212
+ }
202
213
  if (looksLikeWorkflowOrAutomationRequest(lower)) return false;
203
214
  if (
204
215
  /\b(open|navigate|go to|rename|delete|share|favorite|unfavorite)\b/.test(
@@ -216,7 +227,8 @@ export function looksLikeAnalyticsDataRequest(text: string): boolean {
216
227
  }
217
228
  if (
218
229
  /\b(integration|connect|configure|settings)\b/.test(lower) &&
219
- !ANALYTICS_RESULT_TERMS.test(lower)
230
+ !ANALYTICS_INTENT_TERMS.test(lower) &&
231
+ !SOURCE_SEARCH_INTENT_TERMS.test(lower)
220
232
  ) {
221
233
  return false;
222
234
  }
@@ -263,7 +275,7 @@ export function isGenericNoDataFallback(text: string): boolean {
263
275
  }
264
276
 
265
277
  const SAFE_NO_DATA_RESPONSE =
266
- /\b(?:i can't|i cannot|can't retrieve|cannot retrieve|couldn't retrieve|unable to retrieve|don't have access|do not have access|not configured|missing credentials?|need (?:a|the)? ?data source|need to know which source|which source|which data source|clarify|can you|once (?:that'?s|it is) (?:connected|configured|available)|no data source|without a successful|query failed|source query failed|sql failed|error running|before (?:i|we) can (?:calculate|report|answer|analyze)|i need to query)\b/i;
278
+ /\b(?:i can't|i cannot|can't retrieve|cannot retrieve|couldn't retrieve|unable to retrieve|don't have access|do not have access|not configured|not connected|missing credentials?|need (?:a|the)? ?data source|need to know which source|which source|which data source|clarify|can you|once (?:that'?s|it is) (?:connected|configured|available)|no data source|without a successful|query failed|source query failed|sql failed|error running|before (?:i|we) can (?:calculate|report|answer|analyze)|i need to query)\b/i;
267
279
 
268
280
  export function isSafeNoDataAnalyticsResponse(text: string): boolean {
269
281
  const trimmed = text.trim();
@@ -1,6 +1,7 @@
1
1
  import { getOrgContext } from "@agent-native/core/org";
2
2
  import {
3
3
  createAgentChatPlugin,
4
+ buildDeepLink,
4
5
  loadActionsFromStaticRegistry,
5
6
  type AgentLoopFinalResponseGuardContext,
6
7
  } from "@agent-native/core/server";
@@ -11,9 +12,9 @@ import {
11
12
  INITIAL_TOOL_NAMES,
12
13
  } from "../lib/agent-chat-plan-mode";
13
14
  import { ANALYTICS_CONNECTOR_CATALOG } from "../lib/analytics-connector-catalog";
15
+ import { credentialProviderConfigs } from "../lib/credential-keys";
14
16
  import {
15
17
  failedDataQueryAttemptMessage,
16
- GENERIC_NO_DATA_FALLBACK_MESSAGE,
17
18
  hasExplicitPartialDisclosure,
18
19
  hasFailedCorpusWorkflowEvidence,
19
20
  hasDataQueryAttempt,
@@ -30,6 +31,12 @@ import {
30
31
 
31
32
  const ANALYTICS_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
32
33
 
34
+ const ANALYTICS_DATA_SOURCES_LINK = buildDeepLink({
35
+ app: "analytics",
36
+ view: "data-sources",
37
+ to: "/data-sources",
38
+ });
39
+
33
40
  export const SIMPLE_TIME_BOUNDED_METRIC_FAST_PATH_GUIDANCE =
34
41
  "SIMPLE TIME-BOUNDED METRIC FAST PATH — When the data dictionary or a known canonical source identifies the metric, run one bounded aggregate. Once it returns a valid result, answer the explicit question immediately with the source, time window, row count, and only necessary caveats. Do not schema-discover, retry, enrich, cross-check, or add breakdowns after that successful result unless the query failed or the result conflicts with the known metric definition. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
35
42
 
@@ -62,6 +69,7 @@ export function analyticsSourceGuidanceOpening(): string {
62
69
  SIMPLE_TIME_BOUNDED_METRIC_FAST_PATH_GUIDANCE +
63
70
  BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
64
71
  ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
72
+ `DATA-SOURCE SETUP UX — Chat remains available when no external data source is connected. For a live-data request that needs an unavailable external provider, explain what is missing in the context of the user's question and guide them naturally to [Connect data sources](${ANALYTICS_DATA_SOURCES_LINK}). Use that real link from the app; do not emit a generic canned no-data sentence. For general conversation, conceptual questions, and questions the built-in first-party source can answer, continue helping normally. ` +
65
73
  "SURFACE DIFFERENTIATION — You are the analytics assistant for definitions, deep-dive analysis, and action. For questions about what a metric, model, or table means, use the Data Dictionary and configured schema tools first. For trends, comparisons, anomalies, current data, or anything that requires querying live data, answer directly in chat with the relevant provider query, dashboard analysis, and inline charts when useful. "
66
74
  );
67
75
  }
@@ -145,6 +153,279 @@ function configuredDataSourceLabels(
145
153
  return [...labels];
146
154
  }
147
155
 
156
+ interface DataSourceStatusSummary {
157
+ checked: boolean;
158
+ externalSourceLabels: string[];
159
+ availableExternalSources: Array<{
160
+ aliases: string[];
161
+ configured: boolean;
162
+ }>;
163
+ setupLink: string;
164
+ }
165
+
166
+ const GENERIC_EXTERNAL_SOURCE_REQUEST_TERMS = /\b(warehouse|crm|payments?)\b/i;
167
+
168
+ const EXTERNAL_SOURCE_PROVIDER_ALIASES = [
169
+ ...credentialProviderConfigs.map(({ provider, label }) => ({
170
+ terms: [provider, label],
171
+ aliases: [provider, label],
172
+ })),
173
+ { terms: ["ga4"], aliases: ["ga4", "google analytics"] },
174
+ { terms: ["twitter/x", "x/twitter"], aliases: ["twitter", "x/twitter"] },
175
+ ];
176
+
177
+ function looksLikeExternalSourceRequest(userText: string): boolean {
178
+ return (
179
+ GENERIC_EXTERNAL_SOURCE_REQUEST_TERMS.test(userText) ||
180
+ EXTERNAL_SOURCE_PROVIDER_ALIASES.some(({ terms }) =>
181
+ terms.some((term) => containsNormalizedPhrase(userText, term)),
182
+ )
183
+ );
184
+ }
185
+
186
+ function normalizeSourceLabel(value: string): string {
187
+ return value
188
+ .toLowerCase()
189
+ .replace(/[^a-z0-9]+/g, " ")
190
+ .trim();
191
+ }
192
+
193
+ function containsNormalizedPhrase(text: string, phrase: string): boolean {
194
+ const normalizedText = normalizeSourceLabel(text);
195
+ const normalizedPhrase = normalizeSourceLabel(phrase);
196
+ return Boolean(
197
+ normalizedPhrase &&
198
+ (normalizedText === normalizedPhrase ||
199
+ normalizedText.startsWith(`${normalizedPhrase} `) ||
200
+ normalizedText.endsWith(` ${normalizedPhrase}`) ||
201
+ normalizedText.includes(` ${normalizedPhrase} `)),
202
+ );
203
+ }
204
+
205
+ function sourceAliasesOverlap(left: string[], right: string[]): boolean {
206
+ return left.some((leftAlias) =>
207
+ right.some((rightAlias) => {
208
+ const normalizedLeft = normalizeSourceLabel(leftAlias);
209
+ const normalizedRight = normalizeSourceLabel(rightAlias);
210
+ return (
211
+ normalizedLeft === normalizedRight ||
212
+ normalizedLeft.startsWith(`${normalizedRight} `) ||
213
+ normalizedLeft.endsWith(` ${normalizedRight}`) ||
214
+ normalizedLeft.includes(` ${normalizedRight} `) ||
215
+ normalizedRight.startsWith(`${normalizedLeft} `) ||
216
+ normalizedRight.endsWith(` ${normalizedLeft}`) ||
217
+ normalizedRight.includes(` ${normalizedLeft} `)
218
+ );
219
+ }),
220
+ );
221
+ }
222
+
223
+ function hasMissingRequestedExternalSource(
224
+ userText: string,
225
+ configuredSourceLabels: string[],
226
+ availableExternalSources: DataSourceStatusSummary["availableExternalSources"] = [],
227
+ ): boolean {
228
+ const configuredAliases = [
229
+ ...configuredSourceLabels.map((label) => [label]),
230
+ ...availableExternalSources
231
+ .filter(({ configured }) => configured)
232
+ .map(({ aliases }) => aliases),
233
+ ];
234
+ const sourceAliases = [
235
+ ...EXTERNAL_SOURCE_PROVIDER_ALIASES,
236
+ ...availableExternalSources.map(({ aliases }) => ({
237
+ terms: aliases,
238
+ aliases,
239
+ })),
240
+ ];
241
+ return sourceAliases
242
+ .filter(({ terms }) =>
243
+ terms.some((term) => containsNormalizedPhrase(userText, term)),
244
+ )
245
+ .some(
246
+ ({ aliases }) =>
247
+ !configuredAliases.some((configured) =>
248
+ sourceAliasesOverlap(configured, aliases),
249
+ ),
250
+ );
251
+ }
252
+
253
+ function dataSourceStatusSummary(
254
+ toolResults: AgentLoopFinalResponseGuardContext["toolResults"],
255
+ ): DataSourceStatusSummary {
256
+ const externalSourceLabels = new Set<string>();
257
+ const availableExternalSources = new Map<
258
+ string,
259
+ { aliases: string[]; configured: boolean }
260
+ >();
261
+ let checked = false;
262
+ let setupLink = ANALYTICS_DATA_SOURCES_LINK;
263
+
264
+ const addAvailableExternalSource = (
265
+ provider: unknown,
266
+ label: unknown,
267
+ configured: boolean,
268
+ ) => {
269
+ const aliases = [provider, label]
270
+ .filter((value): value is string => typeof value === "string")
271
+ .map((value) => value.trim())
272
+ .filter(Boolean);
273
+ const key = normalizeSourceLabel(aliases[0] ?? aliases[1] ?? "");
274
+ if (!key) return;
275
+ const existing = availableExternalSources.get(key);
276
+ if (existing) {
277
+ existing.configured ||= configured;
278
+ existing.aliases = [...new Set([...existing.aliases, ...aliases])];
279
+ return;
280
+ }
281
+ availableExternalSources.set(key, { aliases, configured });
282
+ };
283
+
284
+ for (const result of toolResults ?? []) {
285
+ const normalizedName = String(result.name ?? "")
286
+ .trim()
287
+ .toLowerCase()
288
+ .replace(/[\s_]+/g, "-");
289
+ if (normalizedName !== "data-source-status" || result.isError) continue;
290
+
291
+ checked = true;
292
+ let parsed: Record<string, unknown>;
293
+ try {
294
+ const value = JSON.parse(String(result.content ?? ""));
295
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
296
+ continue;
297
+ }
298
+ parsed = value as Record<string, unknown>;
299
+ } catch {
300
+ continue;
301
+ }
302
+
303
+ let foundSetupLink = false;
304
+ for (const candidate of [
305
+ parsed.dataSourcesSetupLink,
306
+ parsed.dataSourcesLink,
307
+ parsed.setupLink,
308
+ ]) {
309
+ const url =
310
+ typeof candidate === "string"
311
+ ? candidate
312
+ : candidate &&
313
+ typeof candidate === "object" &&
314
+ !Array.isArray(candidate)
315
+ ? (candidate as Record<string, unknown>).url
316
+ : undefined;
317
+ if (typeof url === "string" && url.trim()) {
318
+ setupLink = url.trim();
319
+ foundSetupLink = true;
320
+ break;
321
+ }
322
+ }
323
+ if (
324
+ !foundSetupLink &&
325
+ typeof parsed.settingsPath === "string" &&
326
+ parsed.settingsPath.trim()
327
+ ) {
328
+ setupLink = parsed.settingsPath.trim();
329
+ }
330
+
331
+ const compactSources = Array.isArray(parsed.configuredDataSources)
332
+ ? parsed.configuredDataSources
333
+ : [];
334
+ for (const source of compactSources) {
335
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
336
+ continue;
337
+ }
338
+ const record = source as Record<string, unknown>;
339
+ const provider = String(record.provider ?? "")
340
+ .trim()
341
+ .toLowerCase();
342
+ const via = String(record.via ?? "")
343
+ .trim()
344
+ .toLowerCase();
345
+ if (provider === "first-party" || via === "built-in") continue;
346
+ const label = record.label ?? record.provider;
347
+ if (typeof label === "string" && label.trim()) {
348
+ externalSourceLabels.add(label.trim());
349
+ }
350
+ addAvailableExternalSource(record.provider, label, true);
351
+ }
352
+
353
+ // Backward compatibility for status responses that predate the compact
354
+ // configuredDataSources summary.
355
+ const providers = Array.isArray(parsed.providers) ? parsed.providers : [];
356
+ for (const provider of providers) {
357
+ if (
358
+ !provider ||
359
+ typeof provider !== "object" ||
360
+ Array.isArray(provider)
361
+ ) {
362
+ continue;
363
+ }
364
+ const record = provider as Record<string, unknown>;
365
+ const providerId = String(record.provider ?? "")
366
+ .trim()
367
+ .toLowerCase();
368
+ if (providerId === "first-party") continue;
369
+ const label = record.label ?? record.provider;
370
+ addAvailableExternalSource(
371
+ record.provider,
372
+ label,
373
+ record.configured === true,
374
+ );
375
+ if (record.configured !== true) continue;
376
+ if (typeof label === "string" && label.trim()) {
377
+ externalSourceLabels.add(label.trim());
378
+ }
379
+ }
380
+
381
+ const workspaceConnections =
382
+ parsed.workspaceConnections &&
383
+ typeof parsed.workspaceConnections === "object" &&
384
+ !Array.isArray(parsed.workspaceConnections)
385
+ ? (parsed.workspaceConnections as Record<string, unknown>)
386
+ : null;
387
+ const workspaceProviders = Array.isArray(workspaceConnections?.providers)
388
+ ? workspaceConnections.providers
389
+ : [];
390
+ for (const provider of workspaceProviders) {
391
+ if (
392
+ !provider ||
393
+ typeof provider !== "object" ||
394
+ Array.isArray(provider)
395
+ ) {
396
+ continue;
397
+ }
398
+ const record = provider as Record<string, unknown>;
399
+ const providerId = record.id ?? record.provider;
400
+ const label = record.label ?? providerId;
401
+ const configured =
402
+ record.configured === true || record.grantState === "connected";
403
+ addAvailableExternalSource(providerId, label, configured);
404
+ if (configured && typeof label === "string" && label.trim()) {
405
+ externalSourceLabels.add(label.trim());
406
+ }
407
+ }
408
+ }
409
+
410
+ return {
411
+ checked,
412
+ externalSourceLabels: [...externalSourceLabels],
413
+ availableExternalSources: [...availableExternalSources.values()],
414
+ setupLink,
415
+ };
416
+ }
417
+
418
+ function includesDataSourcesLink(text: string, setupLink: string): boolean {
419
+ const normalizedSetupLink = setupLink.trim().replace(/&amp;/g, "&");
420
+ if (!normalizedSetupLink) return false;
421
+ const normalizedText = text.replace(/&amp;/g, "&");
422
+ const linkPattern = /\[[^\]]+\]\((<[^>]+>|[^\s)]+)(?:\s+["'][^)]*["'])?\)/g;
423
+ return [...normalizedText.matchAll(linkPattern)].some((match) => {
424
+ const destination = match[1]?.replace(/^<|>$/g, "");
425
+ return destination === normalizedSetupLink;
426
+ });
427
+ }
428
+
148
429
  export function realDataFinalGuard(
149
430
  context: AgentLoopFinalResponseGuardContext,
150
431
  ) {
@@ -172,6 +453,22 @@ export function realDataFinalGuard(
172
453
  return null;
173
454
  }
174
455
  const incompleteEvidence = hasIncompleteDataEvidence(context.toolResults);
456
+ const dataQueryAttempted = hasDataQueryAttempt(context.toolResults);
457
+ const sourceStatus = dataSourceStatusSummary(context.toolResults);
458
+ const noConnectedExternalSources =
459
+ sourceStatus.checked && sourceStatus.externalSourceLabels.length === 0;
460
+ const externalSourceRequest = looksLikeExternalSourceRequest(userText);
461
+ const missingRequestedExternalSource = hasMissingRequestedExternalSource(
462
+ userText,
463
+ sourceStatus.externalSourceLabels,
464
+ sourceStatus.availableExternalSources,
465
+ );
466
+ const firstPartySourceShouldBeTried =
467
+ noConnectedExternalSources && !externalSourceRequest;
468
+ const needsDataSourceLink =
469
+ !sourceStatus.checked ||
470
+ (externalSourceRequest &&
471
+ (noConnectedExternalSources || missingRequestedExternalSource));
175
472
  if (
176
473
  hasFailedCorpusWorkflowEvidence(context.toolResults) &&
177
474
  looksLikeCoverageSensitiveAnalyticsRequest(userText) &&
@@ -225,17 +522,80 @@ export function realDataFinalGuard(
225
522
  "I can't make a confident exhaustive analytics claim yet because part of the source evidence was aborted, truncated, or still paginated. I need to recover the missing coverage or state the answer as partial with the inspected sample size.",
226
523
  };
227
524
  }
228
- if (hasDataQueryAttempt(context.toolResults)) return null;
229
- if (isSafeNoDataAnalyticsResponse(context.text)) return null;
525
+ if (dataQueryAttempted) return null;
526
+ if (isSafeNoDataAnalyticsResponse(context.text)) {
527
+ if (firstPartySourceShouldBeTried) {
528
+ return {
529
+ retryMessage:
530
+ "The built-in first-party Analytics source is available even though no external provider is connected. Use `query-agent-native-analytics` for the user's first-party product, usage, conversion, or observability question before explaining that data is unavailable. Only guide the user to connect a source if the request specifically needs an external provider.",
531
+ fallbackMessage:
532
+ "I couldn't complete a grounded first-party Analytics query yet. Please retry and I'll use the built-in Analytics source before asking you to connect an external provider.",
533
+ maxRetries: 2,
534
+ expandToolSurface: true,
535
+ };
536
+ }
537
+ if (
538
+ needsDataSourceLink &&
539
+ !includesDataSourcesLink(context.text, sourceStatus.setupLink)
540
+ ) {
541
+ return {
542
+ retryMessage: `The response correctly explains that the requested live data is unavailable, but it needs a contextual next step. Explain which external source is missing, keep the conversation open, and include this exact markdown link: [Connect data sources](${sourceStatus.setupLink}). Do not use the generic no-grounded-data fallback.`,
543
+ fallbackMessage: `I can help with that once the relevant source is connected. [Connect data sources](${sourceStatus.setupLink})`,
544
+ maxRetries: 2,
545
+ };
546
+ }
547
+ return null;
548
+ }
230
549
  const failedQueryMessage = failedDataQueryAttemptMessage(context.toolResults);
231
550
  if (failedQueryMessage) {
551
+ if (
552
+ needsDataSourceLink &&
553
+ !includesDataSourcesLink(context.text, sourceStatus.setupLink)
554
+ ) {
555
+ return {
556
+ retryMessage: `${failedQueryMessage} Explain which external source is missing and include this exact markdown link: [Connect data sources](${sourceStatus.setupLink}).`,
557
+ fallbackMessage: `${failedQueryMessage} [Connect data sources](${sourceStatus.setupLink})`,
558
+ maxRetries: 2,
559
+ };
560
+ }
232
561
  return {
233
562
  retryMessage: failedQueryMessage,
234
563
  fallbackMessage: failedQueryMessage,
235
564
  };
236
565
  }
237
566
 
567
+ if (
568
+ needsDataSourceLink &&
569
+ (missingRequestedExternalSource ||
570
+ (noConnectedExternalSources && externalSourceRequest))
571
+ ) {
572
+ return {
573
+ retryMessage: `The requested external source is not connected. Explain what is missing in the context of the user's question and include this exact markdown link: [Connect data sources](${sourceStatus.setupLink}). Do not use the generic no-grounded-data fallback.`,
574
+ fallbackMessage: `I can help with that once the relevant source is connected. [Connect data sources](${sourceStatus.setupLink})`,
575
+ maxRetries: 2,
576
+ expandToolSurface: true,
577
+ };
578
+ }
579
+
238
580
  const configuredSources = configuredDataSourceLabels(context.toolResults);
581
+ if (firstPartySourceShouldBeTried) {
582
+ return {
583
+ retryMessage:
584
+ "The user asked for live analytics, and the built-in first-party Analytics source is available even though no external provider is connected. Call `query-agent-native-analytics` for first-party product, usage, conversion, or observability data and answer from that result. If the request specifically names an external provider, explain what is missing and include the real Connect data sources link.",
585
+ fallbackMessage:
586
+ "I couldn't complete a grounded first-party Analytics query yet. Please retry and I'll use the built-in Analytics source before asking you to connect an external provider.",
587
+ maxRetries: 2,
588
+ expandToolSurface: true,
589
+ };
590
+ }
591
+ if (noConnectedExternalSources) {
592
+ return {
593
+ retryMessage: `The user asked for live analytics, but data-source-status found no connected external providers. The built-in first-party source is still available for first-party Analytics data. If this request needs an external source, respond naturally in the context of the user's question, explain what is missing, and include [Connect data sources](${sourceStatus.setupLink}). Do not use a generic canned no-data response.`,
594
+ fallbackMessage: `I can help with that once the relevant source is connected. [Connect data sources](${sourceStatus.setupLink})`,
595
+ maxRetries: 2,
596
+ expandToolSurface: true,
597
+ };
598
+ }
239
599
  const configuredSourceGuidance = configuredSources.length
240
600
  ? ` \`data-source-status\` already confirmed these connected sources: ${configuredSources.join(", ")}. Do not claim that no sources are connected and do not ask the user to reconnect them. Immediately call the relevant query action for one of those sources.`
241
601
  : "";
@@ -246,7 +606,7 @@ export function realDataFinalGuard(
246
606
  " If the right response is a clarification, plan, or explicit unavailable/credentials-missing message with no metrics or source-record claims, finalize that directly instead.",
247
607
  fallbackMessage: configuredSources.length
248
608
  ? `I found connected data sources (${configuredSources.join(", ")}), but the model still did not run a real source query. Please retry the request; you do not need to reconnect those sources.`
249
- : GENERIC_NO_DATA_FALLBACK_MESSAGE,
609
+ : `I couldn't complete a grounded answer to that request. If the relevant provider isn't connected, [connect data sources](${ANALYTICS_DATA_SOURCES_LINK}) and I'll try again with real data.`,
250
610
  // Some models use separate turns for status, schema discovery, and the
251
611
  // actual query. One corrective turn was enough for Sonnet but caused Luna
252
612
  // to hit the fallback before it reached the query.
@@ -66,6 +66,7 @@ export function VocabularySection() {
66
66
  const removeTerm = useActionMutation<unknown, { id: string }>(
67
67
  "remove-vocabulary-term",
68
68
  {
69
+ method: "DELETE",
69
70
  onMutate: async (vars) => {
70
71
  await queryClient.cancelQueries({ queryKey: QUERY_KEY });
71
72
  const prev = queryClient.getQueryData(QUERY_KEY);
@@ -182,23 +182,29 @@ function FolderItem({
182
182
  ) : (
183
183
  <IconFolder className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
184
184
  )}
185
- <span className="truncate">{node.name}</span>
185
+ <span className="truncate" title={node.name}>
186
+ {node.name}
187
+ </span>
186
188
  </NavLink>
187
189
  </div>
188
190
  </ContextMenuTrigger>
189
191
  <ContextMenuContent>
190
192
  <ContextMenuItem
191
193
  onSelect={() => {
192
- setRenameValue(node.name);
193
- setRenameOpen(true);
194
+ setTimeout(() => {
195
+ setRenameValue(node.name);
196
+ setRenameOpen(true);
197
+ }, 0);
194
198
  }}
195
199
  >
196
200
  <IconEdit className="h-3.5 w-3.5 me-2" /> {t("folderTree.rename")}
197
201
  </ContextMenuItem>
198
202
  <ContextMenuItem
199
203
  onSelect={() => {
200
- setNewValue("");
201
- setNewOpen(true);
204
+ setTimeout(() => {
205
+ setNewValue("");
206
+ setNewOpen(true);
207
+ }, 0);
202
208
  }}
203
209
  >
204
210
  <IconFolderPlus className="h-3.5 w-3.5 me-2" />{" "}
@@ -206,7 +212,7 @@ function FolderItem({
206
212
  </ContextMenuItem>
207
213
  <ContextMenuSeparator />
208
214
  <ContextMenuItem
209
- onSelect={() => setConfirmDelete(true)}
215
+ onSelect={() => setTimeout(() => setConfirmDelete(true), 0)}
210
216
  className="text-destructive"
211
217
  >
212
218
  <IconTrash className="h-3.5 w-3.5 me-2" />{" "}
@@ -263,7 +269,6 @@ function FolderItem({
263
269
  ),
264
270
  },
265
271
  );
266
- setRenameOpen(false);
267
272
  }}
268
273
  >
269
274
  {t("common.save")}
@@ -311,7 +316,6 @@ function FolderItem({
311
316
  ),
312
317
  },
313
318
  );
314
- setNewOpen(false);
315
319
  }}
316
320
  >
317
321
  {t("common.create")}
@@ -346,7 +350,6 @@ function FolderItem({
346
350
  ),
347
351
  },
348
352
  );
349
- setConfirmDelete(false);
350
353
  }}
351
354
  >
352
355
  {t("folderTree.delete")}
@@ -11,7 +11,7 @@ import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
11
11
  import {
12
12
  InvitationBanner,
13
13
  OrgSwitcher,
14
- useOrg,
14
+ useOrgRole,
15
15
  } from "@agent-native/core/client/org";
16
16
  import {
17
17
  IconInbox,
@@ -115,7 +115,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
115
115
  const { shouldShowPromo, shouldShowSidebarLink, dismiss } = useDesktopPromo();
116
116
  usePrefetchVideoStorageStatus();
117
117
 
118
- const { data: org } = useOrg();
118
+ const { org, canManageOrg } = useOrgRole();
119
119
  const hasActiveOrg = Boolean(org?.orgId);
120
120
  const { data: organizations } = useOrganizations({ enabled: hasActiveOrg });
121
121
  const currentOrganizationId =
@@ -466,19 +466,23 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
466
466
  <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
467
467
  {t("navigation.spaces")}
468
468
  </span>
469
- <Tooltip>
470
- <TooltipTrigger asChild>
471
- <button
472
- type="button"
473
- aria-label={t("navigation.spaces")}
474
- className="rounded p-1 text-muted-foreground hover:bg-accent"
475
- onClick={() => setNewSpaceOpen(true)}
476
- >
477
- <IconPlus className="h-3.5 w-3.5" />
478
- </button>
479
- </TooltipTrigger>
480
- <TooltipContent>{t("navigation.spaces")}</TooltipContent>
481
- </Tooltip>
469
+ {canManageOrg && (
470
+ <Tooltip>
471
+ <TooltipTrigger asChild>
472
+ <button
473
+ type="button"
474
+ aria-label={t("navigation.spaces")}
475
+ className="rounded p-1 text-muted-foreground hover:bg-accent"
476
+ onClick={() => setNewSpaceOpen(true)}
477
+ >
478
+ <IconPlus className="h-3.5 w-3.5" />
479
+ </button>
480
+ </TooltipTrigger>
481
+ <TooltipContent>
482
+ {t("navigation.spaces")}
483
+ </TooltipContent>
484
+ </Tooltip>
485
+ )}
482
486
  </div>
483
487
  <ul className="space-y-0.5">
484
488
  {(spaces?.spaces ?? []).map((s: any) => {
@@ -661,7 +665,6 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
661
665
  },
662
666
  );
663
667
  setNewFolderName("");
664
- setNewFolderOpen(false);
665
668
  }}
666
669
  >
667
670
  {t("common.create")}
@@ -1,4 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
+ import { useOrgRole } from "@agent-native/core/client/org";
2
3
  import { IconPlus, IconUsersGroup } from "@tabler/icons-react";
3
4
  import { useState } from "react";
4
5
 
@@ -29,6 +30,7 @@ function Skeleton() {
29
30
  export default function SpacesIndexRoute() {
30
31
  const t = useT();
31
32
  const [createOpen, setCreateOpen] = useState(false);
33
+ const { canManageOrg } = useOrgRole();
32
34
  const { data: organizations } = useOrganizations();
33
35
  const currentOrganizationId =
34
36
  organizations?.currentId ?? organizations?.organizations?.[0]?.id;
@@ -53,15 +55,17 @@ export default function SpacesIndexRoute() {
53
55
  {t("navigation.spaces")}
54
56
  </h1>
55
57
  </div>
56
- <div className="ml-auto">
57
- <Button
58
- size="sm"
59
- className="gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
60
- onClick={() => setCreateOpen(true)}
61
- >
62
- <IconPlus className="h-4 w-4" /> {t("createSpaceDialog.newSpace")}
63
- </Button>
64
- </div>
58
+ {canManageOrg && (
59
+ <div className="ml-auto">
60
+ <Button
61
+ size="sm"
62
+ className="gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
63
+ onClick={() => setCreateOpen(true)}
64
+ >
65
+ <IconPlus className="h-4 w-4" /> {t("createSpaceDialog.newSpace")}
66
+ </Button>
67
+ </div>
68
+ )}
65
69
  </PageHeader>
66
70
 
67
71
  <div className="flex-1 overflow-y-auto p-5">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-15
4
+ ---
5
+
6
+ Folders and sidebar links are clickable again immediately after creating or renaming a folder.