@agent-native/core 0.107.0 → 0.107.1

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 (131) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +10 -0
  3. package/corpus/core/docs/content/integrations.mdx +27 -27
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +19 -0
  6. package/corpus/core/src/agent/run-store.ts +98 -2
  7. package/corpus/core/src/client/AssistantChat.tsx +13 -0
  8. package/corpus/core/src/client/CommandMenu.tsx +5 -0
  9. package/corpus/core/src/client/active-run-state.ts +34 -0
  10. package/corpus/core/src/client/agent-chat-adapter.ts +4 -0
  11. package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +41 -11
  12. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +27 -9
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +45 -0
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +30 -9
  15. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +2 -2
  16. package/corpus/core/src/localization/default-messages.ts +4 -1
  17. package/corpus/core/src/secrets/register-framework-secrets.ts +28 -0
  18. package/corpus/core/src/server/agent-chat-plugin.ts +29 -1
  19. package/corpus/core/src/vite/client.ts +24 -0
  20. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +29 -21
  21. package/corpus/templates/analytics/AGENTS.md +8 -0
  22. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  23. package/corpus/templates/analytics/actions/update-dashboard.ts +6 -2
  24. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +35 -7
  25. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -3
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -1
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/extension-slot.ts +11 -0
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +27 -0
  29. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +6 -0
  30. package/corpus/templates/analytics/changelog/2026-07-17-dashboard-extension-boxes.md +6 -0
  31. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +8 -0
  32. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  33. package/corpus/templates/assets/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  34. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +6 -1
  35. package/corpus/templates/brain/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  36. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +1 -1
  37. package/corpus/templates/calendar/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  38. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  39. package/corpus/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  40. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  41. package/corpus/templates/clips/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  42. package/corpus/templates/content/actions/_content-space-access.ts +45 -2
  43. package/corpus/templates/content/actions/create-content-database.ts +41 -7
  44. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  45. package/corpus/templates/content/app/root.tsx +1 -10
  46. package/corpus/templates/content/changelog/2026-07-17-creating-a-database-now-completes-reliably-in-hosted-workspa.md +6 -0
  47. package/corpus/templates/content/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  48. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +4 -1
  49. package/corpus/templates/dispatch/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  50. package/corpus/templates/forms/app/components/layout/Layout.tsx +2 -4
  51. package/corpus/templates/forms/app/components/ui/popover.tsx +25 -1
  52. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +6 -1
  53. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +13 -10
  54. package/corpus/templates/forms/app/root.tsx +6 -1
  55. package/corpus/templates/forms/changelog/2026-07-17-form-builder-popovers-stay-within-the-viewport.md +6 -0
  56. package/corpus/templates/forms/changelog/2026-07-17-form-editor-loading-matches-current-sidebar-free-layout.md +6 -0
  57. package/corpus/templates/forms/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  58. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
  59. package/corpus/templates/mail/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  60. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +14 -3
  61. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +8 -1
  62. package/corpus/templates/plan/app/root.tsx +0 -2
  63. package/corpus/templates/plan/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  64. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  65. package/corpus/templates/slides/app/context/DeckContext.tsx +23 -23
  66. package/corpus/templates/slides/changelog/2026-07-17-duplicated-slides-now-stay-in-the-deck.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  68. package/corpus/templates/tasks/app/components/layout/Layout.tsx +2 -13
  69. package/corpus/templates/tasks/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it.md +6 -0
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +13 -1
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-store.d.ts +17 -0
  74. package/dist/agent/run-store.d.ts.map +1 -1
  75. package/dist/agent/run-store.js +84 -2
  76. package/dist/agent/run-store.js.map +1 -1
  77. package/dist/client/AssistantChat.d.ts.map +1 -1
  78. package/dist/client/AssistantChat.js +10 -1
  79. package/dist/client/AssistantChat.js.map +1 -1
  80. package/dist/client/CommandMenu.d.ts.map +1 -1
  81. package/dist/client/CommandMenu.js +4 -0
  82. package/dist/client/CommandMenu.js.map +1 -1
  83. package/dist/client/active-run-state.d.ts +7 -0
  84. package/dist/client/active-run-state.d.ts.map +1 -1
  85. package/dist/client/active-run-state.js +28 -0
  86. package/dist/client/active-run-state.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +4 -1
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
  91. package/dist/client/resources/McpConnectionSuggestion.js +27 -8
  92. package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
  93. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  94. package/dist/client/resources/McpIntegrationDialog.js +4 -2
  95. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  96. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  97. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  98. package/dist/client/resources/mcp-integration-catalog.js +33 -0
  99. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  100. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  101. package/dist/client/sharing/ShareButton.js +15 -12
  102. package/dist/client/sharing/ShareButton.js.map +1 -1
  103. package/dist/client/use-agent-chat-home-handoff.d.ts +1 -1
  104. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  105. package/dist/client/use-agent-chat-home-handoff.js +1 -1
  106. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  107. package/dist/collab/awareness.d.ts +2 -2
  108. package/dist/collab/awareness.d.ts.map +1 -1
  109. package/dist/collab/routes.d.ts +1 -1
  110. package/dist/collab/struct-routes.d.ts +1 -1
  111. package/dist/localization/default-messages.d.ts +2 -0
  112. package/dist/localization/default-messages.d.ts.map +1 -1
  113. package/dist/localization/default-messages.js +3 -1
  114. package/dist/localization/default-messages.js.map +1 -1
  115. package/dist/observability/routes.d.ts +5 -5
  116. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  117. package/dist/secrets/register-framework-secrets.js +25 -0
  118. package/dist/secrets/register-framework-secrets.js.map +1 -1
  119. package/dist/secrets/routes.d.ts +9 -9
  120. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  121. package/dist/server/agent-chat-plugin.js +23 -1
  122. package/dist/server/agent-chat-plugin.js.map +1 -1
  123. package/dist/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  124. package/dist/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  125. package/dist/vite/client.d.ts.map +1 -1
  126. package/dist/vite/client.js +20 -0
  127. package/dist/vite/client.js.map +1 -1
  128. package/docs/content/integrations.mdx +27 -27
  129. package/package.json +1 -1
  130. package/src/templates/chat/app/hooks/use-navigation-state.ts +4 -1
  131. package/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
@@ -67,6 +67,7 @@ interface SqlChartCardProps {
67
67
  isDragSource?: boolean;
68
68
  selectedForChat?: boolean;
69
69
  onSelectForChat?: (options?: SelectDashboardPanelOptions) => void;
70
+ extensionContext?: Record<string, unknown> | null;
70
71
  }
71
72
 
72
73
  const PanelDragHandle = memo(function PanelDragHandle({
@@ -122,6 +123,7 @@ export function SqlChartCard({
122
123
  isDragSource = false,
123
124
  selectedForChat = false,
124
125
  onSelectForChat,
126
+ extensionContext,
125
127
  }: SqlChartCardProps) {
126
128
  const t = useT();
127
129
  const queryClient = useQueryClient();
@@ -346,6 +348,7 @@ export function SqlChartCard({
346
348
  panel={panel}
347
349
  resolvedSql={resolvedSql}
348
350
  loadData
351
+ extensionContext={extensionContext}
349
352
  />
350
353
  )}
351
354
  <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
@@ -435,6 +438,7 @@ export function SqlChartCard({
435
438
  panel={panel}
436
439
  resolvedSql={resolvedSql}
437
440
  loadData
441
+ extensionContext={extensionContext}
438
442
  />
439
443
  </ChartFillHeight>
440
444
  </div>
@@ -616,6 +620,7 @@ export function SqlChartCard({
616
620
  resolvedSql={resolvedSql}
617
621
  loadData={shouldLoadData}
618
622
  onExportCsvChange={handleExportCsvChange}
623
+ extensionContext={extensionContext}
619
624
  />
620
625
  </CardContent>
621
626
  </Card>
@@ -627,7 +632,12 @@ export function SqlChartCard({
627
632
  </DialogHeader>
628
633
  <div className="flex min-h-0 flex-1 flex-col overflow-auto">
629
634
  <ChartFillHeight>
630
- <SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
635
+ <SqlChart
636
+ panel={panel}
637
+ resolvedSql={resolvedSql}
638
+ loadData
639
+ extensionContext={extensionContext}
640
+ />
631
641
  </ChartFillHeight>
632
642
  </div>
633
643
  </DialogContent>
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stable extension-point name for a dashboard box. The dashboard and panel ids
3
+ * are encoded independently so arbitrary saved ids cannot collapse into one
4
+ * slot or add path separators to the slots API.
5
+ */
6
+ export function dashboardExtensionSlotId(
7
+ dashboardId: string,
8
+ panelId: string,
9
+ ): string {
10
+ return `analytics.dashboard.${encodeURIComponent(dashboardId)}.panel.${encodeURIComponent(panelId)}`;
11
+ }
@@ -142,6 +142,7 @@ import {
142
142
  resolveFilterVars,
143
143
  } from "./DashboardFilterBar";
144
144
  import { EmailReportDialog } from "./EmailReportDialog";
145
+ import { dashboardExtensionSlotId } from "./extension-slot";
145
146
  import { interpolate } from "./interpolate";
146
147
  import { serializePanelSql } from "./panel-sql";
147
148
  import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog";
@@ -271,6 +272,7 @@ const PanelCell = memo(function PanelCell({
271
272
  onRemovePanel,
272
273
  onEditPanel,
273
274
  onSavePanel,
275
+ dashboardExtensionContext,
274
276
  }: {
275
277
  panel: SqlPanel;
276
278
  vars: Record<string, string>;
@@ -286,6 +288,7 @@ const PanelCell = memo(function PanelCell({
286
288
  onRemovePanel: (panelId: string) => void;
287
289
  onEditPanel: (panel: SqlPanel) => void;
288
290
  onSavePanel: (panel: SqlPanel) => Promise<void>;
291
+ dashboardExtensionContext: Record<string, unknown>;
289
292
  }) {
290
293
  const resolved = useMemo(
291
294
  () =>
@@ -366,6 +369,23 @@ const PanelCell = memo(function PanelCell({
366
369
  isDragSource={isDragSource}
367
370
  selectedForChat={selectedForChat}
368
371
  onSelectForChat={handleSelectForChat}
372
+ extensionContext={
373
+ panel.chartType === "extension"
374
+ ? {
375
+ ...dashboardExtensionContext,
376
+ panel: {
377
+ id: panel.id,
378
+ title: panel.title,
379
+ slotId:
380
+ panel.config?.extensionSlotId ??
381
+ dashboardExtensionSlotId(
382
+ String(dashboardExtensionContext.dashboardId),
383
+ panel.id,
384
+ ),
385
+ },
386
+ }
387
+ : undefined
388
+ }
369
389
  />
370
390
  </div>
371
391
  );
@@ -1966,6 +1986,13 @@ export default function SqlDashboardPage() {
1966
1986
  onRemovePanel={removePanel}
1967
1987
  onEditPanel={openEditPanel}
1968
1988
  onSavePanel={handleSavePanel}
1989
+ dashboardExtensionContext={{
1990
+ dashboardId,
1991
+ dashboardName: dashboard.name,
1992
+ dashboardDescription:
1993
+ dashboard.description ?? null,
1994
+ filters: vars,
1995
+ }}
1969
1996
  />
1970
1997
  <DashboardDropLine
1971
1998
  slot={{
@@ -99,8 +99,14 @@ export interface SqlPanelConfig {
99
99
  /**
100
100
  * Extension panels only (`chartType: "extension"`): id of the extension to
101
101
  * render inline as a sandboxed iframe instead of running the SQL pipeline.
102
+ * Kept for dashboards saved before slot-backed dashboard boxes.
102
103
  */
103
104
  extensionId?: string;
105
+ /**
106
+ * Extension panels only: a named extension-point slot. The dashboard renders
107
+ * its installed extensions as this panel's box and supplies dashboard context.
108
+ */
109
+ extensionSlotId?: string;
104
110
  }
105
111
 
106
112
  export interface SqlPanel {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Dashboards can now host installed extensions as contextual, slot-backed boxes.
@@ -64,6 +64,14 @@ function panelSummaries(config: Record<string, unknown>) {
64
64
  typeof panelConfig.description === "string"
65
65
  ? panelConfig.description
66
66
  : undefined,
67
+ extensionId:
68
+ typeof panelConfig.extensionId === "string"
69
+ ? panelConfig.extensionId
70
+ : undefined,
71
+ extensionSlotId:
72
+ typeof panelConfig.extensionSlotId === "string"
73
+ ? panelConfig.extensionSlotId
74
+ : undefined,
67
75
  };
68
76
  });
69
77
  }
@@ -704,6 +704,7 @@ export default createAgentChatPlugin({
704
704
  analyticsSourceGuidanceOpening() +
705
705
  "DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
706
706
  "DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
707
+ 'DASHBOARD EXTENSION BOX RULE — Analytics dashboards support extension boxes. Insert `chartType: "extension"` panels with `config.extensionSlotId` set to `analytics.dashboard.<dashboard-id>.panel.<panel-id>`, then call `add-extension-slot-target` and `install-extension` with that same id. The install is per-user; the extension box stays on the shared dashboard and receives dashboard/panel/current-filter context. Use `get-sql-dashboard` panel summaries to inspect an existing box; `config.extensionId` is only the legacy direct-embed format. ' +
707
708
  "EXTENSION DATA-REPAIR RULE — When fixing data in an existing extension-backed dashboard or migrated surface such as Risk Meeting, inspect the current dashboard and extension first, then use `update-extension` `patches`/`edits` that change only the data-loading seam. Preserve the existing layout, CSS, copy, and interactions; never reconstruct the full HTML body for a data-only fix. Full-body replacement is blocked unless `allowFullReplacement=true`, which is reserved for an explicit visual rewrite or a complete replacement body supplied by the user. If a focused edit fails, change the target instead of retrying identical arguments. " +
708
709
  'FIRST-PARTY DASHBOARD TIME RULE — AI-generated `source: "first-party"` panels are dashboard-time-bound by default: set `config.timeScope` to `dashboard` and include a matching dashboard time predicate. `{{timeRange}}` requires a matching `filters` entry with `id: "timeRange"` and `type: "select"`; `{{<id>Start}}`/`{{<id>End}}` require a matching `type: "date-range"` filter with that id. Allowed `timeScope` values are `dashboard`, `fixed-window`, `cohort-history`, and `all-time`; use `all-time` only when the user requests full available history and put all-time, lifetime, or historical in the title or description. Server validation rejects unbound first-party SQL. ' +
709
710
  "DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  appBasePath,
3
3
  appPath,
4
+ isAgentChatHomeHandoffActive,
4
5
  markAgentChatHomeHandoff,
5
6
  useAgentRouteState,
6
7
  } from "@agent-native/core/client";
@@ -106,7 +107,11 @@ export function useNavigationState() {
106
107
  return `${path}${params.size ? `?${params.toString()}` : ""}`;
107
108
  },
108
109
  onNavigate: (_command, path) => {
109
- if (location.pathname === "/" && pathnameFromPath(path) !== "/") {
110
+ if (
111
+ location.pathname === "/" &&
112
+ pathnameFromPath(path) !== "/" &&
113
+ isAgentChatHomeHandoffActive("brain")
114
+ ) {
110
115
  markAgentChatHomeHandoff("brain");
111
116
  }
112
117
  },
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -390,7 +390,7 @@ export function AppLayout({ children }: AppLayoutProps) {
390
390
  />
391
391
  <AgentSidebar
392
392
  position="right"
393
- defaultOpen
393
+ defaultOpen={false}
394
394
  emptyStateText={t("agentSidebar.emptyState")}
395
395
  agentPageHref="/agent"
396
396
  suggestions={[
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  appBasePath,
3
+ isAgentChatHomeHandoffActive,
3
4
  appPath,
4
5
  markAgentChatHomeHandoff,
5
6
  useAgentRouteState,
@@ -34,7 +35,9 @@ export function useNavigationState() {
34
35
  isChatPath(location.pathname) &&
35
36
  !isChatPath(pathnameFromPath(path))
36
37
  ) {
37
- markAgentChatHomeHandoff("chat");
38
+ if (isAgentChatHomeHandoffActive("chat")) {
39
+ markAgentChatHomeHandoff("chat");
40
+ }
38
41
  }
39
42
  },
40
43
  });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -559,7 +559,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
559
559
 
560
560
  <AgentSidebar
561
561
  position="right"
562
- defaultOpen={!isMobile}
562
+ defaultOpen={false}
563
563
  emptyStateText={t("navigation.agentEmptyState")}
564
564
  suggestions={[
565
565
  t("navigation.agentSuggestionSummary"),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -1,9 +1,25 @@
1
1
  import { getDbExec } from "@agent-native/core/db";
2
+ import { table, text } from "@agent-native/core/db/schema";
2
3
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
- import { eq } from "drizzle-orm";
4
+ import { and, eq, sql } from "drizzle-orm";
4
5
 
5
6
  import { getDb, schema } from "../server/db/index.js";
6
7
 
8
+ // Keep these lightweight table references local. Importing the public org
9
+ // barrel also initializes auth's long-lived cleanup timer, which breaks test
10
+ // suites that intentionally drain fake timers.
11
+ const organizations = table("organizations", {
12
+ id: text("id").primaryKey(),
13
+ name: text("name").notNull(),
14
+ createdBy: text("created_by").notNull(),
15
+ });
16
+
17
+ const orgMembers = table("org_members", {
18
+ orgId: text("org_id").notNull(),
19
+ email: text("email").notNull(),
20
+ role: text("role").notNull(),
21
+ });
22
+
7
23
  export type ContentSpaceRole = "viewer" | "editor" | "owner";
8
24
 
9
25
  export type ContentSpaceAccess = {
@@ -21,7 +37,32 @@ export function normalizeContentSpaceEmail(email: string): string {
21
37
  export async function getContentOrganizationMembership(
22
38
  orgId: string,
23
39
  userEmail: string,
40
+ options: { db?: any } = {},
24
41
  ): Promise<{ role: string; name: string; createdBy: string } | null> {
42
+ if (options.db) {
43
+ const [row] = await options.db
44
+ .select({
45
+ role: orgMembers.role,
46
+ name: organizations.name,
47
+ createdBy: organizations.createdBy,
48
+ })
49
+ .from(orgMembers)
50
+ .innerJoin(organizations, eq(organizations.id, orgMembers.orgId))
51
+ .where(
52
+ and(
53
+ eq(orgMembers.orgId, orgId),
54
+ sql`LOWER(${orgMembers.email}) = ${normalizeContentSpaceEmail(userEmail)}`,
55
+ ),
56
+ )
57
+ .limit(1);
58
+ return row
59
+ ? {
60
+ role: String(row.role ?? "member").toLowerCase(),
61
+ name: row.name,
62
+ createdBy: row.createdBy,
63
+ }
64
+ : null;
65
+ }
25
66
  const result = await getDbExec().execute({
26
67
  sql: `SELECT m.role AS role, o.name AS name, o.created_by AS "createdBy"
27
68
  FROM org_members m
@@ -90,11 +131,12 @@ export async function listContentOrganizationMemberships(userEmail: string) {
90
131
  export async function resolveContentSpaceAccess(
91
132
  spaceId: string,
92
133
  requiredRole: "viewer" | "editor" = "viewer",
134
+ options: { db?: any } = {},
93
135
  ): Promise<ContentSpaceAccess> {
94
136
  const userEmail = getRequestUserEmail();
95
137
  if (!userEmail) throw new Error("no authenticated user");
96
138
  const normalizedUserEmail = normalizeContentSpaceEmail(userEmail);
97
- const [space] = await getDb()
139
+ const [space] = await (options.db ?? getDb())
98
140
  .select()
99
141
  .from(schema.contentSpaces)
100
142
  .where(eq(schema.contentSpaces.id, spaceId));
@@ -115,6 +157,7 @@ export async function resolveContentSpaceAccess(
115
157
  const membership = await getContentOrganizationMembership(
116
158
  space.orgId,
117
159
  normalizedUserEmail,
160
+ options,
118
161
  );
119
162
  if (!membership)
120
163
  throw new Error(`Not authorized for Content space "${spaceId}"`);
@@ -5,7 +5,13 @@ import {
5
5
  getRequestOrgId,
6
6
  getRequestUserEmail,
7
7
  } from "@agent-native/core/server/request-context";
8
- import { assertAccess, type ShareRole } from "@agent-native/core/sharing";
8
+ import {
9
+ accessFilter,
10
+ assertAccess,
11
+ currentAccess,
12
+ ForbiddenError,
13
+ type ShareRole,
14
+ } from "@agent-native/core/sharing";
9
15
  import { and, eq, sql } from "drizzle-orm";
10
16
  import { z } from "zod";
11
17
 
@@ -158,9 +164,34 @@ export async function resolveContentDatabaseSpace(
158
164
  return spaceId;
159
165
  }
160
166
 
167
+ async function assertDocumentEditorAccess(db: any, documentId: string) {
168
+ const [document] = await db
169
+ .select()
170
+ .from(schema.documents)
171
+ .where(
172
+ and(
173
+ eq(schema.documents.id, documentId),
174
+ accessFilter(
175
+ schema.documents,
176
+ schema.documentShares,
177
+ currentAccess(),
178
+ "editor",
179
+ ),
180
+ ),
181
+ );
182
+ if (!document) {
183
+ throw new ForbiddenError(`No editor access to document ${documentId}`);
184
+ }
185
+ return document;
186
+ }
187
+
161
188
  export async function createContentDatabaseRecord(
162
189
  args: CreateDatabaseRequest,
163
- options: { db?: any; spaceId?: string } = {},
190
+ options: {
191
+ db?: any;
192
+ spaceId?: string;
193
+ resolveSpaceAccess?: typeof resolveContentSpaceAccess;
194
+ } = {},
164
195
  ): Promise<string> {
165
196
  const db = options.db ?? getDb();
166
197
  const now = new Date().toISOString();
@@ -178,8 +209,7 @@ export async function createContentDatabaseRecord(
178
209
  }> = [];
179
210
 
180
211
  if (documentId) {
181
- const access = await assertAccess("document", documentId, "editor");
182
- const document = access.resource;
212
+ const document = await assertDocumentEditorAccess(db, documentId);
183
213
  ownerEmail = document.ownerEmail as string;
184
214
  orgId = (document.orgId as string | null) ?? null;
185
215
  spaceId = (document.spaceId as string | null) ?? spaceId;
@@ -230,8 +260,7 @@ export async function createContentDatabaseRecord(
230
260
  let hideFromSearch = 0;
231
261
 
232
262
  if (parentId) {
233
- const parentAccess = await assertAccess("document", parentId, "editor");
234
- const parent = parentAccess.resource;
263
+ const parent = await assertDocumentEditorAccess(db, parentId);
235
264
  ownerEmail = parent.ownerEmail as string;
236
265
  orgId = (parent.orgId as string | null) ?? null;
237
266
  spaceId = (parent.spaceId as string | null) ?? spaceId;
@@ -257,7 +286,12 @@ export async function createContentDatabaseRecord(
257
286
  "A top-level database requires a resolved Content space",
258
287
  );
259
288
  }
260
- const spaceAccess = await resolveContentSpaceAccess(spaceId, "editor");
289
+ const spaceAccess = await (
290
+ options.resolveSpaceAccess ?? resolveContentSpaceAccess
291
+ )(spaceId, "editor", { db });
292
+ if (spaceAccess.space.id !== spaceId) {
293
+ throw new Error("Resolved Content space does not match the request");
294
+ }
261
295
  ownerEmail = getRequestUserEmail() ?? ownerEmail;
262
296
  orgId = spaceAccess.space.orgId;
263
297
  visibility = orgId ? "org" : "private";
@@ -189,7 +189,7 @@ export function Layout({ children }: LayoutProps) {
189
189
  )}
190
190
  <AgentSidebar
191
191
  position="right"
192
- defaultOpen={!isMobile && !isNarrowDesktop}
192
+ defaultOpen={false}
193
193
  agentPageHref="/agent"
194
194
  emptyStateText={t("chat.emptyState")}
195
195
  suggestions={[
@@ -479,15 +479,6 @@ function PublicAgentShell({ children }: { children: React.ReactNode }) {
479
479
 
480
480
  useEffect(() => setMounted(true), []);
481
481
 
482
- useEffect(() => {
483
- if (!mounted) return;
484
- if (!window.matchMedia("(min-width: 768px)").matches) return;
485
- const id = window.setTimeout(() => {
486
- window.dispatchEvent(new Event("agent-panel:open"));
487
- }, 0);
488
- return () => window.clearTimeout(id);
489
- }, [mounted]);
490
-
491
482
  const content = <>{children}</>;
492
483
 
493
484
  if (!mounted) {
@@ -503,7 +494,7 @@ function PublicAgentShell({ children }: { children: React.ReactNode }) {
503
494
  return (
504
495
  <AgentSidebar
505
496
  position="right"
506
- defaultOpen
497
+ defaultOpen={false}
507
498
  defaultSidebarWidth={420}
508
499
  emptyStateText={t("chat.publicEmptyState")}
509
500
  suggestions={[
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Creating a database now completes reliably in hosted workspaces
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  appBasePath,
3
3
  appPath,
4
+ isAgentChatHomeHandoffActive,
4
5
  markAgentChatHomeHandoff,
5
6
  useAgentRouteState,
6
7
  } from "@agent-native/core/client";
@@ -54,7 +55,9 @@ export function useNavigationState(extensions?: DispatchExtensionConfig) {
54
55
  routerPath(location.pathname) === "/chat" &&
55
56
  pathnameFromPath(path) !== "/chat"
56
57
  ) {
57
- markAgentChatHomeHandoff("dispatch");
58
+ if (isAgentChatHomeHandoffActive("dispatch")) {
59
+ markAgentChatHomeHandoff("dispatch");
60
+ }
58
61
  }
59
62
  },
60
63
  });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ The agent chat sidebar stays closed until you open it or start a chat handoff.
@@ -51,8 +51,6 @@ export function Layout({ children }: LayoutProps) {
51
51
  if (!formId) return null;
52
52
  return { type: "form" as const, id: formId };
53
53
  }, [location.pathname]);
54
- const sidebarScope = chatHomeHandoffActive ? null : formScope;
55
-
56
54
  if (BARE_ROUTES.has(location.pathname)) {
57
55
  return <>{children}</>;
58
56
  }
@@ -89,7 +87,7 @@ export function Layout({ children }: LayoutProps) {
89
87
  <AgentSidebar
90
88
  position="right"
91
89
  agentPageHref="/agent"
92
- defaultOpen
90
+ defaultOpen={false}
93
91
  chatViewTransition
94
92
  storageKey="forms"
95
93
  browserTabId={TAB_ID}
@@ -101,7 +99,7 @@ export function Layout({ children }: LayoutProps) {
101
99
  t("agent.suggestionSubmissions"),
102
100
  t("agent.suggestionExport"),
103
101
  ]}
104
- scope={sidebarScope}
102
+ scope={formScope}
105
103
  >
106
104
  <div className="flex h-full flex-1 flex-col overflow-hidden">
107
105
  {showHeader ? <Header /> : null}
@@ -1 +1,25 @@
1
- export * from "@agent-native/toolkit/ui/popover";
1
+ import {
2
+ Popover,
3
+ PopoverAnchor,
4
+ PopoverContent as ToolkitPopoverContent,
5
+ PopoverTrigger,
6
+ } from "@agent-native/toolkit/ui/popover";
7
+ import * as React from "react";
8
+
9
+ import { cn } from "@/lib/utils";
10
+
11
+ const PopoverContent = React.forwardRef<
12
+ React.ElementRef<typeof ToolkitPopoverContent>,
13
+ React.ComponentPropsWithoutRef<typeof ToolkitPopoverContent>
14
+ >(({ className, collisionPadding = 16, ...props }, ref) => (
15
+ <ToolkitPopoverContent
16
+ ref={ref}
17
+ collisionPadding={collisionPadding}
18
+ className={cn("max-w-[calc(100vw-2rem)]", className)}
19
+ {...props}
20
+ />
21
+ ));
22
+
23
+ PopoverContent.displayName = ToolkitPopoverContent.displayName;
24
+
25
+ export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  appBasePath,
3
+ isAgentChatHomeHandoffActive,
3
4
  markAgentChatHomeHandoff,
4
5
  useAgentRouteState,
5
6
  } from "@agent-native/core/client";
@@ -131,7 +132,11 @@ export function useNavigationState() {
131
132
  navigateOptions: { flushSync: true, replace: true },
132
133
  onNavigate: (_command, path) => {
133
134
  void prewarmFormsRoutePath(path);
134
- if (location.pathname === "/ask" && path !== "/ask") {
135
+ if (
136
+ location.pathname === "/ask" &&
137
+ path !== "/ask" &&
138
+ isAgentChatHomeHandoffActive("forms")
139
+ ) {
135
140
  markAgentChatHomeHandoff("forms");
136
141
  }
137
142
  },
@@ -359,10 +359,19 @@ export function FormBuilderPage() {
359
359
  <Skeleton className="h-8 w-20 rounded-md" />
360
360
  </div>
361
361
  </div>
362
- {/* Body: builder + properties */}
363
- <div className="flex flex-1 overflow-hidden">
364
- <div className="flex-1 overflow-auto p-4 sm:p-6">
365
- <div className="max-w-2xl mx-auto space-y-4">
362
+ {/* Tabs */}
363
+ <div className="border-b border-border px-2 sm:px-4 py-2 shrink-0 overflow-hidden">
364
+ <div className="flex w-max items-center gap-1 rounded-lg bg-muted/40 p-1">
365
+ <Skeleton className="h-8 w-12 rounded-md" />
366
+ <Skeleton className="h-8 w-16 rounded-md" />
367
+ <Skeleton className="h-8 w-16 rounded-md" />
368
+ <Skeleton className="h-8 w-24 rounded-md" />
369
+ </div>
370
+ </div>
371
+ {/* Body: builder */}
372
+ <div className="flex flex-1 overflow-hidden relative">
373
+ <div className="flex-1 overflow-auto bg-muted/30">
374
+ <div className="max-w-2xl mx-auto py-4 sm:py-8 px-3 sm:px-4 space-y-4">
366
375
  <Skeleton className="h-8 w-2/3" />
367
376
  <Skeleton className="h-4 w-1/2" />
368
377
  <div className="space-y-3 pt-4">
@@ -378,12 +387,6 @@ export function FormBuilderPage() {
378
387
  </div>
379
388
  </div>
380
389
  </div>
381
- <div className="hidden lg:block w-72 border-s border-border p-4 space-y-4">
382
- <Skeleton className="h-4 w-32" />
383
- <Skeleton className="h-9 w-full" />
384
- <Skeleton className="h-9 w-full" />
385
- <Skeleton className="h-9 w-full" />
386
- </div>
387
390
  </div>
388
391
  </div>
389
392
  );
@@ -8,6 +8,7 @@ import {
8
8
  useCommandMenuShortcut,
9
9
  getThemeInitScript,
10
10
  configureTracking,
11
+ isAgentChatHomeHandoffActive,
11
12
  markAgentChatHomeHandoff,
12
13
  navigateWithAgentChatViewTransition,
13
14
  setClientAppState,
@@ -199,7 +200,11 @@ function OpenLinkInterceptor() {
199
200
  if (!path) return;
200
201
 
201
202
  event.preventDefault();
202
- if (location.pathname === "/ask" && path !== "/ask") {
203
+ if (
204
+ location.pathname === "/ask" &&
205
+ path !== "/ask" &&
206
+ isAgentChatHomeHandoffActive("forms")
207
+ ) {
203
208
  markAgentChatHomeHandoff("forms");
204
209
  }
205
210
  navigateWithAgentChatViewTransition(navigate, path);