@agent-native/core 0.77.6 → 0.77.8

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 (177) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/production-agent.ts +18 -0
  5. package/corpus/core/src/agent/thread-data-builder.ts +21 -2
  6. package/corpus/core/src/cli/create.ts +35 -1
  7. package/corpus/core/src/cli/skills.ts +8 -0
  8. package/corpus/core/src/client/AgentPanel.tsx +57 -25
  9. package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
  10. package/corpus/core/src/client/analytics.ts +24 -2
  11. package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
  12. package/corpus/core/src/client/use-chat-threads.ts +104 -55
  13. package/corpus/core/src/db/ddl-guard.ts +190 -0
  14. package/corpus/core/src/secrets/storage.ts +36 -2
  15. package/corpus/core/src/server/analytics.ts +27 -3
  16. package/corpus/core/src/settings/store.ts +40 -6
  17. package/corpus/core/src/sharing/access.ts +35 -13
  18. package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
  19. package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
  20. package/corpus/core/src/sharing/registry.ts +9 -0
  21. package/corpus/core/src/vite/client.ts +3 -0
  22. package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
  23. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
  24. package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
  25. package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
  26. package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
  28. package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
  29. package/corpus/templates/analytics/netlify.toml +1 -0
  30. package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
  31. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
  32. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
  33. package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
  34. package/corpus/templates/assets/netlify.toml +1 -0
  35. package/corpus/templates/brain/netlify.toml +1 -0
  36. package/corpus/templates/calendar/app/i18n-data.ts +12 -0
  37. package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
  38. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
  39. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
  40. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
  41. package/corpus/templates/calendar/netlify.toml +1 -0
  42. package/corpus/templates/chat/netlify.toml +1 -0
  43. package/corpus/templates/clips/AGENTS.md +9 -7
  44. package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
  45. package/corpus/templates/clips/actions/archive-recording.ts +5 -2
  46. package/corpus/templates/clips/actions/clear-edits.ts +5 -2
  47. package/corpus/templates/clips/actions/create-folder.ts +7 -2
  48. package/corpus/templates/clips/actions/delete-folder.ts +7 -4
  49. package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
  50. package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
  51. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
  52. package/corpus/templates/clips/actions/list-notifications.ts +9 -5
  53. package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
  54. package/corpus/templates/clips/actions/list-recordings.ts +4 -1
  55. package/corpus/templates/clips/actions/move-recording.ts +5 -1
  56. package/corpus/templates/clips/actions/rename-folder.ts +6 -3
  57. package/corpus/templates/clips/actions/request-transcript.ts +5 -2
  58. package/corpus/templates/clips/actions/restore-recording.ts +5 -2
  59. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
  60. package/corpus/templates/clips/actions/set-chapters.ts +5 -2
  61. package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
  62. package/corpus/templates/clips/actions/split-recording.ts +5 -2
  63. package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
  64. package/corpus/templates/clips/actions/trash-recording.ts +5 -2
  65. package/corpus/templates/clips/actions/undo-edit.ts +5 -2
  66. package/corpus/templates/clips/actions/view-screen.ts +2 -1
  67. package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
  68. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
  69. package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
  71. package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
  72. package/corpus/templates/clips/chrome-extension/package.json +3 -0
  73. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  74. package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
  75. package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
  76. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
  77. package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
  78. package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
  79. package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
  80. package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
  81. package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
  82. package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
  83. package/corpus/templates/clips/netlify.toml +1 -0
  84. package/corpus/templates/clips/server/db/index.ts +1 -0
  85. package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
  86. package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
  87. package/corpus/templates/clips/server/lib/recordings.ts +18 -2
  88. package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
  89. package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
  90. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
  91. package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
  92. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
  93. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
  95. package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
  96. package/corpus/templates/content/netlify.toml +1 -0
  97. package/corpus/templates/design/netlify.toml +1 -0
  98. package/corpus/templates/dispatch/netlify.toml +1 -0
  99. package/corpus/templates/forms/netlify.toml +1 -0
  100. package/corpus/templates/macros/netlify.toml +1 -0
  101. package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
  102. package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
  103. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
  104. package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
  105. package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
  106. package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
  107. package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
  108. package/corpus/templates/mail/netlify.toml +1 -0
  109. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
  110. package/corpus/templates/plan/netlify.toml +1 -0
  111. package/corpus/templates/slides/netlify.toml +1 -0
  112. package/corpus/templates/videos/netlify.toml +1 -0
  113. package/dist/agent/production-agent.d.ts.map +1 -1
  114. package/dist/agent/production-agent.js +18 -0
  115. package/dist/agent/production-agent.js.map +1 -1
  116. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  117. package/dist/agent/thread-data-builder.js +23 -2
  118. package/dist/agent/thread-data-builder.js.map +1 -1
  119. package/dist/cli/create.d.ts.map +1 -1
  120. package/dist/cli/create.js +28 -1
  121. package/dist/cli/create.js.map +1 -1
  122. package/dist/cli/skills.d.ts +1 -1
  123. package/dist/cli/skills.d.ts.map +1 -1
  124. package/dist/cli/skills.js +8 -0
  125. package/dist/cli/skills.js.map +1 -1
  126. package/dist/client/AgentPanel.d.ts +8 -2
  127. package/dist/client/AgentPanel.d.ts.map +1 -1
  128. package/dist/client/AgentPanel.js +29 -15
  129. package/dist/client/AgentPanel.js.map +1 -1
  130. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  131. package/dist/client/MultiTabAssistantChat.js +4 -4
  132. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  133. package/dist/client/analytics.d.ts.map +1 -1
  134. package/dist/client/analytics.js +22 -2
  135. package/dist/client/analytics.js.map +1 -1
  136. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  137. package/dist/client/use-agent-engine-configured.js +23 -13
  138. package/dist/client/use-agent-engine-configured.js.map +1 -1
  139. package/dist/client/use-chat-threads.d.ts +4 -0
  140. package/dist/client/use-chat-threads.d.ts.map +1 -1
  141. package/dist/client/use-chat-threads.js +49 -4
  142. package/dist/client/use-chat-threads.js.map +1 -1
  143. package/dist/collab/routes.d.ts +2 -2
  144. package/dist/db/ddl-guard.d.ts +88 -0
  145. package/dist/db/ddl-guard.d.ts.map +1 -0
  146. package/dist/db/ddl-guard.js +180 -0
  147. package/dist/db/ddl-guard.js.map +1 -0
  148. package/dist/notifications/routes.d.ts +1 -1
  149. package/dist/observability/routes.d.ts +5 -5
  150. package/dist/progress/routes.d.ts +1 -1
  151. package/dist/resources/handlers.d.ts +3 -3
  152. package/dist/secrets/storage.d.ts.map +1 -1
  153. package/dist/secrets/storage.js +26 -2
  154. package/dist/secrets/storage.js.map +1 -1
  155. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  156. package/dist/server/analytics.d.ts +4 -0
  157. package/dist/server/analytics.d.ts.map +1 -1
  158. package/dist/server/analytics.js +19 -3
  159. package/dist/server/analytics.js.map +1 -1
  160. package/dist/server/transcribe-voice.d.ts +1 -1
  161. package/dist/settings/store.d.ts.map +1 -1
  162. package/dist/settings/store.js +32 -6
  163. package/dist/settings/store.js.map +1 -1
  164. package/dist/sharing/access.d.ts.map +1 -1
  165. package/dist/sharing/access.js +26 -12
  166. package/dist/sharing/access.js.map +1 -1
  167. package/dist/sharing/actions/share-resource.js +20 -9
  168. package/dist/sharing/actions/share-resource.js.map +1 -1
  169. package/dist/sharing/actions/unshare-resource.js +13 -2
  170. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  171. package/dist/sharing/registry.d.ts +9 -0
  172. package/dist/sharing/registry.d.ts.map +1 -1
  173. package/dist/sharing/registry.js.map +1 -1
  174. package/dist/vite/client.d.ts.map +1 -1
  175. package/dist/vite/client.js +1 -0
  176. package/dist/vite/client.js.map +1 -1
  177. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, sql, type SQL } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { defineAction } from "../../action.js";
@@ -89,6 +89,25 @@ function nanoid(size = 12): string {
89
89
  return id;
90
90
  }
91
91
 
92
+ function normalizePrincipalId(
93
+ principalType: "user" | "org",
94
+ principalId: string,
95
+ ): string {
96
+ return principalType === "user"
97
+ ? principalId.trim().toLowerCase()
98
+ : principalId;
99
+ }
100
+
101
+ function principalIdMatches(
102
+ sharesTable: any,
103
+ principalType: "user" | "org",
104
+ principalId: string,
105
+ ): SQL {
106
+ return principalType === "user"
107
+ ? sql`lower(${sharesTable.principalId}) = ${principalId}`
108
+ : eq(sharesTable.principalId, principalId);
109
+ }
110
+
92
111
  /**
93
112
  * Returns true if the given email is either an active member of `orgId` or
94
113
  * has a pending invitation to `orgId`. Used by resources whose registration
@@ -163,6 +182,10 @@ export default defineAction({
163
182
  );
164
183
  const actor = getRequestUserEmail();
165
184
  if (!actor) throw new ForbiddenError("Not signed in");
185
+ const principalId = normalizePrincipalId(
186
+ args.principalType,
187
+ args.principalId,
188
+ );
166
189
  const beforeExtensionTargets = await getExtensionShareChangeTargets(
167
190
  args.resourceType,
168
191
  args.resourceId,
@@ -176,10 +199,10 @@ export default defineAction({
176
199
  );
177
200
  }
178
201
  if (args.principalType === "user") {
179
- const ok = await isOrgMemberOrInvited(resourceOrgId, args.principalId);
202
+ const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);
180
203
  if (!ok) {
181
204
  throw new ForbiddenError(
182
- `${args.principalId} is not in your organization. Invite them to the organization first, then share.`,
205
+ `${principalId} is not in your organization. Invite them to the organization first, then share.`,
183
206
  );
184
207
  }
185
208
  } else if (args.principalType === "org") {
@@ -187,7 +210,7 @@ export default defineAction({
187
210
  // extension code in the viewer's auth context — the same threat
188
211
  // model that blocks public + cross-org user shares. Pin org-
189
212
  // principal shares to the resource's own org.
190
- if (args.principalId !== resourceOrgId) {
213
+ if (principalId !== resourceOrgId) {
191
214
  throw new ForbiddenError(
192
215
  `${reg.displayName} can only be shared with its own organization, not a different one.`,
193
216
  );
@@ -203,7 +226,7 @@ export default defineAction({
203
226
  and(
204
227
  eq(reg.sharesTable.resourceId, args.resourceId),
205
228
  eq(reg.sharesTable.principalType, args.principalType),
206
- eq(reg.sharesTable.principalId, args.principalId),
229
+ principalIdMatches(reg.sharesTable, args.principalType, principalId),
207
230
  ),
208
231
  );
209
232
 
@@ -225,7 +248,7 @@ export default defineAction({
225
248
  id,
226
249
  resourceId: args.resourceId,
227
250
  principalType: args.principalType,
228
- principalId: args.principalId,
251
+ principalId,
229
252
  role: args.role,
230
253
  createdBy: actor,
231
254
  createdAt: new Date().toISOString(),
@@ -240,7 +263,7 @@ export default defineAction({
240
263
  args.notify !== false &&
241
264
  args.principalType === "user" &&
242
265
  isEmailConfigured() &&
243
- !isSyntheticQaEmail(args.principalId)
266
+ !isSyntheticQaEmail(principalId)
244
267
  ) {
245
268
  try {
246
269
  const titleCol = reg.titleColumn ?? "title";
@@ -268,12 +291,12 @@ export default defineAction({
268
291
  heading: "You've been given access",
269
292
  paragraphs: [
270
293
  `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
271
- `Use the button below to open it. If prompted, sign in with ${emailStrong(args.principalId)}.`,
294
+ `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
272
295
  ],
273
296
  cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
274
297
  footer: `You received this because ${actor} granted you ${args.role} access.`,
275
298
  });
276
- await sendEmail({ to: args.principalId, subject, html, text });
299
+ await sendEmail({ to: principalId, subject, html, text });
277
300
  } catch (err) {
278
301
  console.error(
279
302
  "[share-resource] failed to send share notification:",
@@ -1,4 +1,4 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, sql, type SQL } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { defineAction } from "../../action.js";
@@ -9,6 +9,25 @@ import {
9
9
  notifyExtensionShareChanged,
10
10
  } from "./extension-change.js";
11
11
 
12
+ function normalizePrincipalId(
13
+ principalType: "user" | "org",
14
+ principalId: string,
15
+ ): string {
16
+ return principalType === "user"
17
+ ? principalId.trim().toLowerCase()
18
+ : principalId;
19
+ }
20
+
21
+ function principalIdMatches(
22
+ sharesTable: any,
23
+ principalType: "user" | "org",
24
+ principalId: string,
25
+ ): SQL {
26
+ return principalType === "user"
27
+ ? sql`lower(${sharesTable.principalId}) = ${principalId}`
28
+ : eq(sharesTable.principalId, principalId);
29
+ }
30
+
12
31
  export default defineAction({
13
32
  description:
14
33
  "Revoke a previously granted share. Owner or admin role required.",
@@ -28,13 +47,17 @@ export default defineAction({
28
47
  args.resourceId,
29
48
  );
30
49
  const db = reg.getDb() as any;
50
+ const principalId = normalizePrincipalId(
51
+ args.principalType,
52
+ args.principalId,
53
+ );
31
54
  await db
32
55
  .delete(reg.sharesTable)
33
56
  .where(
34
57
  and(
35
58
  eq(reg.sharesTable.resourceId, args.resourceId),
36
59
  eq(reg.sharesTable.principalType, args.principalType),
37
- eq(reg.sharesTable.principalId, args.principalId),
60
+ principalIdMatches(reg.sharesTable, args.principalType, principalId),
38
61
  ),
39
62
  );
40
63
  await notifyExtensionShareChanged(
@@ -78,6 +78,15 @@ export interface ShareableResourceRegistration {
78
78
  userEmail?: string;
79
79
  orgId?: string;
80
80
  };
81
+ /**
82
+ * When true, direct ownership is recognized by owner_email regardless of the
83
+ * caller's active org. Use this only for resource types where the template's
84
+ * own actions already treat owner_email as the cross-org authority and list
85
+ * views add their own org filters.
86
+ *
87
+ * Default: `false`.
88
+ */
89
+ ownerAccessIgnoresOrg?: boolean;
81
90
  }
82
91
 
83
92
  // Stash the registry on globalThis so it survives SSR bundle duplication.
@@ -1843,6 +1843,9 @@ function createAgentNativeConfig(
1843
1843
  define: {
1844
1844
  ...(userConfig.define ?? {}),
1845
1845
  ...(options.define ?? {}),
1846
+ __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: JSON.stringify(
1847
+ process.env.GA_MEASUREMENT_ID?.trim() || "",
1848
+ ),
1846
1849
  // Framework route warmup controls how SSR `.data` routes are fetched:
1847
1850
  // ordinary fetches keep them CDN-cacheable, while native prefetch headers
1848
1851
  // can be refused before the CDN/origin sees the request. Keep this value
@@ -151,32 +151,43 @@ export default defineAction({
151
151
  );
152
152
  const visibleKeys = new Set(results.map((result) => result.key));
153
153
  const providers = credentialProviderConfigs
154
- .filter((provider) =>
155
- provider.requiredKeys.every((key) => visibleKeys.has(key)),
156
- )
154
+ .filter((provider) => {
155
+ const requiredMode = provider.requiredMode ?? "all";
156
+ return requiredMode === "any"
157
+ ? provider.requiredKeys.some((key) => visibleKeys.has(key))
158
+ : provider.requiredKeys.every((key) => visibleKeys.has(key));
159
+ })
157
160
  .map((provider) => {
158
161
  const optionalKeys = provider.optionalKeys ?? [];
159
- const missingRequiredKeys = provider.requiredKeys.filter(
160
- (key) => !configuredKeys.has(key),
161
- );
162
+ const requiredMode = provider.requiredMode ?? "all";
163
+ const hasRequiredCredentials =
164
+ requiredMode === "any"
165
+ ? provider.requiredKeys.some((key) => configuredKeys.has(key))
166
+ : provider.requiredKeys.every((key) => configuredKeys.has(key));
167
+ const missingRequiredKeys = hasRequiredCredentials
168
+ ? []
169
+ : provider.requiredKeys.filter((key) => !configuredKeys.has(key));
162
170
  const configuredProviderKeys = [
163
171
  ...provider.requiredKeys,
164
172
  ...optionalKeys,
165
173
  ].filter((key) => configuredKeys.has(key));
174
+ const workspaceConnection =
175
+ workspaceProviderStatusById.get(provider.provider) ??
176
+ summarizeWorkspaceConnections(
177
+ provider.provider,
178
+ workspace.connections,
179
+ workspace.grants,
180
+ );
166
181
  return {
167
182
  provider: provider.provider,
168
183
  label: provider.label,
169
- configured: missingRequiredKeys.length === 0,
184
+ configured:
185
+ hasRequiredCredentials ||
186
+ workspaceConnection.grantState === "connected",
170
187
  configuredKeys: configuredProviderKeys,
171
188
  missingRequiredKeys,
172
189
  optionalKeys,
173
- workspaceConnection:
174
- workspaceProviderStatusById.get(provider.provider) ??
175
- summarizeWorkspaceConnections(
176
- provider.provider,
177
- workspace.connections,
178
- workspace.grants,
179
- ),
190
+ workspaceConnection,
180
191
  };
181
192
  });
182
193
  return {
@@ -60,8 +60,8 @@ export function NewAnalysisDialog() {
60
60
  const status = statusData as DataSourceStatusResponse | undefined;
61
61
  const envStatus = credentialRowsFromStatus(status);
62
62
  const configuredSources = useMemo(
63
- () => getConfiguredDataSources(envStatus),
64
- [envStatus],
63
+ () => getConfiguredDataSources(envStatus, status),
64
+ [envStatus, status],
65
65
  );
66
66
  const configuredSourceNames = configuredSources.map((source) => source.name);
67
67
  const statusMessage = status?.message || status?.error;
@@ -79,6 +79,10 @@ const sharedConnectionLabels: Record<SharedConnectionStatusKind, string> = {
79
79
  local_credentials: "Local credentials",
80
80
  };
81
81
 
82
+ function normalizeCredentialKey(key: string): string {
83
+ return key.trim().toUpperCase();
84
+ }
85
+
82
86
  export function credentialRowsFromStatus(
83
87
  data: DataSourceStatusResponse | EnvKeyStatus[] | undefined,
84
88
  ): EnvKeyStatus[] {
@@ -90,7 +94,9 @@ export function getOptionalCredentialKeys(source: DataSource): Set<string> {
90
94
  return new Set(
91
95
  source.walkthroughSteps
92
96
  .filter((step) => step.optional)
93
- .map((step) => step.inputKey)
97
+ .map((step) =>
98
+ step.inputKey ? normalizeCredentialKey(step.inputKey) : undefined,
99
+ )
94
100
  .filter((k): k is string => Boolean(k)),
95
101
  );
96
102
  }
@@ -99,11 +105,21 @@ export function isSourceConfigured(
99
105
  source: DataSource,
100
106
  envStatus: EnvKeyStatus[],
101
107
  ): boolean {
102
- const statusMap = new Map(envStatus.map((s) => [s.key, s.configured]));
108
+ const statusMap = new Map(
109
+ envStatus.map((s) => [normalizeCredentialKey(s.key), s.configured]),
110
+ );
103
111
  const optionalKeys = getOptionalCredentialKeys(source);
104
- return source.envKeys
105
- .filter((key) => !optionalKeys.has(key))
106
- .every((key) => statusMap.get(key) === true);
112
+ const requiredKeys = source.envKeys.filter(
113
+ (key) => !optionalKeys.has(normalizeCredentialKey(key)),
114
+ );
115
+ if (source.credentialRequirementMode === "any") {
116
+ return requiredKeys.some(
117
+ (key) => statusMap.get(normalizeCredentialKey(key)) === true,
118
+ );
119
+ }
120
+ return requiredKeys.every(
121
+ (key) => statusMap.get(normalizeCredentialKey(key)) === true,
122
+ );
107
123
  }
108
124
 
109
125
  export function getWorkspaceProviderIdForSource(
@@ -132,6 +148,14 @@ export function getWorkspaceConnectionForSource(
132
148
  );
133
149
  }
134
150
 
151
+ export function getProviderStatusForSource(
152
+ source: DataSource,
153
+ data: DataSourceStatusResponse | undefined,
154
+ ): DataSourceProviderStatus | undefined {
155
+ const providerId = getWorkspaceProviderIdForSource(source) ?? source.id;
156
+ return data?.providers?.find((provider) => provider.provider === providerId);
157
+ }
158
+
135
159
  export function getSharedConnectionStatus(
136
160
  source: DataSource,
137
161
  data: DataSourceStatusResponse | undefined,
@@ -178,12 +202,29 @@ export function isSourceReady(
178
202
  ): boolean {
179
203
  return (
180
204
  isSourceConfigured(source, envStatus) ||
205
+ getProviderStatusForSource(source, data)?.configured === true ||
181
206
  getSharedConnectionStatus(source, data, envStatus)?.kind === "ready"
182
207
  );
183
208
  }
184
209
 
210
+ export function isSourceLocallyConfigured(
211
+ source: DataSource,
212
+ data: DataSourceStatusResponse | undefined,
213
+ envStatus: EnvKeyStatus[],
214
+ ): boolean {
215
+ if (isSourceConfigured(source, envStatus)) return true;
216
+ const providerStatus = getProviderStatusForSource(source, data);
217
+ if (!providerStatus?.configured) return false;
218
+ return providerStatus.configuredKeys.length > 0;
219
+ }
220
+
185
221
  export function getConfiguredDataSources(
186
222
  envStatus: EnvKeyStatus[],
223
+ data?: DataSourceStatusResponse,
187
224
  ): DataSource[] {
188
- return dataSources.filter((source) => isSourceConfigured(source, envStatus));
225
+ return dataSources.filter((source) =>
226
+ data
227
+ ? isSourceReady(source, data, envStatus)
228
+ : isSourceConfigured(source, envStatus),
229
+ );
189
230
  }
@@ -52,6 +52,7 @@ export interface DataSource {
52
52
  category: DataSourceCategory;
53
53
  icon: ComponentType<Record<string, unknown>>;
54
54
  envKeys: string[];
55
+ credentialRequirementMode?: "all" | "any";
55
56
  walkthroughSteps: WalkthroughStep[];
56
57
  docsUrl: string;
57
58
  }
@@ -380,7 +381,8 @@ export const dataSources: DataSource[] = [
380
381
  description: "CRM deals, contacts, companies, tickets, and pipelines",
381
382
  category: "crm",
382
383
  icon: IconShoppingCart,
383
- envKeys: ["HUBSPOT_ACCESS_TOKEN"],
384
+ envKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
385
+ credentialRequirementMode: "any",
384
386
  docsUrl: "https://developers.hubspot.com/docs/api/overview",
385
387
  walkthroughSteps: [
386
388
  {
@@ -391,10 +393,19 @@ export const dataSources: DataSource[] = [
391
393
  linkText: "Private Apps",
392
394
  },
393
395
  {
394
- title: "Enter your Access Token",
396
+ title: "Enter your Private App Token",
395
397
  description: "Copy the access token from your private app.",
398
+ inputKey: "HUBSPOT_PRIVATE_APP_TOKEN",
399
+ inputLabel: "Private App Token",
400
+ inputPlaceholder: "pat-na1-...",
401
+ inputType: "password",
402
+ },
403
+ {
404
+ title: "Legacy access token",
405
+ description:
406
+ "Only use this if you already have an older HubSpot access token saved under the legacy key.",
396
407
  inputKey: "HUBSPOT_ACCESS_TOKEN",
397
- inputLabel: "Access Token",
408
+ inputLabel: "Legacy Access Token",
398
409
  inputPlaceholder: "pat-na1-...",
399
410
  inputType: "password",
400
411
  },
@@ -66,7 +66,7 @@ import {
66
66
  getOptionalCredentialKeys,
67
67
  getSharedConnectionStatus,
68
68
  isSourceReady,
69
- isSourceConfigured,
69
+ isSourceLocallyConfigured,
70
70
  credentialRowsFromStatus,
71
71
  type DataSourceStatusResponse,
72
72
  type EnvKeyStatus,
@@ -674,6 +674,13 @@ function ConnectedView({
674
674
  ),
675
675
  );
676
676
  const optionalKeys = getOptionalCredentialKeys(source);
677
+ const isAnyCredentialMode = source.credentialRequirementMode === "any";
678
+ const configuredCredentialKeys = new Set(
679
+ envStatus.filter((s) => s.configured).map((s) => s.key),
680
+ );
681
+ const hasAlternativeCredential =
682
+ isAnyCredentialMode &&
683
+ source.envKeys.some((key) => configuredCredentialKeys.has(key));
677
684
 
678
685
  const handleDisconnect = () => {
679
686
  if (sharedCredentialKeys.length > 0) {
@@ -852,8 +859,7 @@ function ConnectedView({
852
859
  <div className="flex items-start justify-between gap-4">
853
860
  <div className="flex-1 space-y-2">
854
861
  {source.envKeys.map((key) => {
855
- const configured =
856
- envStatus.find((s) => s.key === key)?.configured ?? false;
862
+ const configured = configuredCredentialKeys.has(key);
857
863
  const optional = optionalKeys.has(key);
858
864
  return (
859
865
  <div
@@ -873,6 +879,11 @@ function ConnectedView({
873
879
  <IconCircle className="h-3 w-3" />
874
880
  {t("dataSources.optional")}
875
881
  </span>
882
+ ) : hasAlternativeCredential ? (
883
+ <span className="flex items-center gap-1 whitespace-nowrap text-muted-foreground">
884
+ <IconCircle className="h-3 w-3" />
885
+ Alternative
886
+ </span>
876
887
  ) : (
877
888
  <span className="flex items-center gap-1 whitespace-nowrap text-rose-400">
878
889
  <IconAlertCircle className="h-3 w-3" />
@@ -1643,7 +1654,11 @@ export default function DataSources() {
1643
1654
  <DataSourceCard
1644
1655
  key={source.id}
1645
1656
  source={source}
1646
- locallyConfigured={isSourceConfigured(source, envStatus)}
1657
+ locallyConfigured={isSourceLocallyConfigured(
1658
+ source,
1659
+ statusData,
1660
+ envStatus,
1661
+ )}
1647
1662
  ready={isSourceReady(source, statusData, envStatus)}
1648
1663
  sharedConnectionStatus={getSharedConnectionStatus(
1649
1664
  source,
@@ -1676,7 +1691,11 @@ export default function DataSources() {
1676
1691
  <DataSourceCard
1677
1692
  key={source.id}
1678
1693
  source={source}
1679
- locallyConfigured={isSourceConfigured(source, envStatus)}
1694
+ locallyConfigured={isSourceLocallyConfigured(
1695
+ source,
1696
+ statusData,
1697
+ envStatus,
1698
+ )}
1680
1699
  ready={isSourceReady(source, statusData, envStatus)}
1681
1700
  sharedConnectionStatus={getSharedConnectionStatus(
1682
1701
  source,
@@ -1,9 +1,9 @@
1
- import { useSendToAgentChat, useT } from "@agent-native/core/client";
2
1
  import {
3
- IconAlertTriangle,
4
- IconAlignLeft,
5
- IconLoader2,
6
- } from "@tabler/icons-react";
2
+ PromptComposer,
3
+ useSendToAgentChat,
4
+ useT,
5
+ } from "@agent-native/core/client";
6
+ import { IconAlertTriangle, IconAlignLeft } from "@tabler/icons-react";
7
7
  import { useEffect, useState, type ReactElement, type ReactNode } from "react";
8
8
  import { toast } from "sonner";
9
9
 
@@ -30,7 +30,6 @@ import {
30
30
  SelectValue,
31
31
  } from "@/components/ui/select";
32
32
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
33
- import { Textarea } from "@/components/ui/textarea";
34
33
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
35
34
  import { canFormatPanelSql, formatPanelSql } from "@/lib/format-sql";
36
35
 
@@ -182,7 +181,6 @@ function PanelEditorContent({
182
181
  const [form, setForm] = useState<PanelFormValues>(() => panelToForm(panel));
183
182
  const [saving, setSaving] = useState(false);
184
183
  const [error, setError] = useState<string | null>(null);
185
- const [prompt, setPrompt] = useState("");
186
184
  const [tab, setTab] = useState<"describe" | "manual">("describe");
187
185
  const { send, isGenerating } = useSendToAgentChat();
188
186
 
@@ -192,7 +190,6 @@ function PanelEditorContent({
192
190
  setForm(panelToForm(panel));
193
191
  setError(null);
194
192
  setSaving(false);
195
- setPrompt("");
196
193
  // Editing an existing panel always goes straight to the manual form.
197
194
  setTab(panel ? "manual" : "describe");
198
195
  }
@@ -200,7 +197,6 @@ function PanelEditorContent({
200
197
 
201
198
  const isEdit = !!panel;
202
199
  const canSave = form.title.trim().length > 0 && form.sql.trim().length > 0;
203
- const canGenerate = prompt.trim().length > 0 && !isGenerating;
204
200
  const canFormat = canFormatPanelSql(form.source);
205
201
 
206
202
  const handleSubmit = async () => {
@@ -219,13 +215,14 @@ function PanelEditorContent({
219
215
  }
220
216
  };
221
217
 
222
- const handleDescribe = () => {
223
- if (!canGenerate) return;
218
+ const handleDescribe = (text: string) => {
219
+ const trimmed = text.trim();
220
+ if (!trimmed || isGenerating) return;
224
221
  const titlesLine = existingPanelTitles.length
225
222
  ? `Existing panels on this dashboard: ${existingPanelTitles.join(", ")}.`
226
223
  : "This dashboard has no panels yet.";
227
224
  send({
228
- message: prompt.trim(),
225
+ message: trimmed,
229
226
  context:
230
227
  `The user wants to add a new panel to SQL dashboard "${dashboardId}". ${titlesLine} ` +
231
228
  `REAL_DATA_REQUIRED: before saving or answering, run at least one real data-source query action for this panel; \`data-source-status\`, \`list-data-dictionary\`, \`update-dashboard\`, and dry-run validation do not count as data queries. ` +
@@ -246,7 +243,6 @@ function PanelEditorContent({
246
243
  `After the panel saves, call \`refresh-screen\` so the UI picks up the change.`,
247
244
  submit: true,
248
245
  });
249
- setPrompt("");
250
246
  onOpenChange(false);
251
247
  };
252
248
 
@@ -441,28 +437,14 @@ function PanelEditorContent({
441
437
 
442
438
  <TabsContent value="describe" className="mt-4">
443
439
  <div className="grid gap-3">
444
- <Label htmlFor="panel-prompt">{t("panelEditor.whatToChart")}</Label>
445
- <Textarea
446
- id="panel-prompt"
447
- value={prompt}
448
- onChange={(e) => setPrompt(e.target.value)}
449
- placeholder={t("panelEditor.promptPlaceholder")}
450
- className="min-h-[140px] resize-y text-sm"
440
+ <Label>{t("panelEditor.whatToChart")}</Label>
441
+ <PromptComposer
451
442
  autoFocus
452
- onKeyDown={(e) => {
453
- if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
454
- e.preventDefault();
455
- handleDescribe();
456
- }
457
- }}
443
+ disabled={isGenerating}
444
+ placeholder={t("panelEditor.promptPlaceholder")}
445
+ draftScope="analytics:add-panel"
446
+ onSubmit={handleDescribe}
458
447
  />
459
- <p className="text-xs text-muted-foreground">
460
- {t("panelEditor.describeHelp", {
461
- shortcut: `${
462
- /Mac|iPhone|iPad/.test(navigator.userAgent) ? "⌘" : "Ctrl"
463
- }+Enter`,
464
- })}
465
- </p>
466
448
  </div>
467
449
  <EditorFooter className="mt-4">
468
450
  <Button
@@ -473,16 +455,6 @@ function PanelEditorContent({
473
455
  >
474
456
  {t("panelEditor.cancel")}
475
457
  </Button>
476
- <Button size="sm" onClick={handleDescribe} disabled={!canGenerate}>
477
- {isGenerating ? (
478
- <>
479
- <IconLoader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
480
- {t("panelEditor.generating")}
481
- </>
482
- ) : (
483
- t("panelEditor.generate")
484
- )}
485
- </Button>
486
458
  </EditorFooter>
487
459
  </TabsContent>
488
460
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Fixed HubSpot data source status so private app tokens, legacy tokens, and shared workspace grants show as ready consistently.
@@ -5,6 +5,7 @@ publish = "templates/analytics/dist"
5
5
  functions = "templates/analytics/.netlify/functions-internal"
6
6
 
7
7
  [build.environment]
8
+ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
8
9
  NITRO_PRESET = "netlify"
9
10
  NPM_CONFIG_PRODUCTION = "false"
10
11
  AGENT_PROD_CODE_EXECUTION = "sandboxed"
@@ -8,6 +8,7 @@ export interface CredentialProviderConfig {
8
8
  provider: string;
9
9
  label: string;
10
10
  requiredKeys: string[];
11
+ requiredMode?: "all" | "any";
11
12
  optionalKeys?: string[];
12
13
  }
13
14
 
@@ -217,7 +218,8 @@ export const credentialProviderConfigs: CredentialProviderConfig[] = [
217
218
  {
218
219
  provider: "hubspot",
219
220
  label: "HubSpot",
220
- requiredKeys: ["HUBSPOT_ACCESS_TOKEN"],
221
+ requiredKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
222
+ requiredMode: "any",
221
223
  },
222
224
  {
223
225
  provider: "gong",
@@ -5,7 +5,11 @@ import {
5
5
  resolveCredential,
6
6
  withRequestContextFromEvent,
7
7
  } from "../../lib/credentials";
8
- import { resolveAnalyticsGongCredentials } from "../../lib/provider-credentials";
8
+ import {
9
+ HUBSPOT_ANALYTICS_CREDENTIAL_KEYS,
10
+ resolveAnalyticsGongCredentials,
11
+ resolveAnalyticsProviderCredential,
12
+ } from "../../lib/provider-credentials";
9
13
 
10
14
  export default defineEventHandler(async (event) => {
11
15
  const body = await readBody(event);
@@ -91,8 +95,13 @@ export default defineEventHandler(async (event) => {
91
95
  }
92
96
 
93
97
  case "hubspot": {
94
- const token = await resolveCredential("HUBSPOT_ACCESS_TOKEN", ctx);
95
- if (!token) return { ok: false, error: "Missing access token" };
98
+ const credential = await resolveAnalyticsProviderCredential({
99
+ provider: "hubspot",
100
+ keys: HUBSPOT_ANALYTICS_CREDENTIAL_KEYS,
101
+ ctx,
102
+ });
103
+ const token = credential?.value;
104
+ if (!token) return { ok: false, error: "Missing HubSpot token" };
96
105
  const res = await fetch(
97
106
  "https://api.hubapi.com/crm/v3/objects/contacts?limit=1",
98
107
  {
@@ -2,6 +2,7 @@ import {
2
2
  DevDatabaseLink,
3
3
  FeedbackButton,
4
4
  appPath,
5
+ focusAgentChat,
5
6
  navigateWithAgentChatViewTransition,
6
7
  useActionQuery,
7
8
  useChatThreads,
@@ -507,14 +508,16 @@ export function Sidebar() {
507
508
  onClick={(event) => {
508
509
  if (
509
510
  item.href === "/" &&
510
- !isCreateRoute &&
511
511
  !event.metaKey &&
512
512
  !event.ctrlKey &&
513
513
  !event.shiftKey &&
514
514
  !event.altKey
515
515
  ) {
516
516
  event.preventDefault();
517
- navigateWithAgentChatViewTransition(navigate, "/");
517
+ focusAgentChat();
518
+ if (!isCreateRoute || location.pathname !== "/") {
519
+ navigateWithAgentChatViewTransition(navigate, "/");
520
+ }
518
521
  }
519
522
  }}
520
523
  className={cn(