@agent-native/core 0.75.1 → 0.75.2

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 (30) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +30 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +56 -0
  5. package/corpus/core/src/agent/production-agent.ts +16 -3
  6. package/corpus/core/src/deploy/build.ts +87 -40
  7. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +0 -8
  8. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -1
  9. package/corpus/templates/analytics/app/components/SqlEditor.tsx +57 -0
  10. package/corpus/templates/analytics/app/components/SqlHighlight.tsx +27 -28
  11. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +130 -8
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -3
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +4 -4
  14. package/corpus/templates/analytics/changelog/2026-06-24-active-user-dashboard-panels-classify-templates-more-accurat.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-panel-deletes-now-stay-deleted-after-refresh.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-06-24-sql-panel-editors-now-highlight-query-syntax-and-keep-one-cl.md +6 -0
  17. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
  18. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -11
  19. package/dist/agent/durable-background.d.ts +44 -0
  20. package/dist/agent/durable-background.d.ts.map +1 -1
  21. package/dist/agent/durable-background.js +51 -0
  22. package/dist/agent/durable-background.js.map +1 -1
  23. package/dist/agent/production-agent.d.ts.map +1 -1
  24. package/dist/agent/production-agent.js +16 -3
  25. package/dist/agent/production-agent.js.map +1 -1
  26. package/dist/deploy/build.d.ts +37 -21
  27. package/dist/deploy/build.d.ts.map +1 -1
  28. package/dist/deploy/build.js +84 -40
  29. package/dist/deploy/build.js.map +1 -1
  30. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 1160
31
- - template files: 4025
31
+ - template files: 4029
@@ -1,5 +1,35 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.75.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b3543d: Durable background agent-chat runs now actually receive Netlify's 15-min async
8
+ budget by reaching a STANDALONE background function at its DIRECT url, bypassing
9
+ Nitro's synchronous `/*` catch-all.
10
+
11
+ The previous emit wrote the background function into `.netlify/functions-internal`
12
+ with a custom `config.path` of the `_process-run` route. A custom `config.path`
13
+ makes a function reachable ONLY at that path (not at its default function url),
14
+ `functions-internal` is not exposed at a default url, and Netlify routed
15
+ `/_agent-native/agent-chat/_process-run` to the synchronous Nitro `server`
16
+ catch-all instead — so the worker was capped at the ~60s wall and degraded to
17
+ 40s-chunked runs (confirmed live: a POST to the process-run path returned a
18
+ synchronous 401 from the handler rather than a 202 async ack).
19
+
20
+ The build now emits a standalone function into the STANDARD
21
+ `.netlify/functions/server-agent-background` dir with `background: true` and NO
22
+ custom `path`, so Netlify exposes it at `/.netlify/functions/server-agent-background`
23
+ and invokes it asynchronously (immediate 202 ack, 15-min budget). Its entry sets
24
+ `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start and rewrites
25
+ the incoming request path back to the `_process-run` route before delegating to
26
+ the Nitro handler, preserving the method, all headers (the HMAC
27
+ `Authorization: Bearer` the plugin verifies survives the rewrite), and the body.
28
+ The foreground self-dispatch (and server-driven continuation chunks) now target
29
+ that direct url on hosted Netlify via a shared `resolveAgentChatProcessRunDispatchPath`
30
+ helper, and stay on the framework route everywhere else. The graceful inline
31
+ 40s fallback on a dispatch fast-fail is unchanged.
32
+
3
33
  ## 0.75.1
4
34
 
5
35
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.75.1",
3
+ "version": "0.75.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -51,6 +51,62 @@ import {
51
51
  export const AGENT_CHAT_PROCESS_RUN_PATH =
52
52
  "/_agent-native/agent-chat/_process-run";
53
53
 
54
+ /**
55
+ * Name of the standalone Netlify background function the build emits (see
56
+ * `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so
57
+ * the emit and the dispatch-path helper below can never drift on the name.
58
+ *
59
+ * MUST end in `-background` — both because that is the conventional Netlify
60
+ * async-function suffix and because `isInBackgroundFunctionRuntime()` reads the
61
+ * `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.
62
+ */
63
+ export const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
64
+
65
+ /**
66
+ * Direct invocation URL of the standalone background function on Netlify.
67
+ *
68
+ * The function is emitted into the STANDARD functions dir with `background:true`
69
+ * and NO custom `config.path`, so Netlify exposes it (and ONLY it) at the
70
+ * default function URL `/.netlify/functions/<name>` and invokes it
71
+ * asynchronously (immediate HTTP 202 ack, 15-min budget). Hitting this URL
72
+ * directly BYPASSES Nitro's `/*` catch-all `server` function — which is the
73
+ * whole point: the previous approach put a custom `config.path` on the function
74
+ * and Netlify routed `/_agent-native/agent-chat/_process-run` to the synchronous
75
+ * `server` catch-all instead (verified live: a synchronous 401 from the handler
76
+ * rather than a 202 async ack).
77
+ */
78
+ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_BACKGROUND_FUNCTION_NAME}`;
79
+
80
+ /**
81
+ * Resolve the path the foreground POST should self-dispatch the chat background
82
+ * worker to.
83
+ *
84
+ * On hosted Netlify (`NETLIFY` truthy and not `netlify dev`) the standalone
85
+ * async background function is reachable ONLY at its direct function URL
86
+ * (`/.netlify/functions/server-agent-background`); POSTing the framework route
87
+ * `AGENT_CHAT_PROCESS_RUN_PATH` would land on Nitro's synchronous `/*` catch-all
88
+ * (the ~60s `server` function) and NEVER get the 15-min budget. The standalone
89
+ * function's entry rewrites the incoming path back to `AGENT_CHAT_PROCESS_RUN_PATH`
90
+ * before delegating to the Nitro handler, so the `_process-run` plugin still
91
+ * dispatches it — only the OUTER URL differs.
92
+ *
93
+ * Everywhere else (local dev, `netlify dev`, non-Netlify hosts where the
94
+ * standalone function does not exist) we keep dispatching to the framework route
95
+ * directly — the same in-process catch-all handles it, and there is no separate
96
+ * async function to reach. The HMAC token (signed over the runId) is unchanged
97
+ * either way; only the URL path differs.
98
+ */
99
+ export function resolveAgentChatProcessRunDispatchPath(): string {
100
+ if (
101
+ process.env.NETLIFY &&
102
+ process.env.NETLIFY !== "false" &&
103
+ process.env.NETLIFY_LOCAL !== "true"
104
+ ) {
105
+ return AGENT_BACKGROUND_FUNCTION_URL_PATH;
106
+ }
107
+ return AGENT_CHAT_PROCESS_RUN_PATH;
108
+ }
109
+
54
110
  /**
55
111
  * Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app
56
112
  * opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).
@@ -65,10 +65,10 @@ import {
65
65
  } from "./run-manager.js";
66
66
  import type { ActiveRun } from "./run-manager.js";
67
67
  import {
68
- AGENT_CHAT_PROCESS_RUN_PATH,
69
68
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
70
69
  isAgentChatDurableBackgroundEnabled,
71
70
  isInBackgroundFunctionRuntime,
71
+ resolveAgentChatProcessRunDispatchPath,
72
72
  } from "./durable-background.js";
73
73
  import { fireInternalDispatch } from "../server/self-dispatch.js";
74
74
  import { readBody } from "../server/h3-helpers.js";
@@ -4394,7 +4394,15 @@ export function createProductionAgentHandler(
4394
4394
  try {
4395
4395
  await fireInternalDispatch({
4396
4396
  event,
4397
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4397
+ // On hosted Netlify this is the standalone background function's DIRECT
4398
+ // url (`/.netlify/functions/server-agent-background`) — POSTing the
4399
+ // framework `_process-run` path would land on Nitro's synchronous `/*`
4400
+ // catch-all and never get the 15-min budget. The function's entry
4401
+ // rewrites the path back to AGENT_CHAT_PROCESS_RUN_PATH before
4402
+ // delegating to Nitro, so the `_process-run` plugin still runs and the
4403
+ // Authorization Bearer HMAC survives. Off-Netlify it stays the
4404
+ // framework path (handled in-process).
4405
+ path: resolveAgentChatProcessRunDispatchPath(),
4398
4406
  taskId: runId,
4399
4407
  body: {
4400
4408
  ...body,
@@ -4636,7 +4644,12 @@ export function createProductionAgentHandler(
4636
4644
  try {
4637
4645
  await fireInternalDispatch({
4638
4646
  event,
4639
- path: AGENT_CHAT_PROCESS_RUN_PATH,
4647
+ // Continuation chunks must also land on the standalone async
4648
+ // background function (its direct url on hosted Netlify) so
4649
+ // each chunk keeps the 15-min budget; same path-resolution as
4650
+ // the initial dispatch. The function entry rewrites the path
4651
+ // back to `_process-run` for the Nitro router.
4652
+ path: resolveAgentChatProcessRunDispatchPath(),
4640
4653
  taskId: nextRunId,
4641
4654
  body: {
4642
4655
  ...body,
@@ -34,7 +34,10 @@ import {
34
34
  } from "./workspace-core.js";
35
35
  import { generateActionRegistryForProject } from "../vite/action-types-plugin.js";
36
36
  import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
37
- import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
37
+ import {
38
+ AGENT_BACKGROUND_FUNCTION_NAME,
39
+ AGENT_CHAT_PROCESS_RUN_PATH,
40
+ } from "../agent/durable-background.js";
38
41
  import {
39
42
  AGENT_NATIVE_SOCIAL_IMAGE_ALT,
40
43
  AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER,
@@ -1502,36 +1505,52 @@ export function isDurableBackgroundDeployEnabled(): boolean {
1502
1505
  }
1503
1506
 
1504
1507
  /**
1505
- * Single-template Netlify build: emit a SECOND function whose name ends in
1506
- * `-background`, re-exporting the same `main.mjs` handler bundle, so the chat
1507
- * `_process-run` POST lands on Netlify's async (15-min) function instead of the
1508
- * synchronous one. Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1508
+ * Single-template Netlify build: emit a STANDALONE async background function so
1509
+ * the chat `_process-run` worker runs on Netlify's async (15-min) function
1510
+ * reached at its DIRECT url, instead of the synchronous `/*` catch-all.
1511
+ * Additive + flag-gated (see `isDurableBackgroundDeployEnabled`).
1509
1512
  *
1510
- * Nitro's `netlify` preset emits a single function at
1511
- * `.netlify/functions-internal/server` (`server.mjs` → `main.mjs`). We copy
1512
- * that directory to a sibling `<...>-background` function and write an entry
1513
- * with a `config.path` of the process-run route.
1513
+ * Nitro's `netlify` preset emits a single synchronous function at
1514
+ * `.netlify/functions-internal/server` (`server.mjs` → `main.mjs`, `config.path`
1515
+ * `/*`). We copy that bundle to a STANDALONE function in the STANDARD functions
1516
+ * dir `.netlify/functions/server-agent-background` and write an entry that:
1517
+ * 1. Sets `globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true` at cold start
1518
+ * (read back by `isInBackgroundFunctionRuntime()` so the worker takes the
1519
+ * ~13-min soft-timeout). A `globalThis` flag — NOT `process.env` — keeps the
1520
+ * no-env-mutation guard satisfied and carries no cross-request state.
1521
+ * 2. Declares `export const config = { background: true }` with NO `path`. With
1522
+ * no custom `path`, Netlify exposes the function at the DEFAULT url
1523
+ * `/.netlify/functions/server-agent-background` and `background:true` makes
1524
+ * it ASYNC (immediate HTTP 202 ack, 15-min budget). The foreground POST
1525
+ * self-dispatches to that direct url
1526
+ * (`resolveAgentChatProcessRunDispatchPath`), BYPASSING Nitro's `/*`
1527
+ * catch-all entirely.
1528
+ * 3. REWRITES the incoming request path to `AGENT_CHAT_PROCESS_RUN_PATH` before
1529
+ * delegating to the Nitro handler (`./main.mjs` default export — a Netlify
1530
+ * v2 / Web-standard `async (Request) => Response` handler), so the Nitro
1531
+ * router dispatches it to the `_process-run` plugin. Method, ALL headers
1532
+ * (critically the HMAC `Authorization: Bearer` the plugin verifies) and the
1533
+ * body are preserved by cloning the incoming `Request` with only its URL
1534
+ * pathname rewritten.
1514
1535
  *
1515
- * The function declares `background: true` so Netlify invokes it ASYNC (HTTP
1516
- * 202 ack) with the 15-min budget, and a `config.path` of the process-run route
1517
- * so the `_process-run` self-dispatch lands on it. The earlier version set the
1518
- * path but omitted `background: true` and relied on the legacy `-background`
1519
- * filename which the `config` object overrides so Netlify served it
1520
- * SYNCHRONOUSLY (~60s) and the durable worker never got the 15-min budget
1521
- * (confirmed live: a POST to the process-run path returned a synchronous 401
1522
- * from the handler instead of a 202 async ack).
1536
+ * WHY this replaced the previous `config.path` approach: a function with a custom
1537
+ * `config.path` is reachable ONLY at that path (not at its default function url)
1538
+ * and `functions-internal` is not exposed at the default url at all. In prod,
1539
+ * Netlify routed `/_agent-native/agent-chat/_process-run` to the synchronous
1540
+ * Nitro `server` catch-all, NOT to the background functionverified live: a
1541
+ * POST to the process-run path returned a SYNCHRONOUS 401 from the handler
1542
+ * (the Nitro plugin ran) instead of a 202 async ack. A standalone function at
1543
+ * its direct url cannot be shadowed by the catch-all.
1523
1544
  *
1524
- * ⚠️ One Netlify runtime behavior still resolves only on a real deploy: the
1525
- * routing precedence between this function's `config.path` and Nitro's catch-all
1526
- * for that exact path. If the catch-all wins, the run still completes via the
1527
- * 40s soft-timeout path on the sync function (no durable win, no regression).
1528
- * See docs/design/durable-agent-runs.md (Open risks #1).
1545
+ * Safety net regardless of Netlify routing nuance: if the direct-url dispatch
1546
+ * fast-fails (e.g. 404 when this function was not emitted), the foreground
1547
+ * handler degrades to an inline 40s synchronous run (see production-agent.ts).
1529
1548
  */
1530
1549
  export function emitSingleTemplateNetlifyBackgroundFunction(
1531
1550
  projectCwd: string,
1532
1551
  ): void {
1533
- const functionsDir = path.join(projectCwd, ".netlify", "functions-internal");
1534
- const serverDir = path.join(functionsDir, "server");
1552
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
1553
+ const serverDir = path.join(internalDir, "server");
1535
1554
  if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
1536
1555
  // Nitro output layout differs from what we expected — skip rather than
1537
1556
  // guess. The single-function deploy is unaffected.
@@ -1541,13 +1560,19 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1541
1560
  );
1542
1561
  return;
1543
1562
  }
1544
- const backgroundName = "server-agent-background";
1563
+ const backgroundName = AGENT_BACKGROUND_FUNCTION_NAME;
1564
+ // Emit into the STANDARD functions dir (NOT functions-internal) so Netlify
1565
+ // exposes the function at its default url `/.netlify/functions/<name>`.
1566
+ const functionsDir = path.join(projectCwd, ".netlify", "functions");
1545
1567
  const dest = path.join(functionsDir, backgroundName);
1568
+ fs.mkdirSync(functionsDir, { recursive: true });
1546
1569
  fs.rmSync(dest, { recursive: true, force: true });
1547
1570
  copyDir(serverDir, dest);
1548
- // Drop the original Nitro entry so our background entry is the entrypoint.
1571
+ // Drop the original Nitro `/*` entry so our standalone entry is the entrypoint
1572
+ // and the copied bundle does NOT re-register the catch-all `config.path`.
1549
1573
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1550
1574
 
1575
+ const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1551
1576
  const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1552
1577
  // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
1553
1578
  // in this function. The deployed Lambda name is NOT guaranteed to end in
@@ -1557,26 +1582,47 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
1557
1582
  // set-once isolate marker read back by isInBackgroundFunctionRuntime().
1558
1583
  globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
1559
1584
 
1585
+ // The framework route the Nitro router dispatches to (the _process-run plugin).
1586
+ const PROCESS_RUN_PATH = ${processRunPath};
1587
+
1560
1588
  let cachedHandler;
1561
1589
 
1562
- export default async function handler(...args) {
1590
+ // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1591
+ // Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
1592
+ // We are reached at this function's DIRECT url
1593
+ // (/.netlify/functions/${backgroundName}); rewrite the path to PROCESS_RUN_PATH
1594
+ // so Nitro routes it to the _process-run plugin, preserving method, ALL headers
1595
+ // (the HMAC Authorization: Bearer MUST survive — the plugin verifies it) and the
1596
+ // body. We clone the incoming Request with only its URL pathname rewritten;
1597
+ // query + origin are preserved.
1598
+ export default async function handler(request) {
1563
1599
  cachedHandler ??= (await import("./main.mjs")).default;
1564
- return cachedHandler(...args);
1600
+ const url = new URL(request.url);
1601
+ url.pathname = PROCESS_RUN_PATH;
1602
+ // Read the body once and pass it through. GET/HEAD have no body.
1603
+ const method = request.method || "POST";
1604
+ const hasBody = method !== "GET" && method !== "HEAD";
1605
+ const body = hasBody ? await request.text() : undefined;
1606
+ const rewritten = new Request(url.toString(), {
1607
+ method,
1608
+ headers: request.headers,
1609
+ body,
1610
+ });
1611
+ return cachedHandler(rewritten);
1565
1612
  }
1566
1613
 
1567
1614
  export const config = {
1568
1615
  name: "agent background handler",
1569
1616
  generator: "agent-native build",
1570
- // background: true is what actually makes Netlify invoke this ASYNCHRONOUSLY
1571
- // (immediate HTTP 202 ack) with the 15-minute budget. Without it, a function
1572
- // that declares a custom \`path\` is served SYNCHRONOUSLY (~60s) even when its
1573
- // file name ends in "-background" the \`config\` object overrides the legacy
1574
- // filename convention. That omission is why the durable worker was capped at
1575
- // ~60s in prod (verified live: POST to the process-run path returned a
1576
- // synchronous 401 from the handler, not a 202 async ack). See Netlify docs:
1577
- // build/functions/background-functions + build/functions/configuration.
1617
+ // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
1618
+ // 202 ack) with the 15-minute budget. NO custom \`path\`: that keeps the
1619
+ // function reachable at its DEFAULT url /.netlify/functions/${backgroundName},
1620
+ // which BYPASSES Nitro's /* catch-all. The previous version set a custom
1621
+ // \`config.path\` and Netlify routed the process-run path to the SYNCHRONOUS
1622
+ // catch-all instead (verified live: a synchronous 401 from the handler, not a
1623
+ // 202 async ack). See Netlify docs: build/functions/background-functions +
1624
+ // build/functions/configuration.
1578
1625
  background: true,
1579
- path: ${JSON.stringify([AGENT_CHAT_PROCESS_RUN_PATH])},
1580
1626
  nodeBundler: "none",
1581
1627
  includedFiles: ["**"],
1582
1628
  preferStatic: false,
@@ -1584,9 +1630,10 @@ export const config = {
1584
1630
  `;
1585
1631
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
1586
1632
  console.log(
1587
- `[build] Emitted durable-background function "${backgroundName}" ` +
1588
- `(path ${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification ` +
1589
- `of Netlify async routing see docs/design/durable-agent-runs.md.`,
1633
+ `[build] Emitted standalone durable-background function "${backgroundName}" ` +
1634
+ `at /.netlify/functions/${backgroundName} (async, rewrites to ` +
1635
+ `${AGENT_CHAT_PROCESS_RUN_PATH}). REQUIRES real-deploy verification of ` +
1636
+ `Netlify async invocation — see docs/design/durable-agent-runs.md.`,
1590
1637
  );
1591
1638
  }
1592
1639
 
@@ -24,10 +24,6 @@ function seededResponse(
24
24
  };
25
25
  }
26
26
 
27
- function isEmptyConfig(config: Record<string, unknown>): boolean {
28
- return !Array.isArray(config.panels) || config.panels.length === 0;
29
- }
30
-
31
27
  export default defineAction({
32
28
  description:
33
29
  "Get a SQL analytics dashboard by ID, including its full panel config, visibility, and access metadata.",
@@ -78,10 +74,6 @@ export default defineAction({
78
74
  });
79
75
  }
80
76
  const config = dash.config as Record<string, unknown>;
81
- if (isEmptyConfig(config)) {
82
- const seed = loadDashboardSeed(args.id);
83
- if (seed) return seededResponse(args.id, seed);
84
- }
85
77
  return {
86
78
  id: args.id,
87
79
  ...config,
@@ -466,7 +466,9 @@ export default defineAction({
466
466
  .optional()
467
467
  .describe("Replace the whole dashboard config (or a JSON string)."),
468
468
  }),
469
- http: false,
469
+ // The SQL dashboard editor persists user edits through callAction(), which
470
+ // needs this action mounted under /_agent-native/actions/update-dashboard.
471
+ http: { method: "POST" },
470
472
  mcpApp: {
471
473
  compactCatalog: true,
472
474
  resource: embedApp({
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { SqlHighlight } from "@/components/SqlHighlight";
4
+
5
+ export interface SqlEditorProps extends Omit<
6
+ React.TextareaHTMLAttributes<HTMLTextAreaElement>,
7
+ "value"
8
+ > {
9
+ value: string;
10
+ }
11
+
12
+ export const SqlEditor = React.forwardRef<HTMLTextAreaElement, SqlEditorProps>(
13
+ ({ className, onScroll, value, disabled, readOnly, ...props }, ref) => {
14
+ const highlightRef = React.useRef<HTMLPreElement | null>(null);
15
+
16
+ const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
17
+ const highlight = highlightRef.current;
18
+ if (highlight) {
19
+ highlight.scrollTop = event.currentTarget.scrollTop;
20
+ highlight.scrollLeft = event.currentTarget.scrollLeft;
21
+ }
22
+ onScroll?.(event);
23
+ };
24
+
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "relative min-h-[200px] rounded-md border border-input bg-background ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
29
+ disabled && "cursor-not-allowed opacity-50",
30
+ className,
31
+ )}
32
+ >
33
+ <SqlHighlight
34
+ ref={highlightRef}
35
+ aria-hidden="true"
36
+ sql={value || " "}
37
+ preClassName="pointer-events-none absolute inset-0 overflow-hidden rounded-md bg-transparent p-3 text-xs leading-5"
38
+ className="text-foreground"
39
+ />
40
+ <textarea
41
+ ref={ref}
42
+ value={value}
43
+ disabled={disabled}
44
+ readOnly={readOnly}
45
+ onScroll={handleScroll}
46
+ spellCheck={false}
47
+ className={cn(
48
+ "relative z-10 min-h-[inherit] w-full resize-y rounded-md border-0 bg-transparent p-3 font-mono text-xs leading-5 text-transparent caret-foreground outline-none selection:bg-primary/30 placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed",
49
+ readOnly && "cursor-default",
50
+ )}
51
+ {...props}
52
+ />
53
+ </div>
54
+ );
55
+ },
56
+ );
57
+ SqlEditor.displayName = "SqlEditor";
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "@/lib/utils";
3
3
 
4
4
  const KEYWORDS = new Set([
@@ -266,36 +266,35 @@ const TOKEN_CLASS: Record<TokenType, string> = {
266
266
  plain: "",
267
267
  };
268
268
 
269
- interface SqlHighlightProps {
269
+ interface SqlHighlightProps extends HTMLAttributes<HTMLPreElement> {
270
270
  sql: string;
271
- className?: string;
272
271
  preClassName?: string;
273
272
  }
274
273
 
275
- export function SqlHighlight({
276
- sql,
277
- className,
278
- preClassName,
279
- }: SqlHighlightProps) {
280
- const tokens = tokenize(sql);
281
- const nodes: ReactNode[] = tokens.map((tok, idx) => {
282
- const cls = TOKEN_CLASS[tok.type];
283
- if (!cls) return tok.value;
274
+ export const SqlHighlight = forwardRef<HTMLPreElement, SqlHighlightProps>(
275
+ function SqlHighlight({ sql, className, preClassName, ...props }, ref) {
276
+ const tokens = tokenize(sql);
277
+ const nodes: ReactNode[] = tokens.map((tok, idx) => {
278
+ const cls = TOKEN_CLASS[tok.type];
279
+ if (!cls) return tok.value;
280
+ return (
281
+ <span key={idx} className={cls}>
282
+ {tok.value}
283
+ </span>
284
+ );
285
+ });
284
286
  return (
285
- <span key={idx} className={cls}>
286
- {tok.value}
287
- </span>
287
+ <pre
288
+ ref={ref}
289
+ {...props}
290
+ className={cn(
291
+ "font-mono text-xs leading-5 whitespace-pre-wrap break-words",
292
+ preClassName,
293
+ className,
294
+ )}
295
+ >
296
+ <code>{nodes}</code>
297
+ </pre>
288
298
  );
289
- });
290
- return (
291
- <pre
292
- className={cn(
293
- "font-mono text-xs leading-5 whitespace-pre-wrap break-words",
294
- preClassName,
295
- className,
296
- )}
297
- >
298
- <code>{nodes}</code>
299
- </pre>
300
- );
301
- }
299
+ },
300
+ );