@agent-native/core 0.153.10 → 0.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +20 -30
  3. package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  5. package/corpus/templates/analytics/app/lib/utils.ts +1 -1
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
  7. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
  8. package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
  9. package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
  10. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
  11. package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
  12. package/corpus/templates/calendar/app/lib/utils.ts +1 -1
  13. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
  14. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
  15. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  16. package/corpus/templates/clips/app/lib/utils.ts +5 -6
  17. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
  18. package/corpus/templates/clips/desktop/src/app.tsx +26 -12
  19. package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
  20. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
  21. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
  22. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
  23. package/corpus/templates/content/app/i18n-data.ts +10 -10
  24. package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
  25. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
  26. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
  27. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
  28. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
  29. package/corpus/templates/design/actions/get-design-system.ts +103 -3
  30. package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
  31. package/corpus/templates/design/actions/import-github.ts +32 -138
  32. package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
  33. package/corpus/templates/design/actions/present-design-variants.ts +93 -4
  34. package/corpus/templates/design/actions/run-design-audit.ts +229 -0
  35. package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
  36. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
  37. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
  38. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
  39. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
  40. package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
  41. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  42. package/corpus/templates/design/app/i18n-data.ts +32 -1
  43. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
  44. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
  45. package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
  46. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
  47. package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
  48. package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
  49. package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
  50. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
  51. package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
  52. package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
  53. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
  54. package/corpus/templates/design/shared/design-review.ts +1 -0
  55. package/corpus/templates/design/shared/html-integrity.ts +171 -0
  56. package/corpus/templates/mail/app/lib/utils.ts +1 -1
  57. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
  58. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
  59. package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
  60. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  61. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
  62. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +58 -33
  63. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +28 -41
  64. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
  65. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
  66. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
  67. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
  68. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
  69. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
  70. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
  71. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
  72. package/corpus/templates/slides/app/global.css +24 -0
  73. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +5 -1
  74. package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
  75. package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
  76. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
  77. package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
  78. package/corpus/templates/slides/app/lib/image-download.ts +84 -0
  79. package/corpus/templates/slides/app/lib/utils.ts +1 -1
  80. package/corpus/templates/slides/app/pages/Index.tsx +25 -9
  81. package/corpus/templates/slides/netlify.toml +1 -0
  82. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
  83. package/dist/agent/production-agent.js +1 -1
  84. package/dist/brand-kit/types.d.ts +10 -2
  85. package/dist/cli/code.js +1 -1
  86. package/dist/cli/index.js +30 -1
  87. package/dist/cli/native-dependencies.d.ts +24 -0
  88. package/dist/cli/native-dependencies.js +264 -0
  89. package/dist/cli/plan-local.js +1 -1
  90. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  91. package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
  92. package/dist/cli/visualize-repo.js +1 -1
  93. package/dist/client/FeedbackButton.js +12 -12
  94. package/dist/client/agent-chat-adapter.js +3 -4
  95. package/dist/client/assistant-ui-recovery.d.ts +1 -1
  96. package/dist/client/assistant-ui-recovery.js +4 -0
  97. package/dist/client/chat/message-components.d.ts +11 -3
  98. package/dist/client/chat/message-components.js +31 -5
  99. package/dist/client/chat/tool-call-display.d.ts +2 -1
  100. package/dist/client/chat/tool-call-display.js +33 -15
  101. package/dist/client/db-admin/SqlEditor.js +1 -1
  102. package/dist/client/dev-overlay/DevOverlay.js +2 -2
  103. package/dist/client/extensions/portable-extension.js +4 -0
  104. package/dist/client/guided-questions.d.ts +8 -0
  105. package/dist/client/guided-questions.js +12 -3
  106. package/dist/client/sharing/ShareButton.js +16 -24
  107. package/dist/client/sse-event-processor.js +35 -15
  108. package/dist/client/tool-display.d.ts +9 -0
  109. package/dist/client/tool-display.js +39 -0
  110. package/dist/client/vite-dev-recovery-script.js +1 -1
  111. package/dist/collab/awareness.d.ts +2 -2
  112. package/dist/collab/routes.d.ts +1 -1
  113. package/dist/collab/struct-routes.d.ts +1 -1
  114. package/dist/extensions/html-shell.js +5 -0
  115. package/dist/notifications/routes.d.ts +1 -1
  116. package/dist/observability/routes.d.ts +3 -3
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/secrets/routes.d.ts +3 -3
  119. package/dist/server/agent-chat/prompt-resources.js +17 -16
  120. package/dist/server/agent-chat-plugin.d.ts +9 -0
  121. package/dist/server/agent-chat-plugin.js +29 -7
  122. package/dist/server/builder-design-systems.d.ts +33 -1
  123. package/dist/server/builder-design-systems.js +293 -11
  124. package/dist/server/design-token-utils.d.ts +11 -0
  125. package/dist/server/design-token-utils.js +68 -16
  126. package/dist/server/index.d.ts +1 -1
  127. package/dist/server/index.js +1 -1
  128. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8189
34
+ - template files: 8201
@@ -1,11 +1,8 @@
1
1
  # Analytics — Agent Guide
2
2
 
3
3
  Analytics is an agent-native BI workspace for sources, queries, dashboards,
4
- charts, and warehouse integrations. Dashboards are canonical; legacy analyses
5
- remain readable for compatibility.
6
-
7
- Use connected sources and MCP tools; reason over fetched data here, not via
8
- another AI tool.
4
+ charts, and warehouse integrations; dashboards are canonical and legacy
5
+ analyses remain readable.
9
6
 
10
7
  ## Skills
11
8
 
@@ -27,49 +24,42 @@ Read the relevant skill before deeper work:
27
24
 
28
25
  ## How To Answer A Data Question
29
26
 
30
- 1. **Search existing work first.** Call `search-analytics-query-catalog`, adapt
31
- the closest saved SQL to the requested filters/window, run it once, and stop.
27
+ 1. **Search existing work first.** For a metric question, call
28
+ `search-analytics-query-catalog`; for dashboard replication or adaptation,
29
+ call `search-dashboard-references` first. Inspect each returned reference
30
+ with `get-sql-dashboard` or `get-explorer-dashboard` by its `kind`; a match
31
+ is context, not authoritative source data. Then adapt the closest saved SQL
32
+ to the requested filters/window, run it once, and stop.
32
33
  2. **One bounded call.** List/filter/count/cohort questions are one SQL statement
33
34
  or one server-side `run-code` script; never page or fan out per item.
34
35
  3. **Escalate on a miss.** If the catalog has no usable result, make one discovery
35
36
  pass (`list-data-dictionary`, `search-bigquery-schema`, `data-source-status`),
36
37
  then query; don't cross-check or add unasked breakdowns.
37
- 4. **Answer in chat.** Return a short table or inline chart, not a dashboard
38
- pointer. Native widgets show summarized data (≤50 rows); above that, state the
39
- total and top rows.
38
+ 4. **Answer in chat.** Return a short table or inline chart, not only a
39
+ dashboard pointer; for >50 rows, state the total and top rows.
40
40
  5. **Deliver exports in chat.** See `analysis-workspace`; don't return only a path.
41
41
  6. **Chunk only reading.** Group 5-10 only for 30+ qualitative items when a query
42
42
  cannot answer; don't chunk queryable questions. See `adhoc-analysis`.
43
43
 
44
44
  ## Core Rules
45
45
 
46
- - A sibling app asking over A2A sends a question or shaped input, never SQL.
47
- Raw-query actions (`sql`, `code`, `script`, `expression`) are not
48
- sibling-invocable because this app owns schema, source selection, and tools.
49
- Prefer natural-language delegation; shaped reads are stable contracts, not
50
- delegation workarounds.
46
+ - A sibling app sends natural-language or shaped input over A2A, never SQL; this
47
+ app owns schema, source selection, and tools. Prefer natural-language
48
+ delegation; shaped reads are stable contracts.
51
49
  - Analytics owns first-party product usage, app/template events, agent-native
52
50
  signups, conversions, and other curated product metrics. Answer sibling-app
53
51
  delegations with the built-in source and query catalog; sibling agents should
54
52
  send a natural-language question, never SQL.
55
53
  - For open-ended delegated requests, choose a safe default and label partial.
56
54
  - Data integrity first. Never invent numbers, dimensions, filters, or source
57
- semantics; only present values you actually retrieved, and state uncertainty.
58
- - Every analytical answer carries audit context: source(s), time window,
59
- filters, row count/sample size, join method, caveats.
55
+ semantics; present only retrieved values with source, window, filters,
56
+ row-count/sample-size, join method, and caveats.
60
57
  - Use actions for sources, queries, charts, dashboards, and sharing. Don't bypass
61
58
  access checks with raw SQL for ownable resources.
62
59
  - Provider actions are bounded shortcuts, not limits. For broad or
63
- absence-sensitive Gong work, stage raw API data and use
64
- `query-staged-dataset` or a Data Program; use `provider-corpus-job` for raw
65
- transcript bodies. See `provider-api`, `data-programs`, and `gong`.
66
- - Custom APIs use `provider-api-register` for public HTTPS provider metadata and
67
- `test-custom-api-connection` for bounded GET previews. Store credential values
68
- in Settings, pass only key names to provider actions, and hand successful
69
- endpoint tests to `save-data-program` for refreshable panels.
70
- - Hosted Analytics cannot reach localhost or private network APIs. Use a
71
- deployed HTTPS endpoint or an explicitly supported secure tunnel; never
72
- weaken the provider runtime's SSRF boundary.
60
+ absence-sensitive Gong work, stage raw API data and use `query-staged-dataset`
61
+ or a Data Program; see `provider-api`, `data-programs`, and `gong` for secure
62
+ provider and hosted-endpoint boundaries.
73
63
  - Create dashboards, panels, or saved artifacts only when explicitly asked;
74
64
  suggest and wait otherwise. Scope them to the question, avoid decorative
75
65
  metrics, and never modify existing dashboards without a directive.
@@ -87,8 +77,8 @@ Read the relevant skill before deeper work:
87
77
  - External MCP callers default to `ask_app` for interpretation, source choice,
88
78
  analysis, or multi-step work. Direct reads require exact, complete input;
89
79
  writes stay `ask_app`-only.
90
- - Dashboard email reports and alert rules are SQL-backed, self-describing action
91
- surfaces — don't hand-wire routes around them. Reports cap at five recipients.
80
+ - Dashboard reports and alert rules use their SQL-backed action surfaces; reports
81
+ cap at five recipients.
92
82
 
93
83
  ## Application State
94
84
 
@@ -0,0 +1,58 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ buildDeepLink,
4
+ getRequestOrgId,
5
+ getRequestUserEmail,
6
+ } from "@agent-native/core/server";
7
+ import { z } from "zod";
8
+
9
+ import { searchDashboardReferences } from "../server/lib/dashboards-store";
10
+
11
+ export default defineAction({
12
+ description:
13
+ "Find a bounded set of accessible saved SQL or explorer dashboards whose id, name, description, or config matches the search text. Use this when the user wants to replicate or adapt an existing dashboard. These are references only: inspect the returned dashboard with the getter for its kind before copying it, and do not assume a first-party Analytics dashboard is the authoritative source for a new question.",
14
+ schema: z.object({
15
+ search: z
16
+ .string()
17
+ .trim()
18
+ .min(2)
19
+ .describe("Focused dashboard name, metric, provider, or config terms"),
20
+ limit: z
21
+ .number()
22
+ .int()
23
+ .min(1)
24
+ .max(24)
25
+ .optional()
26
+ .default(8)
27
+ .describe("Maximum dashboard references to return"),
28
+ }),
29
+ http: { method: "GET" },
30
+ readOnly: true,
31
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
32
+ link: ({ result }) => {
33
+ const first = Array.isArray(result) ? result[0] : null;
34
+ const id =
35
+ first && typeof first === "object"
36
+ ? (first as { id?: string }).id
37
+ : undefined;
38
+ if (!id) return null;
39
+ return {
40
+ url: buildDeepLink({
41
+ app: "analytics",
42
+ view: "adhoc",
43
+ params: { dashboardId: id },
44
+ }),
45
+ label: "Open dashboard reference in Analytics",
46
+ view: "adhoc",
47
+ };
48
+ },
49
+ run: async ({ search, limit }) => {
50
+ const email = getRequestUserEmail();
51
+ if (!email) throw new Error("no authenticated user");
52
+ return searchDashboardReferences(
53
+ { email, orgId: getRequestOrgId() || null },
54
+ search,
55
+ limit,
56
+ );
57
+ },
58
+ });
@@ -2158,7 +2158,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2158
2158
  </TooltipTrigger>
2159
2159
  <TooltipContent side="top">
2160
2160
  {t("sidebar.searchShortcut", {
2161
- shortcut: `${shortcutModifierLabel()}+K`,
2161
+ shortcut: `${shortcutModifierLabel()} K`,
2162
2162
  })}
2163
2163
  </TooltipContent>
2164
2164
  </Tooltip>
@@ -8,5 +8,5 @@ export function isMacPlatform(): boolean {
8
8
  }
9
9
 
10
10
  export function shortcutModifierLabel(): string {
11
- return isMacPlatform() ? "\u2318" : "Ctrl";
11
+ return isMacPlatform() ? "Cmd" : "Ctrl";
12
12
  }
@@ -236,7 +236,7 @@ export function ViewSqlPopover({
236
236
  <p className="text-[11px] text-muted-foreground mt-1.5">
237
237
  {t("sqlDashboard.filterInterpolationHelp", {
238
238
  example: "{{varName}}",
239
- shortcut: `${isMac ? "⌘" : "Ctrl"}+Enter`,
239
+ shortcut: `${isMac ? "Cmd" : "Ctrl"} Enter`,
240
240
  })}
241
241
  </p>
242
242
  ) : null}
@@ -22,6 +22,7 @@ export const INITIAL_TOOL_NAMES = [
22
22
  // build request should not stop at inspection or an empty extension shell
23
23
  // while the agent lazily discovers the next mutating action.
24
24
  "get-sql-dashboard",
25
+ "get-explorer-dashboard",
25
26
  "list-sql-dashboards",
26
27
  "list-extensions",
27
28
  "get-extension",
@@ -33,6 +34,7 @@ export const INITIAL_TOOL_NAMES = [
33
34
  "extension-data-set",
34
35
  "generate-chart",
35
36
  "search-analytics-query-catalog",
37
+ "search-dashboard-references",
36
38
  "query-agent-native-analytics",
37
39
  "bigquery",
38
40
  "search-bigquery-schema",
@@ -28,7 +28,16 @@ import {
28
28
  resolveAccess,
29
29
  type ShareRole,
30
30
  } from "@agent-native/core/sharing";
31
- import { and, desc, eq, inArray, isNotNull, isNull, sql } from "drizzle-orm";
31
+ import {
32
+ and,
33
+ desc,
34
+ eq,
35
+ inArray,
36
+ isNotNull,
37
+ isNull,
38
+ or,
39
+ sql,
40
+ } from "drizzle-orm";
32
41
 
33
42
  import { getDb, schema } from "../db/index.js";
34
43
 
@@ -78,6 +87,19 @@ export interface DashboardSummaryRecord {
78
87
  hiddenBy: string | null;
79
88
  }
80
89
 
90
+ /** Compact, access-scoped reference returned by dashboard discovery. */
91
+ export interface DashboardReferenceRecord {
92
+ id: string;
93
+ kind: DashboardKind;
94
+ name: string;
95
+ description: string | null;
96
+ ownerEmail: string;
97
+ orgId: string | null;
98
+ visibility: "private" | "org" | "public";
99
+ updatedAt: string;
100
+ matchedFields: Array<"id" | "name" | "description" | "config">;
101
+ }
102
+
81
103
  /** Hydrated dashboard row for catalog ranking only. */
82
104
  export interface DashboardCatalogRecord {
83
105
  id: string;
@@ -147,9 +169,23 @@ interface AccessCtx {
147
169
 
148
170
  const SQL_PREFIX = "sql-dashboard-";
149
171
  const EXPLORER_PREFIX = "dashboard-";
172
+
173
+ /**
174
+ * `EXPLORER_PREFIX` also prefixes per-dashboard preference keys such as
175
+ * `dashboard-filters:<id>`, so an unqualified prefix match reads every saved
176
+ * filter set back as an "Untitled" explorer dashboard. Settings sub-namespaces
177
+ * are the only thing that puts a separator inside the remainder, encoded or
178
+ * not, and no dashboard id contains one.
179
+ */
180
+ function isLegacyDashboardId(id: string): boolean {
181
+ return id.length > 0 && !/:|%3a/i.test(id);
182
+ }
150
183
  const ANALYSIS_PREFIX = "adhoc-analysis-";
151
184
  const DASHBOARD_REVISION_LIMIT = 50;
152
185
  const ANALYSIS_REVISION_LIMIT = 30;
186
+ const MAX_DASHBOARD_REFERENCE_RESULTS = 24;
187
+ const MAX_DASHBOARD_REFERENCE_CANDIDATES = 200;
188
+ const OUT_OF_SCOPE_REFERENCE_PROBE_LIMIT = 5;
153
189
 
154
190
  export function normalizeDashboardName(value: string): string {
155
191
  return value.trim().replace(/\s+/g, " ").toLowerCase();
@@ -159,6 +195,138 @@ function nowIso(): string {
159
195
  return new Date().toISOString();
160
196
  }
161
197
 
198
+ function escapeLikeLiteral(value: string): string {
199
+ return value.replace(/[\\%_]/g, "\\$&");
200
+ }
201
+
202
+ type DashboardReferenceSearchQuery = {
203
+ phrase: string;
204
+ terms: string[];
205
+ };
206
+
207
+ function dashboardReferenceSearchQuery(
208
+ search: string,
209
+ ): DashboardReferenceSearchQuery {
210
+ const phrase = search.trim().replace(/\s+/g, " ").toLowerCase();
211
+ return {
212
+ phrase,
213
+ terms: phrase.split(" ").filter(Boolean).slice(0, 8),
214
+ };
215
+ }
216
+
217
+ function dashboardReferenceFieldText(value: unknown): string {
218
+ if (typeof value === "string") return value.toLowerCase();
219
+ if (!value || typeof value !== "object") return "";
220
+ const serialized = JSON.stringify(value);
221
+ return typeof serialized === "string" ? serialized.toLowerCase() : "";
222
+ }
223
+
224
+ function dashboardReferenceMatch(
225
+ row: {
226
+ id?: unknown;
227
+ kind?: unknown;
228
+ name?: unknown;
229
+ description?: unknown;
230
+ config?: unknown;
231
+ ownerEmail?: unknown;
232
+ orgId?: unknown;
233
+ visibility?: unknown;
234
+ updatedAt?: unknown;
235
+ },
236
+ query: DashboardReferenceSearchQuery,
237
+ ): { record: DashboardReferenceRecord; score: number } | null {
238
+ const fields = {
239
+ id: dashboardReferenceFieldText(row.id),
240
+ name: dashboardReferenceFieldText(row.name),
241
+ description: dashboardReferenceFieldText(row.description),
242
+ config: dashboardReferenceFieldText(row.config),
243
+ } satisfies Record<DashboardReferenceRecord["matchedFields"][number], string>;
244
+ const matchedFields = Object.entries(fields)
245
+ .filter(([, value]) => query.terms.some((term) => value.includes(term)))
246
+ .map(
247
+ ([field]) => field as DashboardReferenceRecord["matchedFields"][number],
248
+ );
249
+ const matchedTerms = query.terms.filter((term) =>
250
+ Object.values(fields).some((value) => value.includes(term)),
251
+ ).length;
252
+ if (matchedTerms !== query.terms.length) return null;
253
+
254
+ let score = matchedTerms * 100;
255
+ for (const [field, value] of Object.entries(fields)) {
256
+ const weight =
257
+ field === "name"
258
+ ? 80
259
+ : field === "description"
260
+ ? 45
261
+ : field === "id"
262
+ ? 30
263
+ : 10;
264
+ if (value.includes(query.phrase)) score += 100 + weight;
265
+ if (field === "name" && value === query.phrase) score += 300;
266
+ if (field === "name" && value.startsWith(query.phrase)) score += 40;
267
+ }
268
+
269
+ return {
270
+ record: {
271
+ id: String(row.id ?? ""),
272
+ kind: row.kind === "explorer" ? "explorer" : "sql",
273
+ name: String(row.name ?? "Untitled dashboard"),
274
+ description: typeof row.description === "string" ? row.description : null,
275
+ ownerEmail: String(row.ownerEmail ?? ""),
276
+ orgId: typeof row.orgId === "string" ? row.orgId : null,
277
+ visibility:
278
+ row.visibility === "public" || row.visibility === "org"
279
+ ? row.visibility
280
+ : "private",
281
+ updatedAt: String(row.updatedAt ?? ""),
282
+ matchedFields,
283
+ },
284
+ score,
285
+ };
286
+ }
287
+
288
+ function legacyDashboardReferenceScope(
289
+ key: string,
290
+ ctx: AccessCtx,
291
+ ): {
292
+ id: string;
293
+ kind: DashboardKind;
294
+ ownerEmail: string;
295
+ orgId: string | null;
296
+ visibility: DashboardReferenceRecord["visibility"];
297
+ } | null {
298
+ if (ctx.orgId && key.startsWith(`o:${ctx.orgId}:${SQL_PREFIX}`)) {
299
+ return {
300
+ id: key.slice(`o:${ctx.orgId}:${SQL_PREFIX}`.length),
301
+ kind: "sql",
302
+ ownerEmail: ctx.email,
303
+ orgId: ctx.orgId,
304
+ visibility: "org",
305
+ };
306
+ }
307
+ if (ctx.email && key.startsWith(`u:${ctx.email}:${SQL_PREFIX}`)) {
308
+ return {
309
+ id: key.slice(`u:${ctx.email}:${SQL_PREFIX}`.length),
310
+ kind: "sql",
311
+ ownerEmail: ctx.email,
312
+ orgId: null,
313
+ visibility: "private",
314
+ };
315
+ }
316
+ if (ctx.email && key.startsWith(`u:${ctx.email}:${EXPLORER_PREFIX}`)) {
317
+ const id = key.slice(`u:${ctx.email}:${EXPLORER_PREFIX}`.length);
318
+ if (!isLegacyDashboardId(id)) return null;
319
+ return {
320
+ id,
321
+ kind: "explorer",
322
+ ownerEmail: ctx.email,
323
+ orgId: null,
324
+ visibility: "private",
325
+ };
326
+ }
327
+ return null;
328
+ }
329
+
162
330
  function nanoidFallback(): string {
163
331
  return (
164
332
  Math.random().toString(36).slice(2, 10) +
@@ -518,7 +686,7 @@ export async function listDashboards(
518
686
  key.startsWith(`u:${ctx.email}:${EXPLORER_PREFIX}`)
519
687
  ) {
520
688
  id = key.slice(`u:${ctx.email}:${EXPLORER_PREFIX}`.length);
521
- kind = "explorer";
689
+ kind = isLegacyDashboardId(id) ? "explorer" : null;
522
690
  }
523
691
  if (!id || !kind) continue;
524
692
  if (filter?.kind && filter.kind !== kind) continue;
@@ -665,7 +833,7 @@ export async function listDashboardSummaries(
665
833
  key.startsWith(`u:${ctx.email}:${EXPLORER_PREFIX}`)
666
834
  ) {
667
835
  id = key.slice(`u:${ctx.email}:${EXPLORER_PREFIX}`.length);
668
- kind = "explorer";
836
+ kind = isLegacyDashboardId(id) ? "explorer" : null;
669
837
  }
670
838
  if (!id || !kind || seen.has(id)) continue;
671
839
  if (filter?.kind && filter.kind !== kind) continue;
@@ -707,6 +875,201 @@ export async function listDashboardSummaries(
707
875
  return out;
708
876
  }
709
877
 
878
+ /**
879
+ * Find active saved dashboards by metadata or serialized config.
880
+ *
881
+ * This is deliberately separate from the query catalog: a matching saved
882
+ * dashboard is a replication reference, not proof that its source is
883
+ * authoritative for the question being asked.
884
+ */
885
+ export async function searchDashboardReferences(
886
+ ctx: AccessCtx,
887
+ search: string,
888
+ limit = 8,
889
+ dbOverride?: any,
890
+ ): Promise<DashboardReferenceRecord[]> {
891
+ const query = dashboardReferenceSearchQuery(search);
892
+ if (!query.phrase || query.terms.length === 0) return [];
893
+ const boundedLimit = Math.min(
894
+ Math.max(Number.isFinite(limit) ? Math.trunc(limit) : 8, 1),
895
+ MAX_DASHBOARD_REFERENCE_RESULTS,
896
+ );
897
+ const db = (dbOverride ?? getDb()) as any;
898
+ const access = accessFilter(schema.dashboards, schema.dashboardShares, {
899
+ userEmail: ctx.email,
900
+ orgId: ctx.orgId ?? undefined,
901
+ });
902
+ const wildcardMatches = (term: string) => {
903
+ const pattern = `%${escapeLikeLiteral(term)}%`;
904
+ return or(
905
+ sql<boolean>`lower(${schema.dashboards.id}) LIKE ${pattern} ESCAPE '\\'`,
906
+ sql<boolean>`lower(${schema.dashboards.title}) LIKE ${pattern} ESCAPE '\\'`,
907
+ sql<boolean>`lower(coalesce(${schema.dashboards.config}, '')) LIKE ${pattern} ESCAPE '\\'`,
908
+ );
909
+ };
910
+ const phraseMatch = wildcardMatches(query.phrase);
911
+ const tokenMatch =
912
+ query.terms.length === 1
913
+ ? wildcardMatches(query.terms[0]!)
914
+ : and(...query.terms.map(wildcardMatches));
915
+ const where = and(
916
+ access,
917
+ isNull(schema.dashboards.archivedAt),
918
+ isNull(schema.dashboards.hiddenAt),
919
+ or(phraseMatch, tokenMatch),
920
+ );
921
+ // guard:allow-heavy-dashboard-list-read — bounded wildcard candidates need serialized config to rank references.
922
+ const rows = await db
923
+ .select({
924
+ id: schema.dashboards.id,
925
+ kind: schema.dashboards.kind,
926
+ name: schema.dashboards.title,
927
+ description: isPostgres()
928
+ ? sql<
929
+ string | null
930
+ >`(${schema.dashboards.config}::jsonb ->> 'description')`
931
+ : sql<
932
+ string | null
933
+ >`json_extract(${schema.dashboards.config}, '$.description')`,
934
+ config: schema.dashboards.config,
935
+ ownerEmail: schema.dashboards.ownerEmail,
936
+ orgId: schema.dashboards.orgId,
937
+ visibility: schema.dashboards.visibility,
938
+ updatedAt: schema.dashboards.updatedAt,
939
+ })
940
+ .from(schema.dashboards)
941
+ .where(where)
942
+ .orderBy(desc(schema.dashboards.updatedAt))
943
+ .limit(MAX_DASHBOARD_REFERENCE_CANDIDATES);
944
+
945
+ const ranked: Array<{
946
+ record: DashboardReferenceRecord;
947
+ score: number;
948
+ }> = rows
949
+ .map((row: any) => dashboardReferenceMatch(row, query))
950
+ .filter(
951
+ (
952
+ match: { record: DashboardReferenceRecord; score: number } | null,
953
+ ): match is {
954
+ record: DashboardReferenceRecord;
955
+ score: number;
956
+ } => match !== null,
957
+ );
958
+
959
+ // Older Analytics deployments still have dashboards in settings KV. Search
960
+ // that scoped fallback too, but keep SQL rows authoritative when an id has
961
+ // already been migrated.
962
+ const seen = new Set(
963
+ ranked.map(({ record }) => `${record.kind}:${record.id}`),
964
+ );
965
+ const allSettings = await getAllSettings();
966
+ for (const [key, value] of Object.entries(allSettings)) {
967
+ const scope = legacyDashboardReferenceScope(key, ctx);
968
+ if (
969
+ !scope ||
970
+ !scope.id ||
971
+ typeof value !== "object" ||
972
+ value === null ||
973
+ Array.isArray(value)
974
+ ) {
975
+ continue;
976
+ }
977
+ const { title, config } = configFromSettings(
978
+ value as Record<string, unknown>,
979
+ );
980
+ const match = dashboardReferenceMatch(
981
+ {
982
+ id: scope.id,
983
+ kind: scope.kind,
984
+ name: title,
985
+ description: configDescriptionFromValue(config),
986
+ config,
987
+ ownerEmail: scope.ownerEmail,
988
+ orgId: scope.orgId,
989
+ visibility: scope.visibility,
990
+ updatedAt:
991
+ typeof config.updatedAt === "string"
992
+ ? config.updatedAt
993
+ : typeof config.createdAt === "string"
994
+ ? config.createdAt
995
+ : "",
996
+ },
997
+ query,
998
+ );
999
+ if (!match || seen.has(`${match.record.kind}:${match.record.id}`)) {
1000
+ continue;
1001
+ }
1002
+ seen.add(`${match.record.kind}:${match.record.id}`);
1003
+ ranked.push(match);
1004
+ }
1005
+
1006
+ if (ranked.length === 0) {
1007
+ await assertNoOwnedReferenceHiddenByScope(db, ctx, search, query);
1008
+ }
1009
+
1010
+ return ranked
1011
+ .sort(
1012
+ (a, b) =>
1013
+ b.score - a.score ||
1014
+ b.record.updatedAt.localeCompare(a.record.updatedAt),
1015
+ )
1016
+ .slice(0, boundedLimit)
1017
+ .map(({ record }) => record);
1018
+ }
1019
+
1020
+ /**
1021
+ * With no active organization, `ownerScopeFilter` narrows the owner clause to
1022
+ * `org_id IS NULL` and drops the `visibility = 'org'` clause entirely, so every
1023
+ * dashboard the caller owns under an organization vanishes from this search.
1024
+ * An empty result then reads as "no such dashboard" and the agent answers by
1025
+ * querying raw event tables instead of the dashboard the user named. Probe on
1026
+ * the empty path only, so an ordinary miss stays one query.
1027
+ */
1028
+ async function assertNoOwnedReferenceHiddenByScope(
1029
+ db: any,
1030
+ ctx: AccessCtx,
1031
+ search: string,
1032
+ query: ReturnType<typeof dashboardReferenceSearchQuery>,
1033
+ ): Promise<void> {
1034
+ if (ctx.orgId || !ctx.email) return;
1035
+ const nameMatches = (term: string) => {
1036
+ const pattern = `%${escapeLikeLiteral(term)}%`;
1037
+ return or(
1038
+ sql<boolean>`lower(${schema.dashboards.id}) LIKE ${pattern} ESCAPE '\\'`,
1039
+ sql<boolean>`lower(${schema.dashboards.title}) LIKE ${pattern} ESCAPE '\\'`,
1040
+ );
1041
+ };
1042
+ const rows = await db
1043
+ .select({
1044
+ id: schema.dashboards.id,
1045
+ name: schema.dashboards.title,
1046
+ orgId: schema.dashboards.orgId,
1047
+ })
1048
+ .from(schema.dashboards)
1049
+ .where(
1050
+ and(
1051
+ sql<boolean>`lower(${schema.dashboards.ownerEmail}) = ${ctx.email.toLowerCase()}`,
1052
+ isNotNull(schema.dashboards.orgId),
1053
+ isNull(schema.dashboards.archivedAt),
1054
+ isNull(schema.dashboards.hiddenAt),
1055
+ or(
1056
+ nameMatches(query.phrase),
1057
+ query.terms.length === 1
1058
+ ? nameMatches(query.terms[0]!)
1059
+ : and(...query.terms.map(nameMatches)),
1060
+ ),
1061
+ ),
1062
+ )
1063
+ .limit(OUT_OF_SCOPE_REFERENCE_PROBE_LIMIT);
1064
+ if (!rows?.length) return;
1065
+ const named = rows
1066
+ .map((row: any) => `"${row.name ?? row.id}" (id ${row.id})`)
1067
+ .join(", ");
1068
+ throw new Error(
1069
+ `No dashboard readable in this session matches "${search}", but ${rows.length} organization-scoped dashboard(s) owned by ${ctx.email} do: ${named}. This session resolved no active organization, so they cannot be read. Tell the user their organization context is missing and that the dashboard exists — do not answer from a different data source.`,
1070
+ );
1071
+ }
1072
+
710
1073
  /**
711
1074
  * Hydrate a bounded set of dashboard ids for catalog ranking.
712
1075
  *
@@ -33,7 +33,9 @@ export const CORPUS_REDUCTION_ACTIONS = new Set(["run-code"]);
33
33
  // includes one of these read actions, the bypass still applies even when an
34
34
  // authoring/save action ran alongside it.
35
35
  export const DASHBOARD_CONSTRUCTION_ACTIONS = new Set([
36
+ "search-dashboard-references",
36
37
  "get-sql-dashboard",
38
+ "get-explorer-dashboard",
37
39
  "list-sql-dashboards",
38
40
  "list-extensions",
39
41
  "get-extension",
@@ -159,7 +161,7 @@ function isDashboardMutationActionName(name: string): boolean {
159
161
  // dashboard construction, distinct from an analytics-result question. Turns
160
162
  // like this may inspect and clone a template without running a metric query.
161
163
  const DASHBOARD_CONSTRUCTION_INTENT_TERMS =
162
- /\b(build|create|make|clone|copy|duplicate|adapt|update|edit|change|modify|rename|adjust|refresh|simplify|switch|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
164
+ /\b(build|create|make|replicate|clone|copy|duplicate|adapt|update|edit|change|modify|rename|adjust|refresh|simplify|switch|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
163
165
 
164
166
  const DASHBOARD_CONSTRUCTION_TARGET_TERMS =
165
167
  /\b(dashboard|extension|panel|widget)\b/i;