@agent-native/core 0.153.9 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +20 -30
- package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/lib/utils.ts +1 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
- package/corpus/templates/analytics/app/pages/monitoring/errors/IssueList.tsx +4 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
- package/corpus/templates/calendar/app/lib/utils.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/utils.ts +5 -6
- package/corpus/templates/clips/app/root.tsx +4 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -2
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
- package/corpus/templates/clips/desktop/src/app.tsx +27 -13
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
- package/corpus/templates/content/app/i18n-data.ts +10 -10
- package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
- package/corpus/templates/design/actions/get-design-system.ts +103 -3
- package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
- package/corpus/templates/design/actions/import-github.ts +32 -138
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
- package/corpus/templates/design/actions/present-design-variants.ts +93 -4
- package/corpus/templates/design/actions/run-design-audit.ts +229 -0
- package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/i18n-data.ts +32 -1
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
- package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
- package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +171 -0
- package/corpus/templates/mail/app/lib/utils.ts +1 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
- package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +58 -33
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +28 -41
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
- package/corpus/templates/slides/app/global.css +24 -0
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +5 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
- package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
- package/corpus/templates/slides/app/lib/image-download.ts +84 -0
- package/corpus/templates/slides/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +25 -9
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
- package/dist/agent/production-agent.js +1 -1
- package/dist/brand-kit/types.d.ts +10 -2
- package/dist/cli/code.js +1 -1
- package/dist/cli/index.js +30 -1
- package/dist/cli/native-dependencies.d.ts +24 -0
- package/dist/cli/native-dependencies.js +264 -0
- package/dist/cli/plan-local.js +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.js +2 -1
- package/dist/cli/skills.js +2 -1
- package/dist/cli/visualize-repo.js +1 -1
- package/dist/client/FeedbackButton.js +12 -12
- package/dist/client/NewWorkspaceAppFlow.js +4 -1
- package/dist/client/agent-chat-adapter.js +3 -4
- package/dist/client/agent-page/AgentTabsPage.d.ts +2 -2
- package/dist/client/agent-page/AgentTabsPage.js +12 -9
- package/dist/client/assistant-ui-recovery.d.ts +1 -1
- package/dist/client/assistant-ui-recovery.js +4 -0
- package/dist/client/chat/message-components.d.ts +11 -3
- package/dist/client/chat/message-components.js +31 -5
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.js +33 -15
- package/dist/client/db-admin/SqlEditor.js +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +2 -2
- package/dist/client/extensions/ExtensionsSidebarSection.js +2 -1
- package/dist/client/extensions/portable-extension.js +4 -0
- package/dist/client/guided-questions.d.ts +8 -0
- package/dist/client/guided-questions.js +12 -3
- package/dist/client/onboarding/FirstRunOnboarding.js +2 -1
- package/dist/client/org/TeamPage.js +14 -3
- package/dist/client/settings/SettingsPanel.js +26 -1
- package/dist/client/sharing/ShareButton.js +16 -24
- package/dist/client/sse-event-processor.js +35 -15
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +39 -0
- package/dist/client/vite-dev-recovery-script.js +1 -1
- package/dist/extensions/html-shell.js +5 -0
- package/dist/integrations/catalog.js +9 -8
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/prompt-resources.js +17 -16
- package/dist/server/agent-chat-plugin.d.ts +9 -0
- package/dist/server/agent-chat-plugin.js +31 -9
- package/dist/server/builder-design-systems.d.ts +33 -1
- package/dist/server/builder-design-systems.js +293 -11
- package/dist/server/design-token-utils.d.ts +11 -0
- package/dist/server/design-token-utils.js +68 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/shared/docs-url.d.ts +24 -0
- package/dist/shared/docs-url.js +39 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/default/app/routes/_index.tsx +2 -1
- package/package.json +2 -2
- package/src/templates/default/app/routes/_index.tsx +2 -1
package/corpus/README.md
CHANGED
|
@@ -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
|
|
5
|
-
remain readable
|
|
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.**
|
|
31
|
-
|
|
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
|
|
38
|
-
pointer
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
58
|
-
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
91
|
-
|
|
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()}
|
|
2161
|
+
shortcut: `${shortcutModifierLabel()} K`,
|
|
2162
2162
|
})}
|
|
2163
2163
|
</TooltipContent>
|
|
2164
2164
|
</Tooltip>
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getDefaultMcpIntegrations,
|
|
17
17
|
McpIntegrationLogo,
|
|
18
18
|
} from "@agent-native/core/client/resources";
|
|
19
|
+
import { docsUrl } from "@agent-native/core/shared";
|
|
19
20
|
import {
|
|
20
21
|
IconCheck,
|
|
21
22
|
IconChevronDown,
|
|
@@ -1903,7 +1904,7 @@ function FirstPartyAnalyticsCard() {
|
|
|
1903
1904
|
each one happened.
|
|
1904
1905
|
</p>
|
|
1905
1906
|
<a
|
|
1906
|
-
href="
|
|
1907
|
+
href={docsUrl("tracking", { hash: "posthog-error-tracking" })}
|
|
1907
1908
|
target="_blank"
|
|
1908
1909
|
rel="noreferrer"
|
|
1909
1910
|
className="mt-2 inline-flex items-center gap-1 font-medium text-primary hover:underline"
|
|
@@ -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 ? "
|
|
239
|
+
shortcut: `${isMac ? "Cmd" : "Ctrl"} Enter`,
|
|
240
240
|
})}
|
|
241
241
|
</p>
|
|
242
242
|
) : null}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CodeSurface } from "@agent-native/core/blocks";
|
|
2
|
+
import { docsUrl } from "@agent-native/core/shared";
|
|
2
3
|
import {
|
|
3
4
|
IconAlertTriangle,
|
|
4
5
|
IconBug,
|
|
@@ -28,8 +29,9 @@ import {
|
|
|
28
29
|
useStatusLabel,
|
|
29
30
|
} from "./utils";
|
|
30
31
|
|
|
31
|
-
const ERROR_CAPTURE_DOCS_URL =
|
|
32
|
-
"
|
|
32
|
+
const ERROR_CAPTURE_DOCS_URL = docsUrl("tracking", {
|
|
33
|
+
hash: "posthog-error-tracking",
|
|
34
|
+
});
|
|
33
35
|
|
|
34
36
|
const ERROR_CAPTURE_SNIPPET = `// Agent Native templates already call configureTracking().
|
|
35
37
|
import { configureTracking, captureException } from "@agent-native/core/client/observability";
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useBuilderConnectFlow,
|
|
7
7
|
useBuilderStatus,
|
|
8
8
|
} from "@agent-native/core/client/settings";
|
|
9
|
+
import { docsUrl } from "@agent-native/core/shared";
|
|
9
10
|
import {
|
|
10
11
|
IconCheck,
|
|
11
12
|
IconChevronDown,
|
|
@@ -58,8 +59,9 @@ import { cn } from "@/lib/utils";
|
|
|
58
59
|
|
|
59
60
|
type ReplayRange = "24h" | "7d" | "30d" | "90d" | "all";
|
|
60
61
|
|
|
61
|
-
const SESSION_REPLAY_DOCS_URL =
|
|
62
|
-
"
|
|
62
|
+
const SESSION_REPLAY_DOCS_URL = docsUrl("tracking", {
|
|
63
|
+
hash: "session-replay",
|
|
64
|
+
});
|
|
63
65
|
|
|
64
66
|
const S3_STORAGE_FIELDS = [
|
|
65
67
|
{
|
|
@@ -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 {
|
|
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
|
*
|