@agent-native/core 0.84.18 → 0.84.21

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 (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/run-store.ts +180 -13
  8. package/corpus/core/src/agent/types.ts +1 -1
  9. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  10. package/corpus/core/src/cli/recap.ts +43 -34
  11. package/corpus/core/src/client/AssistantChat.tsx +49 -0
  12. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  13. package/corpus/core/src/client/active-run-state.ts +30 -0
  14. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  15. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
  16. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
  17. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
  18. package/corpus/core/src/client/i18n.tsx +10 -5
  19. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  20. package/corpus/core/src/extensions/html-shell.ts +17 -3
  21. package/corpus/core/src/extensions/theme.ts +39 -0
  22. package/corpus/core/src/file-upload/builder.ts +35 -14
  23. package/corpus/core/src/file-upload/types.ts +3 -0
  24. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  25. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  26. package/corpus/core/src/mcp/org-directory.ts +66 -20
  27. package/corpus/core/src/scripts/call-agent.ts +7 -2
  28. package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
  31. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  32. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  33. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  34. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  35. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  36. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  37. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  38. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  39. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  40. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  41. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  42. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  43. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
  44. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  45. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  46. package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  55. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  56. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  57. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +94 -29
  58. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  59. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
  61. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  62. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  63. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  64. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  65. package/corpus/templates/plan/app/global.css +1 -1
  66. package/dist/a2a/caller-auth.d.ts +1 -0
  67. package/dist/a2a/caller-auth.d.ts.map +1 -1
  68. package/dist/a2a/caller-auth.js +34 -8
  69. package/dist/a2a/caller-auth.js.map +1 -1
  70. package/dist/a2a/client.d.ts +4 -0
  71. package/dist/a2a/client.d.ts.map +1 -1
  72. package/dist/a2a/client.js +148 -58
  73. package/dist/a2a/client.js.map +1 -1
  74. package/dist/agent/production-agent.d.ts.map +1 -1
  75. package/dist/agent/production-agent.js +19 -4
  76. package/dist/agent/production-agent.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +19 -6
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +163 -13
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +1 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  85. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  86. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  87. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  88. package/dist/cli/recap.d.ts +3 -3
  89. package/dist/cli/recap.d.ts.map +1 -1
  90. package/dist/cli/recap.js +41 -34
  91. package/dist/cli/recap.js.map +1 -1
  92. package/dist/client/AssistantChat.d.ts +2 -0
  93. package/dist/client/AssistantChat.d.ts.map +1 -1
  94. package/dist/client/AssistantChat.js +36 -2
  95. package/dist/client/AssistantChat.js.map +1 -1
  96. package/dist/client/PoweredByBadge.d.ts +9 -3
  97. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  98. package/dist/client/PoweredByBadge.js +47 -15
  99. package/dist/client/PoweredByBadge.js.map +1 -1
  100. package/dist/client/active-run-state.d.ts +3 -0
  101. package/dist/client/active-run-state.d.ts.map +1 -1
  102. package/dist/client/active-run-state.js +24 -0
  103. package/dist/client/active-run-state.js.map +1 -1
  104. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  105. package/dist/client/agent-chat-adapter.js +5 -1
  106. package/dist/client/agent-chat-adapter.js.map +1 -1
  107. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  108. package/dist/client/extensions/EmbeddedExtension.js +32 -3
  109. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  110. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  111. package/dist/client/extensions/ExtensionViewer.js +59 -2
  112. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  113. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
  114. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  115. package/dist/client/i18n.d.ts +1 -1
  116. package/dist/client/i18n.d.ts.map +1 -1
  117. package/dist/client/i18n.js +6 -2
  118. package/dist/client/i18n.js.map +1 -1
  119. package/dist/client/sse-event-processor.d.ts +2 -0
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +121 -5
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +1 -1
  126. package/dist/extensions/html-shell.d.ts.map +1 -1
  127. package/dist/extensions/html-shell.js +17 -3
  128. package/dist/extensions/html-shell.js.map +1 -1
  129. package/dist/extensions/theme.d.ts +9 -0
  130. package/dist/extensions/theme.d.ts.map +1 -1
  131. package/dist/extensions/theme.js +38 -0
  132. package/dist/extensions/theme.js.map +1 -1
  133. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  134. package/dist/file-upload/builder.d.ts.map +1 -1
  135. package/dist/file-upload/builder.js +22 -8
  136. package/dist/file-upload/builder.js.map +1 -1
  137. package/dist/file-upload/types.d.ts +5 -1
  138. package/dist/file-upload/types.d.ts.map +1 -1
  139. package/dist/file-upload/types.js.map +1 -1
  140. package/dist/integrations/a2a-continuation-processor.js +43 -18
  141. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  142. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  143. package/dist/mcp/builtin-tools.js +3 -0
  144. package/dist/mcp/builtin-tools.js.map +1 -1
  145. package/dist/mcp/org-directory.d.ts.map +1 -1
  146. package/dist/mcp/org-directory.js +58 -20
  147. package/dist/mcp/org-directory.js.map +1 -1
  148. package/dist/notifications/routes.d.ts +3 -3
  149. package/dist/observability/routes.d.ts +6 -6
  150. package/dist/progress/routes.d.ts +1 -1
  151. package/dist/resources/handlers.d.ts +3 -3
  152. package/dist/scripts/call-agent.d.ts.map +1 -1
  153. package/dist/scripts/call-agent.js +2 -2
  154. package/dist/scripts/call-agent.js.map +1 -1
  155. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  156. package/dist/server/transcribe-voice.d.ts +1 -1
  157. package/package.json +1 -1
@@ -150,11 +150,11 @@ async function processClaimedContinuation(
150
150
  return;
151
151
  }
152
152
 
153
- const client = new A2AClient(
154
- continuation.agentUrl,
155
- await signContinuationToken(continuation),
156
- { requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS },
157
- );
153
+ const auth = await signContinuationToken(continuation);
154
+ const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
155
+ requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
156
+ ...(auth.apiKeyFallbacks ? { fallbackApiKeys: auth.apiKeyFallbacks } : {}),
157
+ });
158
158
  const deadline = Date.now() + PROCESSOR_WAIT_MS;
159
159
  let task: Task | null = null;
160
160
 
@@ -438,28 +438,35 @@ function sanitizeFailureReason(reason: string): string {
438
438
 
439
439
  async function signContinuationToken(
440
440
  continuation: A2AContinuation,
441
- ): Promise<string | undefined> {
441
+ ): Promise<{ apiKey?: string; apiKeyFallbacks?: string[] }> {
442
442
  if (continuation.a2aAuthToken === "") {
443
- return undefined;
443
+ return {};
444
444
  }
445
445
 
446
446
  const storedToken = continuation.a2aAuthToken;
447
- if (storedToken && !isLikelyJwt(storedToken)) return storedToken;
448
-
449
- const freshToken = await signFreshContinuationToken(continuation);
450
- if (freshToken) return freshToken;
451
- if (!storedToken) return undefined;
447
+ if (storedToken && !isLikelyJwt(storedToken)) return { apiKey: storedToken };
448
+
449
+ const freshTokens = await signFreshContinuationTokens(continuation);
450
+ if (freshTokens.length > 0) {
451
+ return {
452
+ apiKey: freshTokens[0],
453
+ ...(freshTokens.length > 1
454
+ ? { apiKeyFallbacks: freshTokens.slice(1) }
455
+ : {}),
456
+ };
457
+ }
458
+ if (!storedToken) return {};
452
459
 
453
460
  // Older continuations may have persisted the initial short-lived JWT. Avoid
454
461
  // replaying it forever after expiry; opaque legacy bearer keys can still be
455
462
  // reused because we cannot re-mint those.
456
- if (isLikelyJwt(storedToken)) return undefined;
457
- return storedToken;
463
+ if (isLikelyJwt(storedToken)) return {};
464
+ return { apiKey: storedToken };
458
465
  }
459
466
 
460
- async function signFreshContinuationToken(
467
+ async function signFreshContinuationTokens(
461
468
  continuation: A2AContinuation,
462
- ): Promise<string | undefined> {
469
+ ): Promise<string[]> {
463
470
  let orgDomain: string | undefined;
464
471
  let orgSecret: string | undefined;
465
472
  if (continuation.orgId) {
@@ -472,17 +479,35 @@ async function signFreshContinuationToken(
472
479
  }
473
480
 
474
481
  if (!continuation.ownerEmail || !(orgSecret || process.env.A2A_SECRET)) {
475
- return undefined;
482
+ return [];
476
483
  }
477
484
 
478
- try {
479
- return await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
480
- expiresIn: "30m",
481
- preferGlobalSecret: !orgSecret,
482
- });
483
- } catch {
484
- return undefined;
485
+ const tokens: string[] = [];
486
+ const add = (token: string | undefined) => {
487
+ if (token && !tokens.includes(token)) tokens.push(token);
488
+ };
489
+
490
+ if (process.env.A2A_SECRET?.trim()) {
491
+ try {
492
+ add(
493
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
494
+ expiresIn: "30m",
495
+ preferGlobalSecret: true,
496
+ }),
497
+ );
498
+ } catch {}
499
+ }
500
+ if (orgSecret) {
501
+ try {
502
+ add(
503
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
504
+ expiresIn: "30m",
505
+ preferGlobalSecret: false,
506
+ }),
507
+ );
508
+ } catch {}
485
509
  }
510
+ return tokens;
486
511
  }
487
512
 
488
513
  function isLikelyJwt(token: string): boolean {
@@ -260,6 +260,9 @@ async function createA2AClientForAskApp(origin: string): Promise<{
260
260
  return {
261
261
  client: new A2AClient(origin, auth.apiKey, {
262
262
  requestTimeoutMs: ASK_APP_A2A_REQUEST_TIMEOUT_MS,
263
+ ...(auth.apiKeyFallbacks
264
+ ? { fallbackApiKeys: auth.apiKeyFallbacks }
265
+ : {}),
263
266
  }),
264
267
  metadata,
265
268
  };
@@ -146,6 +146,15 @@ function scopedCacheKey(
146
146
  ].join("|");
147
147
  }
148
148
 
149
+ function authTokenAttempts(auth: {
150
+ apiKey?: string;
151
+ apiKeyFallbacks?: string[];
152
+ }): string[] {
153
+ return [auth.apiKey, ...(auth.apiKeyFallbacks ?? [])].filter(
154
+ (token): token is string => typeof token === "string" && token.length > 0,
155
+ );
156
+ }
157
+
149
158
  function serviceScopedCacheKey(origin: string, orgId: string): string {
150
159
  return [origin, `service-org:${orgId}`].join("|");
151
160
  }
@@ -200,7 +209,8 @@ export async function fetchOrgApps(opts?: {
200
209
  const auth = serviceOrgId
201
210
  ? await resolveOrgDirectoryServiceAuth(serviceOrgId)
202
211
  : await resolveOrgDirectoryCallerAuth();
203
- if (!auth.apiKey) {
212
+ const attempts = authTokenAttempts(auth);
213
+ if (attempts.length === 0) {
204
214
  // No signed token available (no A2A secret / no caller identity) — the
205
215
  // directory requires the org bearer, so degrade silently to local-only.
206
216
  return [];
@@ -215,19 +225,23 @@ export async function fetchOrgApps(opts?: {
215
225
  }
216
226
  }
217
227
 
218
- const res = await fetch(`${origin}/_agent-native/org/apps`, {
219
- method: "GET",
220
- headers: {
221
- Authorization: `Bearer ${auth.apiKey}`,
222
- Accept: "application/json",
223
- },
224
- signal: AbortSignal.timeout(4000),
225
- });
226
- if (res.ok) {
227
- const json = (await res.json()) as { apps?: unknown };
228
- const list = Array.isArray(json?.apps) ? json.apps : [];
229
- apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);
230
- ttl = SUCCESS_TTL_MS;
228
+ for (let i = 0; i < attempts.length; i++) {
229
+ const res = await fetch(`${origin}/_agent-native/org/apps`, {
230
+ method: "GET",
231
+ headers: {
232
+ Authorization: `Bearer ${attempts[i]}`,
233
+ Accept: "application/json",
234
+ },
235
+ signal: AbortSignal.timeout(4000),
236
+ });
237
+ if (res.ok) {
238
+ const json = (await res.json()) as { apps?: unknown };
239
+ const list = Array.isArray(json?.apps) ? json.apps : [];
240
+ apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);
241
+ ttl = SUCCESS_TTL_MS;
242
+ break;
243
+ }
244
+ if (res.status !== 401 || i >= attempts.length - 1) break;
231
245
  }
232
246
  // Non-2xx ⇒ leave apps=[] with the short EMPTY_TTL (silent degrade).
233
247
  } catch {
@@ -252,6 +266,7 @@ export function _resetOrgDirectoryCache(): void {
252
266
 
253
267
  async function resolveOrgDirectoryCallerAuth(): Promise<{
254
268
  apiKey?: string;
269
+ apiKeyFallbacks?: string[];
255
270
  userEmail?: string;
256
271
  orgId?: string;
257
272
  orgDomain?: string;
@@ -266,6 +281,7 @@ async function resolveOrgDirectoryCallerAuth(): Promise<{
266
281
 
267
282
  async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
268
283
  apiKey?: string;
284
+ apiKeyFallbacks?: string[];
269
285
  userEmail?: string;
270
286
  orgId?: string;
271
287
  orgDomain?: string;
@@ -285,12 +301,42 @@ async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
285
301
  import("./connect-store.js"),
286
302
  ]);
287
303
  const userEmail = serviceIdentityEmail("mcp-client", trimmedOrgId);
288
- const apiKey = await signA2AToken(userEmail, orgDomain, orgSecret, {
289
- expiresIn: "5m",
290
- preferGlobalSecret: !orgSecret,
291
- extraClaims: { org_id: trimmedOrgId },
292
- });
293
- return { apiKey, userEmail, orgId: trimmedOrgId, orgDomain };
304
+ const apiKeyAttempts: string[] = [];
305
+ const addApiKeyAttempt = (token: string | undefined) => {
306
+ if (!token || apiKeyAttempts.includes(token)) return;
307
+ apiKeyAttempts.push(token);
308
+ };
309
+ if (process.env.A2A_SECRET?.trim()) {
310
+ try {
311
+ addApiKeyAttempt(
312
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
313
+ expiresIn: "5m",
314
+ preferGlobalSecret: true,
315
+ extraClaims: { org_id: trimmedOrgId },
316
+ }),
317
+ );
318
+ } catch {}
319
+ }
320
+ if (orgSecret) {
321
+ try {
322
+ addApiKeyAttempt(
323
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
324
+ expiresIn: "5m",
325
+ preferGlobalSecret: false,
326
+ extraClaims: { org_id: trimmedOrgId },
327
+ }),
328
+ );
329
+ } catch {}
330
+ }
331
+ return {
332
+ apiKey: apiKeyAttempts[0],
333
+ ...(apiKeyAttempts.length > 1
334
+ ? { apiKeyFallbacks: apiKeyAttempts.slice(1) }
335
+ : {}),
336
+ userEmail,
337
+ orgId: trimmedOrgId,
338
+ orgDomain,
339
+ };
294
340
  } catch {
295
341
  return { orgId: trimmedOrgId, orgDomain };
296
342
  }
@@ -1,6 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import { A2ATaskTimeoutError, callAgent, signA2AToken } from "../a2a/client.js";
3
+ import {
4
+ A2ATaskTimeoutError,
5
+ callAgent,
6
+ shouldPreferGlobalA2ASecret,
7
+ signA2AToken,
8
+ } from "../a2a/client.js";
4
9
  import {
5
10
  formatLlmCredentialErrorMessage,
6
11
  isLlmCredentialError,
@@ -162,7 +167,7 @@ export async function run(
162
167
  callerOrgSecret,
163
168
  {
164
169
  expiresIn: INTEGRATION_A2A_TOKEN_TTL,
165
- preferGlobalSecret: !callerOrgSecret,
170
+ preferGlobalSecret: shouldPreferGlobalA2ASecret(callerOrgSecret),
166
171
  },
167
172
  );
168
173
  } catch {}
@@ -213,7 +213,14 @@ export default defineAction({
213
213
  "Enable cursor-based pagination. After each response, reads cursorPath from the JSON body and re-issues the request with cursorParam or cursorBodyPath set, accumulating items from itemsPath (or whole bodies) until cursor is empty or maxPages is reached. Combine with saveToFile to write the full dataset to a workspace file; use scratch/... for temporary staging.",
214
214
  ),
215
215
  }),
216
- http: false,
216
+ // Mounted over HTTP (POST) so the agent and frontend (`useActionMutation`)
217
+ // can reach it, consistent with provider-api-catalog / provider-api-docs.
218
+ // toolCallable: false blocks the extension iframe bridge — this action
219
+ // accepts arbitrary methods (PUT/DELETE/etc.) against provider APIs, so
220
+ // allowing shared extensions to call it would let malicious extension authors
221
+ // mutate provider data under the opener's credentials.
222
+ http: { method: "POST" },
223
+ toolCallable: false,
217
224
  run: async (args) => {
218
225
  if (args.stageAs) {
219
226
  const ctx = requireRequestCredentialContext("provider-api staging");
@@ -7,6 +7,7 @@ import {
7
7
  IconStar,
8
8
  IconPencil,
9
9
  IconSettings,
10
+ IconFilter,
10
11
  IconGripVertical,
11
12
  IconBook2,
12
13
  IconDatabase,
@@ -251,7 +252,7 @@ function SidebarSectionSettingsPopover({
251
252
  className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/65 opacity-0 transition-all hover:bg-sidebar-accent hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover/section:opacity-100 data-[state=open]:opacity-100"
252
253
  aria-label={settingsLabel}
253
254
  >
254
- <IconSettings className="h-3.5 w-3.5" />
255
+ <IconFilter className="h-3.5 w-3.5" />
255
256
  </button>
256
257
  </PopoverTrigger>
257
258
  </TooltipTrigger>
@@ -3,6 +3,7 @@ import { useDraggable } from "@dnd-kit/core";
3
3
  import {
4
4
  IconGripVertical,
5
5
  IconDotsVertical,
6
+ IconExternalLink,
6
7
  IconMaximize,
7
8
  IconPencil,
8
9
  IconRefresh,
@@ -12,6 +13,7 @@ import {
12
13
  } from "@tabler/icons-react";
13
14
  import { useIsFetching, useQueryClient } from "@tanstack/react-query";
14
15
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
16
+ import { useNavigate } from "react-router";
15
17
 
16
18
  import { ChartFillHeight, SqlChart } from "@/components/dashboard/SqlChart";
17
19
  import {
@@ -119,6 +121,8 @@ export function SqlChartCard({
119
121
 
120
122
  const [confirmOpen, setConfirmOpen] = useState(false);
121
123
  const [expanded, setExpanded] = useState(false);
124
+ const [extRefreshKey, setExtRefreshKey] = useState(0);
125
+ const navigate = useNavigate();
122
126
  const [exportCsv, setExportCsv] = useState<(() => void) | null>(null);
123
127
  const [shouldLoadData, setShouldLoadData] = useState(
124
128
  eagerLoad ||
@@ -291,10 +295,12 @@ export function SqlChartCard({
291
295
  }
292
296
 
293
297
  // Extension panels render their sandboxed iframe full-bleed with no card chrome
294
- // or title — the extension owns its own UI. Editable dashboards still get a
295
- // hover overlay for delete/drag. Editing routes through the agent (the manual
296
- // panel editor has no extension picker), so no inline edit action here.
298
+ // or title — the extension owns its own UI. All viewers get the read-only
299
+ // actions (full screen, refresh, open extension); editable dashboards also get
300
+ // delete and drag.
297
301
  if (panel.chartType === "extension") {
302
+ const extensionId = (panel.config as Record<string, unknown> | undefined)
303
+ ?.extensionId as string | undefined;
298
304
  return (
299
305
  <div
300
306
  ref={setCardNodeRef}
@@ -302,45 +308,97 @@ export function SqlChartCard({
302
308
  data-dragging={isDragSource ? "true" : undefined}
303
309
  className="dashboard-extension-card group relative h-full"
304
310
  >
305
- <SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
306
- {editable ? (
307
- <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
308
- <DropdownMenu>
309
- <Tooltip>
310
- <TooltipTrigger asChild>
311
- <DropdownMenuTrigger asChild>
312
- <button
313
- className="p-1 rounded bg-background/80 text-muted-foreground hover:text-foreground"
314
- aria-label={t("sqlDashboard.panelOptions")}
315
- >
316
- <IconDotsVertical className="h-3.5 w-3.5" />
317
- </button>
318
- </DropdownMenuTrigger>
319
- </TooltipTrigger>
320
- <TooltipContent>
321
- {t("sqlDashboard.panelOptions")}
322
- </TooltipContent>
323
- </Tooltip>
324
- <DropdownMenuContent align="end" className="w-40">
311
+ {!expanded && (
312
+ <SqlChart
313
+ key={extRefreshKey}
314
+ panel={panel}
315
+ resolvedSql={resolvedSql}
316
+ loadData
317
+ />
318
+ )}
319
+ <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
320
+ <DropdownMenu>
321
+ <Tooltip>
322
+ <TooltipTrigger asChild>
323
+ <DropdownMenuTrigger asChild>
324
+ <button
325
+ className="p-1 rounded bg-background/80 text-muted-foreground hover:text-foreground"
326
+ aria-label={t("sqlDashboard.panelOptions")}
327
+ >
328
+ <IconDotsVertical className="h-3.5 w-3.5" />
329
+ </button>
330
+ </DropdownMenuTrigger>
331
+ </TooltipTrigger>
332
+ <TooltipContent>{t("sqlDashboard.panelOptions")}</TooltipContent>
333
+ </Tooltip>
334
+ <DropdownMenuContent align="end" className="w-44">
335
+ <DropdownMenuItem onSelect={() => setExpanded(true)}>
336
+ <IconMaximize className="h-4 w-4 mr-2" />
337
+ {t("sqlDashboard.fullScreen")}
338
+ </DropdownMenuItem>
339
+ {extensionId ? (
325
340
  <DropdownMenuItem
326
- onSelect={(e) => {
327
- e.preventDefault();
328
- setConfirmOpen(true);
329
- }}
341
+ onSelect={() =>
342
+ navigate(
343
+ `/extensions/${extensionId}/${encodeURIComponent(
344
+ (panel.title ?? "extension")
345
+ .toLowerCase()
346
+ .replace(/[^a-z0-9]+/g, "-"),
347
+ )}`,
348
+ )
349
+ }
330
350
  >
331
- <IconTrash className="h-4 w-4 mr-2" />
332
- {t("sidebar.delete")}
351
+ <IconExternalLink className="h-4 w-4 mr-2" />
352
+ Open extension {/* i18n-ignore */}
333
353
  </DropdownMenuItem>
334
- </DropdownMenuContent>
335
- </DropdownMenu>
354
+ ) : null}
355
+ <DropdownMenuSeparator />
356
+ <DropdownMenuItem onSelect={() => setExtRefreshKey((k) => k + 1)}>
357
+ <IconRefresh className="h-4 w-4 mr-2" />
358
+ {t("sqlDashboard.refresh")}
359
+ </DropdownMenuItem>
360
+ {editable ? (
361
+ <>
362
+ <DropdownMenuSeparator />
363
+ <DropdownMenuItem
364
+ onSelect={(e) => {
365
+ e.preventDefault();
366
+ setConfirmOpen(true);
367
+ }}
368
+ >
369
+ <IconTrash className="h-4 w-4 mr-2" />
370
+ {t("sidebar.delete")}
371
+ </DropdownMenuItem>
372
+ </>
373
+ ) : null}
374
+ </DropdownMenuContent>
375
+ </DropdownMenu>
376
+ {editable ? (
336
377
  <PanelDragHandle
337
378
  panelId={panel.id}
338
379
  label={t("sqlDashboard.dragToReorder")}
339
380
  className="p-1 rounded bg-background/80 cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground"
340
381
  iconClassName="h-3.5 w-3.5"
341
382
  />
342
- </div>
343
- ) : null}
383
+ ) : null}
384
+ </div>
385
+ <Dialog open={expanded} onOpenChange={setExpanded}>
386
+ <DialogContent className="flex h-[90vh] w-[95vw] max-w-[1400px] flex-col gap-4">
387
+ <DialogHeader className="shrink-0 pr-8 text-left">
388
+ <DialogTitle className="truncate">{panel.title}</DialogTitle>
389
+ </DialogHeader>
390
+ <div className="flex min-h-0 flex-1 flex-col overflow-auto">
391
+ <ChartFillHeight>
392
+ <SqlChart
393
+ key={extRefreshKey}
394
+ panel={panel}
395
+ resolvedSql={resolvedSql}
396
+ loadData
397
+ />
398
+ </ChartFillHeight>
399
+ </div>
400
+ </DialogContent>
401
+ </Dialog>
344
402
  {editable ? (
345
403
  <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
346
404
  <AlertDialogContent>