@agent-native/core 0.136.0 → 0.136.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +31 -5
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +7 -0
  4. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +52 -19
  5. package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +25 -3
  6. package/corpus/templates/analytics/AGENTS.md +6 -7
  7. package/corpus/templates/analytics/actions/gong-calls.ts +3 -3
  8. package/corpus/templates/analytics/actions/provider-api-catalog.ts +1 -1
  9. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  10. package/corpus/templates/analytics/app/i18n-data.ts +46 -0
  11. package/corpus/templates/analytics/app/pages/Settings.tsx +67 -1
  12. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
  13. package/corpus/templates/analytics/changelog/2026-08-03-dashboard-builds-now-finish-without-an-extra-confirmation.md +6 -0
  14. package/corpus/templates/analytics/changelog/2026-08-03-error-alert-emails-are-opt-in.md +6 -0
  15. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +10 -9
  16. package/corpus/templates/analytics/server/lib/error-capture.ts +38 -10
  17. package/corpus/templates/analytics/server/lib/gong.ts +27 -4
  18. package/corpus/templates/analytics/server/plugins/agent-chat.ts +64 -4
  19. package/corpus/templates/analytics/shared/analytics-user-prefs.ts +7 -0
  20. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +15 -6
  21. package/corpus/templates/clips/actions/apply-rewind-extension.ts +9 -0
  22. package/corpus/templates/clips/actions/cleanup-dictation.ts +2 -2
  23. package/corpus/templates/clips/actions/cleanup-transcript.ts +8 -11
  24. package/corpus/templates/clips/actions/clear-edits.ts +15 -2
  25. package/corpus/templates/clips/actions/create-recording-agent-link.ts +10 -0
  26. package/corpus/templates/clips/actions/finalize-meeting.ts +6 -6
  27. package/corpus/templates/clips/actions/finalize-recording.ts +33 -8
  28. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +1 -3
  29. package/corpus/templates/clips/actions/list-recordings.ts +39 -24
  30. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +4 -1
  31. package/corpus/templates/clips/actions/regenerate-title.ts +2 -2
  32. package/corpus/templates/clips/actions/stitch-recordings.ts +36 -7
  33. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +1 -4
  34. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +3 -1
  35. package/corpus/templates/clips/app/components/library/recording-card.tsx +9 -0
  36. package/corpus/templates/clips/app/components/player/recording-views-badge.tsx +52 -7
  37. package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
  38. package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +17 -10
  39. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  40. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +5 -0
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -0
  42. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  43. package/corpus/templates/clips/changelog/2026-07-31-agent-views-now-show-the-agent-s-name-from-the-link-it-was-g.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-31-clip-cards-and-the-watch-share-page-header-now-show-agent-vi.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-08-01-recordings-stored-in-private-s3-compatible-buckets-finalize-.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-08-03-clip-cleanup-titles-and-meeting-summaries-now-use-luna-by-de.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-08-03-get-a-monthly-recap-email-showing-how-many-people-and-ai-age.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-08-03-get-an-email-the-first-time-an-ai-agent-reads-one-of-your-cl.md +6 -0
  49. package/corpus/templates/clips/learnings.defaults.md +2 -2
  50. package/corpus/templates/clips/package.json +1 -1
  51. package/corpus/templates/clips/scripts/send-real-recap.ts +165 -0
  52. package/corpus/templates/clips/scripts/send-test-emails.ts +60 -0
  53. package/corpus/templates/clips/server/db/schema.ts +4 -0
  54. package/corpus/templates/clips/server/jobs/transactional-emails.ts +256 -0
  55. package/corpus/templates/clips/server/lib/agent-views.ts +38 -11
  56. package/corpus/templates/clips/server/lib/media-storage-provenance.ts +16 -0
  57. package/corpus/templates/clips/server/lib/public-agent-context.ts +3 -1
  58. package/corpus/templates/clips/server/lib/recap-metrics.ts +373 -0
  59. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +84 -4
  60. package/corpus/templates/clips/server/lib/transactional-email-store.ts +41 -2
  61. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +245 -3
  62. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  63. package/corpus/templates/clips/server/register-secrets.ts +2 -2
  64. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
  65. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +42 -1
  66. package/corpus/templates/design/actions/take-design-screenshot.ts +5 -3
  67. package/corpus/templates/design/package.json +1 -1
  68. package/corpus/templates/design/server/lib/playwright-runtime.ts +13 -5
  69. package/corpus/templates/mail/actions/get-automation-settings.ts +4 -12
  70. package/corpus/templates/mail/app/pages/SettingsPage.tsx +1 -1
  71. package/corpus/templates/mail/changelog/2026-08-03-inbox-automations-now-prefer-the-lowest-cost-luna-model-when.md +6 -0
  72. package/corpus/templates/mail/server/lib/automation-engine.ts +19 -14
  73. package/corpus/templates/mail/server/lib/automation-model.ts +110 -0
  74. package/corpus/templates/mail/server/plugins/agent-chat.ts +1 -1
  75. package/corpus/templates/slides/actions/generate-slides-ai.ts +92 -24
  76. package/corpus/templates/slides/changelog/2026-08-03-legacy-slide-outlines-now-prefer-the-lowest-cost-luna-model-.md +6 -0
  77. package/corpus/templates/tasks/package.json +2 -2
  78. package/dist/agent/production-agent.d.ts +2 -2
  79. package/dist/agent/production-agent.js +1 -1
  80. package/dist/catalog.json +2 -0
  81. package/dist/client/AgentTaskCard.js +4 -2
  82. package/dist/client/i18n.js +3 -0
  83. package/dist/client/settings/VoiceTranscriptionSection.js +2 -2
  84. package/dist/collab/struct-routes.d.ts +1 -1
  85. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  86. package/dist/localization/default-messages.d.ts +5 -0
  87. package/dist/localization/default-messages.js +5 -0
  88. package/dist/mcp/oauth-client-metadata.d.ts +7 -0
  89. package/dist/mcp/oauth-client-metadata.js +47 -0
  90. package/dist/mcp/oauth-route.js +7 -3
  91. package/dist/mcp/oauth-store.d.ts +2 -0
  92. package/dist/mcp/oauth-store.js +25 -3
  93. package/dist/observability/traces.d.ts +1 -1
  94. package/dist/observability/traces.js +2 -1
  95. package/dist/progress/routes.d.ts +1 -1
  96. package/dist/provider-api/index.js +29 -1
  97. package/dist/resources/handlers.d.ts +1 -1
  98. package/dist/secrets/routes.d.ts +9 -9
  99. package/dist/server/agent-access.d.ts +3 -1
  100. package/dist/server/agent-access.js +2 -1
  101. package/dist/server/agent-chat/browser-team-tools.d.ts +1 -0
  102. package/dist/server/agent-chat/browser-team-tools.js +5 -4
  103. package/dist/server/agent-chat/context-tools.js +2 -2
  104. package/dist/server/agent-chat-plugin.js +13 -6
  105. package/dist/server/agent-teams-run-queue.d.ts +2 -0
  106. package/dist/server/agent-teams.d.ts +2 -0
  107. package/dist/server/agent-teams.js +45 -2
  108. package/dist/server/request-context.d.ts +2 -0
  109. package/dist/server/short-lived-token.d.ts +7 -0
  110. package/dist/server/short-lived-token.js +7 -1
  111. package/dist/server/ssr-handler.js +19 -1
  112. package/dist/server/transcribe-voice.js +24 -16
  113. package/dist/vite/client.d.ts +37 -1
  114. package/dist/vite/client.js +110 -1
  115. package/docs/content/template-assets.mdx +14 -4
  116. package/package.json +3 -4
@@ -1,4 +1,5 @@
1
1
  import {
2
+ isResolvedEngineUsableForRequest,
2
3
  registerBuiltinEngines,
3
4
  resolveEngine,
4
5
  } from "@agent-native/core/agent/engine";
@@ -20,6 +21,10 @@ import {
20
21
  executeActions,
21
22
  type ActionContext,
22
23
  } from "./automation-actions.js";
24
+ import {
25
+ resolveAutomationModelSettings,
26
+ type AutomationModelSettings,
27
+ } from "./automation-model.js";
23
28
  import {
24
29
  createOAuth2Client,
25
30
  gmailListMessages,
@@ -30,7 +35,6 @@ import {
30
35
  } from "./google-api.js";
31
36
  import { getOAuth2Credentials } from "./google-auth.js";
32
37
 
33
- const DEFAULT_MODEL = "claude-haiku-4-5-20251001";
34
38
  const MAX_EMAILS_PER_RUN = 50;
35
39
  const MAX_PROCESSED_IDS = 500;
36
40
  const PROCESSED_IDS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
@@ -338,11 +342,6 @@ interface RuleMatch {
338
342
  match: boolean;
339
343
  }
340
344
 
341
- interface AutomationModelSettings {
342
- engine?: string;
343
- model?: string;
344
- }
345
-
346
345
  const MODEL_AVAILABILITY_CACHE_TTL_MS = 5 * 60 * 1000;
347
346
  const modelAvailabilityCache = new Map<
348
347
  string,
@@ -370,10 +369,17 @@ async function canUseAutomationModel(
370
369
  !settings.engine || settings.engine === "anthropic"
371
370
  ? await resolveAnthropicKey(ownerEmail)
372
371
  : undefined;
373
- await resolveEngine({
372
+ const engine = await resolveEngine({
374
373
  engineOption: settings.engine,
375
374
  apiKey: anthropicKey,
376
375
  });
376
+ if (
377
+ !(await isResolvedEngineUsableForRequest(engine, {
378
+ apiKey: anthropicKey,
379
+ }))
380
+ ) {
381
+ throw new Error("No LLM provider is connected");
382
+ }
377
383
  });
378
384
  modelAvailabilityCache.set(cacheKey, {
379
385
  ok: true,
@@ -482,13 +488,12 @@ async function getAutomationModelSettings(
482
488
  ownerEmail: string,
483
489
  ): Promise<AutomationModelSettings> {
484
490
  const autoSettings = await getUserSetting(ownerEmail, "automation-settings");
485
- if (autoSettings && typeof autoSettings === "object") {
486
- return {
487
- engine: (autoSettings as any).engine,
488
- model: (autoSettings as any).model,
489
- };
490
- }
491
- return { model: DEFAULT_MODEL };
491
+ return resolveAutomationModelSettings(
492
+ ownerEmail,
493
+ autoSettings && typeof autoSettings === "object"
494
+ ? (autoSettings as AutomationModelSettings)
495
+ : null,
496
+ );
492
497
  }
493
498
 
494
499
  async function evaluateRules(
@@ -0,0 +1,110 @@
1
+ import {
2
+ isResolvedEngineUsableForRequest,
3
+ registerBuiltinEngines,
4
+ resolveEngine,
5
+ } from "@agent-native/core/agent/engine";
6
+ import { runWithRequestContext } from "@agent-native/core/server";
7
+ import { getSetting } from "@agent-native/core/settings";
8
+
9
+ export interface AutomationModelSettings {
10
+ engine?: string;
11
+ model?: string;
12
+ }
13
+
14
+ export const DEFAULT_AUTOMATION_ENGINE = "builder";
15
+ export const DEFAULT_AUTOMATION_MODEL = "gpt-5-6-luna";
16
+
17
+ const CHEAP_MODEL_CANDIDATES: AutomationModelSettings[] = [
18
+ { engine: DEFAULT_AUTOMATION_ENGINE, model: DEFAULT_AUTOMATION_MODEL },
19
+ { engine: "ai-sdk:openai", model: "gpt-5.6-luna" },
20
+ { engine: "ai-sdk:openrouter", model: "openai/gpt-5.6-luna" },
21
+ ];
22
+
23
+ function isUnavailableEngineError(error: unknown): boolean {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ return /No LLM provider is connected|Connect an LLM provider|missing_credentials|not configured|not installed|unknown agent engine|engine .* unavailable/i.test(
26
+ message,
27
+ );
28
+ }
29
+
30
+ async function canResolveEngine(
31
+ ownerEmail: string,
32
+ engineName: string,
33
+ ): Promise<boolean> {
34
+ try {
35
+ registerBuiltinEngines();
36
+ return runWithRequestContext({ userEmail: ownerEmail }, async () => {
37
+ const engine = await resolveEngine({ engineOption: engineName });
38
+ return isResolvedEngineUsableForRequest(engine);
39
+ });
40
+ } catch (error) {
41
+ if (isUnavailableEngineError(error)) return false;
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function resolveEngineDefaultModel(
47
+ ownerEmail: string,
48
+ engineName: string,
49
+ ): Promise<string> {
50
+ registerBuiltinEngines();
51
+ return runWithRequestContext({ userEmail: ownerEmail }, async () => {
52
+ const engine = await resolveEngine({ engineOption: engineName });
53
+ return engine.defaultModel;
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Prefer Luna for background text classification when a Luna-capable provider
59
+ * is actually configured. An app's explicit automation setting always wins;
60
+ * this is only the no-override default.
61
+ */
62
+ export async function resolveDefaultAutomationModel(
63
+ ownerEmail: string,
64
+ ): Promise<AutomationModelSettings> {
65
+ for (const candidate of CHEAP_MODEL_CANDIDATES) {
66
+ if (
67
+ candidate.engine &&
68
+ (await canResolveEngine(ownerEmail, candidate.engine))
69
+ ) {
70
+ return candidate;
71
+ }
72
+ }
73
+
74
+ const agentEngine = (await getSetting("agent-engine")) as {
75
+ engine?: string;
76
+ model?: string;
77
+ } | null;
78
+ if (agentEngine?.engine || agentEngine?.model) {
79
+ const model =
80
+ agentEngine.model ??
81
+ (agentEngine.engine
82
+ ? await resolveEngineDefaultModel(ownerEmail, agentEngine.engine)
83
+ : undefined);
84
+ return {
85
+ engine: agentEngine.engine,
86
+ model,
87
+ };
88
+ }
89
+
90
+ // Leave engine selection to resolveEngine so a configured non-Luna provider
91
+ // remains usable when none of the preferred Luna engines is connected.
92
+ return {};
93
+ }
94
+
95
+ export async function resolveAutomationModelSettings(
96
+ ownerEmail: string,
97
+ settings: AutomationModelSettings | null | undefined,
98
+ ): Promise<AutomationModelSettings> {
99
+ const defaults = await resolveDefaultAutomationModel(ownerEmail);
100
+ if (!settings?.engine && !settings?.model) return defaults;
101
+
102
+ return {
103
+ engine: settings.engine ?? defaults.engine,
104
+ model:
105
+ settings.model ??
106
+ (settings.engine
107
+ ? await resolveEngineDefaultModel(ownerEmail, settings.engine)
108
+ : defaults.model),
109
+ };
110
+ }
@@ -150,7 +150,7 @@ Examples:
150
150
  - User says "archive marketing emails" \u2192 create rule with condition "marketing or promotional email" and action archive
151
151
  - User says "star emails from alice@example.com" \u2192 create rule with condition "from alice@example.com" and action star
152
152
 
153
- Rules are evaluated by a fast AI model (Haiku) and run every minute + when the user opens the app.
153
+ Rules are evaluated by a low-cost text model, preferring GPT-5.6 Luna when a Luna-capable provider is available, and run every minute + when the user opens the app.
154
154
  Use trigger-automations to force immediate processing.
155
155
 
156
156
  Available action types: label (with labelName), archive, mark_read, star, trash.
@@ -1,11 +1,18 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { resolveSecret } from "@agent-native/core/server";
2
+ import { createBuilderEngine } from "@agent-native/core/agent/engine";
3
+ import {
4
+ resolveBuilderCredentials,
5
+ resolveSecret,
6
+ } from "@agent-native/core/server";
3
7
  import type { GeneratedSlide } from "@shared/api";
4
8
  import { z } from "zod";
5
9
 
10
+ const BUILDER_MODEL = "gpt-5-6-luna";
11
+ const GEMINI_MODEL = "gemini-2.0-flash";
12
+
6
13
  export default defineAction({
7
14
  description:
8
- "Legacy helper for the Generate Slides dialog that drafts a whole new deck outline (multiple slides) from a topic. It returns markdown slide drafts, not the app's rendered slide HTML. Agent chat should create decks with create-deck slides: [] plus add-slide HTML instead of this action. Do NOT use this for a request to generate one or more images/image variations for an existing slide — use generate-image-api for that; this action requires its own GEMINI_API_KEY and has no Builder-managed fallback.",
15
+ "Legacy helper for the Generate Slides dialog that drafts a whole new deck outline (multiple slides) from a topic. It returns markdown slide drafts, not the app's rendered slide HTML. Agent chat should create decks with create-deck slides: [] plus add-slide HTML instead of this action. Do NOT use this for a request to generate one or more images/image variations for an existing slide — use generate-image-api for that. When Builder is connected, this uses GPT-5.6 Luna; otherwise it falls back to a user Gemini API key.",
9
16
  schema: z.object({
10
17
  topic: z.string().describe("Presentation topic"),
11
18
  slideCount: z.coerce
@@ -22,15 +29,8 @@ export default defineAction({
22
29
  .describe("Whether to include image prompts (default: true)"),
23
30
  }),
24
31
  run: async (args) => {
25
- const apiKey = await resolveSecret("GEMINI_API_KEY");
26
- if (!apiKey) {
27
- throw new Error(
28
- "Gemini API key not configured. Save GEMINI_API_KEY in settings.",
29
- );
30
- }
31
-
32
32
  const topic = args.topic;
33
- // Cap at 10. Single-shot Gemini JSON generation reliably truncates
33
+ // Cap at 10. Single-shot JSON generation reliably truncates
34
34
  // beyond that — the resulting JSON fails to parse and the user sees
35
35
  // an error. Larger decks should be assembled with sequential
36
36
  // `add-slide` calls from the agent chat instead.
@@ -38,9 +38,6 @@ export default defineAction({
38
38
  const style = args.style;
39
39
  const includeImages = args.includeImages !== false;
40
40
 
41
- const { GoogleGenAI } = await import("@google/genai");
42
- const client = new GoogleGenAI({ apiKey });
43
-
44
41
  const imageInstruction = includeImages
45
42
  ? `For slides where a visual would enhance the message, set the layout to "image" and provide an "imagePrompt" field with a detailed description of what image to generate. The imagePrompt should describe a professional, high-quality image that supports the slide content. Include imagePrompt for roughly 30-40% of slides (not the title slide).`
46
43
  : `Do not include imagePrompt fields.`;
@@ -70,17 +67,44 @@ Rules:
70
67
 
71
68
  Respond ONLY with valid JSON. No markdown code fences, no explanation. Just the JSON array.`;
72
69
 
73
- const response = await client.models.generateContent({
74
- model: "gemini-2.0-flash",
75
- contents: [{ text: prompt }],
76
- config: {
77
- responseMimeType: "application/json",
78
- },
79
- });
80
-
81
- const text = response.candidates?.[0]?.content?.parts?.[0]?.text;
82
- if (!text) {
83
- throw new Error("No response from Gemini");
70
+ const builderCreds = await resolveBuilderCredentials();
71
+ const builderConfigured = Boolean(
72
+ builderCreds.privateKey && builderCreds.publicKey,
73
+ );
74
+ let text: string | undefined;
75
+ let builderError: Error | null = null;
76
+
77
+ if (builderConfigured) {
78
+ try {
79
+ text = await callBuilderGateway(prompt);
80
+ } catch (error) {
81
+ builderError =
82
+ error instanceof Error ? error : new Error(String(error));
83
+ }
84
+ }
85
+
86
+ if (!text?.trim()) {
87
+ const apiKey = await resolveSecret("GEMINI_API_KEY");
88
+ if (!apiKey) {
89
+ throw (
90
+ builderError ??
91
+ new Error(
92
+ "Slides outline generation needs Builder.io Connect or GEMINI_API_KEY.",
93
+ )
94
+ );
95
+ }
96
+
97
+ const { GoogleGenAI } = await import("@google/genai");
98
+ const client = new GoogleGenAI({ apiKey });
99
+ const response = await client.models.generateContent({
100
+ model: GEMINI_MODEL,
101
+ contents: [{ text: prompt }],
102
+ config: {
103
+ responseMimeType: "application/json",
104
+ },
105
+ });
106
+ text = response.candidates?.[0]?.content?.parts?.[0]?.text;
107
+ if (!text) throw new Error("No response from Gemini");
84
108
  }
85
109
 
86
110
  let slides: GeneratedSlide[];
@@ -113,3 +137,47 @@ Respond ONLY with valid JSON. No markdown code fences, no explanation. Just the
113
137
  return { slides };
114
138
  },
115
139
  });
140
+
141
+ async function callBuilderGateway(prompt: string): Promise<string> {
142
+ const engine = createBuilderEngine();
143
+ const controller = new AbortController();
144
+ const timeout = setTimeout(() => controller.abort(), 45_000);
145
+ let streamedText = "";
146
+ let finalText = "";
147
+ let terminalError: string | undefined;
148
+
149
+ try {
150
+ for await (const event of engine.stream({
151
+ model: BUILDER_MODEL,
152
+ systemPrompt:
153
+ "Return only the valid JSON requested by the user. Do not use markdown fences or commentary.",
154
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
155
+ tools: [],
156
+ abortSignal: controller.signal,
157
+ maxOutputTokens: 16_000,
158
+ temperature: 0,
159
+ })) {
160
+ if (event.type === "text-delta") streamedText += event.text;
161
+ if (event.type === "assistant-content") {
162
+ finalText = event.parts
163
+ .filter((part) => part.type === "text")
164
+ .map((part) => part.text)
165
+ .join("")
166
+ .trim();
167
+ }
168
+ if (event.type === "stop" && event.reason === "error") {
169
+ terminalError = event.error ?? "Builder gateway returned an error";
170
+ }
171
+ if (event.type === "stop" && event.reason === "max_tokens") {
172
+ terminalError = "Builder gateway truncated the slide outline";
173
+ }
174
+ }
175
+ } finally {
176
+ clearTimeout(timeout);
177
+ }
178
+
179
+ if (terminalError) throw new Error(terminalError);
180
+ const text = (finalText || streamedText).trim();
181
+ if (!text) throw new Error("Builder gateway returned no slide outline");
182
+ return text;
183
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Legacy slide outlines now prefer the lowest-cost Luna model when Builder is connected.
@@ -16,7 +16,7 @@
16
16
  "db:generate": "drizzle-kit generate",
17
17
  "test": "vitest --run --config vitest.config.ts",
18
18
  "test:e2e": "playwright test",
19
- "test:e2e:install": "playwright install chromium",
19
+ "test:e2e:install": "playwright install --only-shell chromium",
20
20
  "test:all": "pnpm test && pnpm test:e2e"
21
21
  },
22
22
  "dependencies": {
@@ -36,7 +36,7 @@
36
36
  "zod": "^4.4.3"
37
37
  },
38
38
  "devDependencies": {
39
- "@playwright/test": "^1.61.0",
39
+ "@playwright/test": "catalog:",
40
40
  "@radix-ui/react-accordion": "^1.2.12",
41
41
  "@radix-ui/react-alert-dialog": "^1.1.15",
42
42
  "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -273,8 +273,8 @@ export interface ProductionAgentOptions {
273
273
  * `AGENT_CHAT_DURABLE_BACKGROUND` flag so the background function is emitted.
274
274
  */
275
275
  durableBackgroundRuns?: boolean;
276
- /** Called when a run starts, with the send function for emitting events and the threadId */
277
- onRunStart?: (send: (event: AgentChatEvent) => void, threadId: string) => void | Promise<void>;
276
+ /** Called when a run starts, with the send function, threadId, and runId. */
277
+ onRunStart?: (send: (event: AgentChatEvent) => void, threadId: string, runId: string) => void | Promise<void>;
278
278
  /**
279
279
  * Called after the engine + model are resolved for this request. Used by
280
280
  * the plugin layer to thread the parent's choices into sub-agents so
@@ -6870,7 +6870,7 @@ export function createProductionAgentHandler(options) {
6870
6870
  }
6871
6871
  // Notify listeners that a run has started (used by agent teams)
6872
6872
  if (options.onRunStart) {
6873
- await options.onRunStart(send, threadId ?? runId);
6873
+ await options.onRunStart(send, threadId ?? runId, runId);
6874
6874
  }
6875
6875
  // Resolve custom workspace agent mentions first.
6876
6876
  if (customAgentRefs.length > 0) {
package/dist/catalog.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "@assistant-ui/react": "^0.12.19",
3
+ "@playwright/test": "1.61.1",
3
4
  "@radix-ui/react-dialog": "^1.1.19",
4
5
  "@radix-ui/react-dropdown-menu": "^2.1.20",
5
6
  "@radix-ui/react-hover-card": "^1.1.19",
@@ -40,6 +41,7 @@
40
41
  "lowlight": "^3.3.0",
41
42
  "next-themes": "^0.4.6",
42
43
  "oxfmt": "0.56.0",
44
+ "playwright": "1.61.1",
43
45
  "react": "^19.2.7",
44
46
  "react-dom": "^19.2.7",
45
47
  "recharts": "^3.9.2",
@@ -4,12 +4,14 @@ import { useState, useEffect, useRef, useCallback } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
6
  import { agentNativePath } from "./api-path.js";
7
+ import { useT } from "./i18n.js";
7
8
  import { cn } from "./utils.js";
8
9
  /**
9
10
  * Rich preview card for a sub-agent task. Listens for agent-task-event
10
11
  * CustomEvents to update its state in real-time.
11
12
  */
12
13
  export function AgentTaskCard({ taskId, threadId, description, onOpen, }) {
14
+ const t = useT();
13
15
  const [expanded, setExpanded] = useState(true);
14
16
  const [status, setStatus] = useState("running");
15
17
  const [preview, setPreview] = useState("");
@@ -142,8 +144,8 @@ export function AgentTaskCard({ taskId, threadId, description, onOpen, }) {
142
144
  ? "border-destructive/30"
143
145
  : isComplete
144
146
  ? "border-emerald-500/20"
145
- : "border-border"), children: [_jsxs("button", { type: "button", "aria-expanded": expanded, className: "flex w-full items-center gap-2 px-3 py-2 text-start transition-colors hover:bg-muted/45", onClick: () => setExpanded(!expanded), children: [_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-md border border-border/60 bg-background/70 px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx(IconSubtask, { className: "h-3 w-3" }), "Sub-agent"] }), _jsx("span", { className: "min-w-0 flex-1 truncate text-xs font-medium text-foreground", children: taskTitle }), _jsxs("span", { className: statusClassName, children: [isRunning ? (_jsx(IconLoader2, { className: "h-3 w-3 animate-spin" })) : isError ? (_jsx(IconAlertCircle, { className: "h-3 w-3" })) : (_jsx(IconCheck, { className: "h-3 w-3" })), statusLabel] }), _jsx(IconChevronRight, { className: cn("h-3 w-3 shrink-0 text-muted-foreground/40 transition-transform duration-150", expanded && "rotate-90", "rtl:-scale-x-100") })] }), expanded && isRunning && currentStepText && (_jsxs("div", { className: "flex gap-1.5 border-t border-border/60 px-3 py-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "shrink-0 font-medium text-foreground/70", children: "Now:" }), _jsx("span", { className: "min-w-0 break-words", children: currentStepText })] })), expanded && hasContent && (_jsx("div", { className: "px-3 pb-2 pt-1", children: _jsx("div", { ref: previewRef, className: "agent-markdown prose prose-sm prose-invert max-h-48 max-w-none overflow-y-auto break-words rounded-md border border-border/50 bg-muted/25 px-3 py-2 text-xs text-muted-foreground", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: displayText.length > 800
147
+ : "border-border"), children: [_jsxs("button", { type: "button", "aria-expanded": expanded, className: "flex w-full items-center gap-2 px-3 py-2 text-start transition-colors hover:bg-muted/45", onClick: () => setExpanded(!expanded), children: [_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-md border border-border/60 bg-background/70 px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx(IconSubtask, { className: "h-3 w-3" }), t("agentTask.backgroundTask")] }), _jsx("span", { className: "min-w-0 flex-1 truncate text-xs font-medium text-foreground", children: taskTitle }), _jsxs("span", { className: statusClassName, children: [isRunning ? (_jsx(IconLoader2, { className: "h-3 w-3 animate-spin" })) : isError ? (_jsx(IconAlertCircle, { className: "h-3 w-3" })) : (_jsx(IconCheck, { className: "h-3 w-3" })), statusLabel] }), _jsx(IconChevronRight, { className: cn("h-3 w-3 shrink-0 text-muted-foreground/40 transition-transform duration-150", expanded && "rotate-90", "rtl:-scale-x-100") })] }), expanded && isRunning && currentStepText && (_jsxs("div", { className: "flex gap-1.5 border-t border-border/60 px-3 py-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "shrink-0 font-medium text-foreground/70", children: "Now:" }), _jsx("span", { className: "min-w-0 break-words", children: currentStepText })] })), expanded && hasContent && (_jsx("div", { className: "px-3 pb-2 pt-1", children: _jsx("div", { ref: previewRef, className: "agent-markdown prose prose-sm prose-invert max-h-48 max-w-none overflow-y-auto break-words rounded-md border border-border/50 bg-muted/25 px-3 py-2 text-xs text-muted-foreground", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: displayText.length > 800
146
148
  ? "..." + displayText.slice(-800)
147
- : displayText }) }) })), expanded && (_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pb-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground/60", children: !hasContent ? emptyMessage : "" }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [isRunning && (_jsxs("button", { onClick: handleStop, className: "inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:bg-muted hover:text-destructive", "aria-label": "Stop sub-agent", children: [_jsx(IconPlayerStop, { className: "h-3 w-3" }), "Stop"] })), _jsxs("button", { onClick: handleOpen, className: "inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground", children: ["Open thread", _jsx(IconExternalLink, { className: "h-3 w-3" })] })] })] }))] }));
149
+ : displayText }) }) })), expanded && (_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pb-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground/60", children: !hasContent ? emptyMessage : "" }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [isRunning && (_jsxs("button", { onClick: handleStop, className: "inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:bg-muted hover:text-destructive", "aria-label": t("agentTask.stop"), children: [_jsx(IconPlayerStop, { className: "h-3 w-3" }), "Stop"] })), _jsxs("button", { onClick: handleOpen, className: "inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground", children: [t("agentTask.openThread"), _jsx(IconExternalLink, { className: "h-3 w-3" })] })] })] }))] }));
148
150
  }
149
151
  //# sourceMappingURL=AgentTaskCard.js.map
@@ -349,6 +349,9 @@ const CORE_FALLBACK_MESSAGES = {
349
349
  "runsTray.finishedHours": "Finished {{count}}h ago",
350
350
  "runsTray.updatedDate": "Updated {{date}}",
351
351
  "runsTray.finishedDate": "Finished {{date}}",
352
+ "agentTask.backgroundTask": "Background task",
353
+ "agentTask.stop": "Stop background task",
354
+ "agentTask.openThread": "Open task thread",
352
355
  "codeRequired.fallbackDetail": "Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.",
353
356
  "codeRequired.defaultFeature": "Make the requested code changes to this app",
354
357
  "codeRequired.branchError": "Failed to create branch",
@@ -292,8 +292,8 @@ export function VoiceTranscriptionSection() {
292
292
  e.stopPropagation();
293
293
  focusKey("GOOGLE_APPLICATION_CREDENTIALS");
294
294
  }, className: "inline-flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-accent/40", children: ["Configure", _jsx(IconExternalLink, { size: 10 })] })) }), _jsx(ProviderOption, { id: "auto", selected: transcriptionMode === "batch" && provider === "auto", onSelect: () => chooseBatchProvider("auto"), title: "Automatic batch fallback", subtitle: "Keep the current Clips fallback chain: Builder Gemini, Gemini, Groq, then OpenAI." }), _jsx(ProviderOption, { id: "builder-gemini", selected: transcriptionMode === "batch" && provider === "builder-gemini", onSelect: () => chooseBatchProvider("builder-gemini"), disabled: !builderStatus?.configured, title: "Builder.io Connect", subtitle: builderStatus?.configured
295
- ? "Use Builder-hosted Gemini Flash-Lite for batch transcription and cleanup."
296
- : "One-click connect for Gemini Flash-Lite cleanup and batch transcription. No Google key needed.", rightSlot: builderStatus?.configured ? (_jsxs("span", { className: "flex items-center gap-1 text-[10px] text-green-500", children: [_jsx(IconCheck, { size: 10 }), "Connected"] })) : (_jsxs("button", { type: "button", onClick: (e) => {
295
+ ? "Use Builder-hosted Gemini Flash-Lite for batch transcription and Luna for text cleanup."
296
+ : "One-click connect for Gemini Flash-Lite transcription and Luna text cleanup. No Google key needed.", rightSlot: builderStatus?.configured ? (_jsxs("span", { className: "flex items-center gap-1 text-[10px] text-green-500", children: [_jsx(IconCheck, { size: 10 }), "Connected"] })) : (_jsxs("button", { type: "button", onClick: (e) => {
297
297
  e.stopPropagation();
298
298
  openBuilderConnect();
299
299
  }, className: "inline-flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-accent/40", children: ["Connect Builder.io", _jsx(IconExternalLink, { size: 10 })] })) }), _jsx(ProviderOption, { id: "gemini", selected: transcriptionMode === "batch" && provider === "gemini", onSelect: () => chooseBatchProvider("gemini"), title: "Google Gemini", subtitle: "BYOK Gemini for AI cleanup and optional strict batch transcription.", rightSlot: geminiConfigured === null ? null : geminiConfigured ? (_jsxs("span", { className: "flex items-center gap-1 text-[10px] text-green-500", children: [_jsx(IconCheck, { size: 10 }), "Key set"] })) : (_jsxs("button", { type: "button", onClick: (e) => {
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -17,12 +17,12 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
28
28
  //# sourceMappingURL=upload-image.d.ts.map
@@ -637,6 +637,11 @@ declare const messages: {
637
637
  updatedDate: string;
638
638
  finishedDate: string;
639
639
  };
640
+ agentTask: {
641
+ backgroundTask: string;
642
+ stop: string;
643
+ openThread: string;
644
+ };
640
645
  codeRequired: {
641
646
  fallbackDetail: string;
642
647
  defaultFeature: string;
@@ -642,6 +642,11 @@ const messages = {
642
642
  updatedDate: "Updated {{date}}",
643
643
  finishedDate: "Finished {{date}}",
644
644
  },
645
+ agentTask: {
646
+ backgroundTask: "Background task",
647
+ stop: "Stop background task",
648
+ openThread: "Open task thread",
649
+ },
645
650
  codeRequired: {
646
651
  fallbackDetail: "Edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way you like.",
647
652
  defaultFeature: "Make the requested code changes to this app",
@@ -10,6 +10,13 @@ export interface OAuthClientMetadataDocument {
10
10
  export declare function isUrlBasedOAuthClientId(clientId: string): boolean;
11
11
  export declare function validateOAuthClientMetadataUrl(clientId: string): URL;
12
12
  export declare function isAllowedOAuthRedirectUri(value: unknown): value is string;
13
+ /**
14
+ * RFC 8252 section 7.3: a native client's loopback redirect URI uses a port
15
+ * chosen at request time, so the authorization server must ignore the port when
16
+ * comparing loopback redirect URIs against the registered set. Non-loopback
17
+ * redirects keep exact matching, so this cannot widen into an open redirect.
18
+ */
19
+ export declare function matchesRegisteredRedirectUri(registered: readonly string[], redirectUri: string, applicationType: "native" | "web"): boolean;
13
20
  export declare function applicationTypeForRedirectUris(redirectUris: string[]): "native" | "web";
14
21
  export declare function resolveOAuthClientMetadataDocument(clientId: string): Promise<OAuthClientMetadataDocument>;
15
22
  //# sourceMappingURL=oauth-client-metadata.d.ts.map
@@ -73,6 +73,53 @@ export function isAllowedOAuthRedirectUri(value) {
73
73
  return false;
74
74
  }
75
75
  }
76
+ function isLoopbackRedirectHost(hostname) {
77
+ return (hostname === "localhost" ||
78
+ hostname === "127.0.0.1" ||
79
+ hostname === "::1" ||
80
+ hostname === "[::1]");
81
+ }
82
+ /**
83
+ * RFC 8252 section 7.3: a native client's loopback redirect URI uses a port
84
+ * chosen at request time, so the authorization server must ignore the port when
85
+ * comparing loopback redirect URIs against the registered set. Non-loopback
86
+ * redirects keep exact matching, so this cannot widen into an open redirect.
87
+ */
88
+ export function matchesRegisteredRedirectUri(registered, redirectUri, applicationType) {
89
+ let candidate;
90
+ try {
91
+ candidate = new URL(redirectUri);
92
+ // coercion-ok: a malformed candidate is an invalid redirect, not a runtime failure.
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ if (candidate.hash || candidate.username || candidate.password)
98
+ return false;
99
+ if (registered.includes(redirectUri))
100
+ return true;
101
+ if (applicationType !== "native" ||
102
+ candidate.protocol !== "http:" ||
103
+ !isLoopbackRedirectHost(candidate.hostname)) {
104
+ return false;
105
+ }
106
+ return registered.some((value) => {
107
+ let allowed;
108
+ try {
109
+ allowed = new URL(value);
110
+ // coercion-ok: a malformed registration cannot match a valid redirect.
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ if (allowed.hash || allowed.username || allowed.password)
116
+ return false;
117
+ return (allowed.protocol === candidate.protocol &&
118
+ allowed.hostname === candidate.hostname &&
119
+ allowed.pathname === candidate.pathname &&
120
+ allowed.search === candidate.search);
121
+ });
122
+ }
76
123
  export function applicationTypeForRedirectUris(redirectUris) {
77
124
  return redirectUris.some((value) => {
78
125
  const url = new URL(value);
@@ -13,7 +13,7 @@ import { getOrgDomain } from "../org/context.js";
13
13
  import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
14
14
  import { getAuthSecret } from "../server/better-auth-instance.js";
15
15
  import { readBody } from "../server/h3-helpers.js";
16
- import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
16
+ import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, matchesRegisteredRedirectUri, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
17
17
  import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, touchOAuthRefreshToken, } from "./oauth-store.js";
18
18
  import { MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS, MCP_OAUTH_SCOPES, normalizeOAuthScope, signMcpOAuthAccessToken, } from "./oauth-token.js";
19
19
  import { MCP_PUBLIC_ROUTE_PREFIX, MCP_ROUTE_PREFIXES } from "./route-paths.js";
@@ -291,7 +291,9 @@ async function handleRegister(event) {
291
291
  requestedApplicationType !== "web") {
292
292
  return oauthError("invalid_client_metadata", "application_type must be native or web");
293
293
  }
294
- const applicationType = requestedApplicationType ?? applicationTypeForRedirectUris(redirectUris);
294
+ const applicationType = requestedApplicationType === "native" || requestedApplicationType === "web"
295
+ ? requestedApplicationType
296
+ : applicationTypeForRedirectUris(redirectUris);
295
297
  const clientName = typeof body.client_name === "string"
296
298
  ? body.client_name.trim().slice(0, 120)
297
299
  : null;
@@ -303,6 +305,7 @@ async function handleRegister(event) {
303
305
  grantTypes: grantTypes.length ? grantTypes : undefined,
304
306
  responseTypes: responseTypes.length ? responseTypes : undefined,
305
307
  tokenEndpointAuthMethod: method,
308
+ applicationType,
306
309
  });
307
310
  }
308
311
  catch (err) {
@@ -555,7 +558,8 @@ async function handleAuthorize(event, options) {
555
558
  const client = isUrlBasedOAuthClientId(clientId)
556
559
  ? await resolveOAuthClientMetadataDocument(clientId).catch(() => null)
557
560
  : await getOAuthClient(clientId);
558
- if (!client || !client.redirectUris.includes(redirectUri)) {
561
+ if (!client ||
562
+ !matchesRegisteredRedirectUri(client.redirectUris, redirectUri, client.applicationType)) {
559
563
  return oauthError("invalid_client", "Unknown client or redirect_uri");
560
564
  }
561
565
  const session = await getSession(event);
@@ -26,6 +26,7 @@ export interface OAuthClientRow {
26
26
  grantTypes: string[];
27
27
  responseTypes: string[];
28
28
  tokenEndpointAuthMethod: string;
29
+ applicationType: "native" | "web";
29
30
  createdAt: number | null;
30
31
  }
31
32
  export interface OAuthCodeRow {
@@ -66,6 +67,7 @@ export declare function registerOAuthClient(params: {
66
67
  grantTypes?: string[];
67
68
  responseTypes?: string[];
68
69
  tokenEndpointAuthMethod?: string;
70
+ applicationType?: "native" | "web";
69
71
  }): Promise<OAuthClientRow>;
70
72
  export declare function getOAuthClient(clientId: string): Promise<OAuthClientRow | null>;
71
73
  export declare function createOAuthCode(params: {