@agent-native/core 0.160.1 → 0.161.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/bigquery.ts +57 -0
  3. package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
  4. package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
  5. package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
  6. package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
  7. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
  8. package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
  9. package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
  10. package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
  14. package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
  15. package/corpus/templates/calendar/actions/create-event.ts +7 -3
  16. package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
  17. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
  18. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
  19. package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
  20. package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
  21. package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
  22. package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
  23. package/corpus/templates/calendar/shared/api.ts +1 -1
  24. package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
  25. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
  26. package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
  27. package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
  28. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
  29. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
  30. package/corpus/templates/content/AGENTS.md +2 -0
  31. package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
  32. package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
  33. package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
  34. package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
  35. package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
  36. package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
  37. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
  38. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
  39. package/corpus/templates/content/parity/matrix.md +30 -30
  40. package/corpus/templates/content/parity/matrix.ts +4 -1
  41. package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
  42. package/corpus/templates/content/shared/database-block-actions.ts +82 -0
  43. package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
  44. package/corpus/templates/content/shared/nfm.ts +3 -2
  45. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
  46. package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
  47. package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
  48. package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
  49. package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
  50. package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
  51. package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
  52. package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
  53. package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
  54. package/corpus/templates/mail/shared/signature.ts +1 -10
  55. package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
  56. package/corpus/templates/slides/actions/patch-deck.ts +1 -1
  57. package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
  58. package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
  59. package/corpus/templates/slides/app/global.css +1 -0
  60. package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
  61. package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
  62. package/corpus/templates/slides/shared/deck-title.ts +12 -4
  63. package/dist/agent/engine/builder-engine.js +6 -1
  64. package/dist/agent/engine/error-detail.d.ts +17 -0
  65. package/dist/agent/engine/error-detail.js +27 -0
  66. package/dist/agent/production-agent.js +23 -5
  67. package/dist/agent/thread-data-builder.js +5 -0
  68. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  69. package/dist/agent/tool-approval-migrations.js +10 -0
  70. package/dist/agent/tool-approval-store.d.ts +12 -0
  71. package/dist/agent/tool-approval-store.js +46 -1
  72. package/dist/agent/types.d.ts +3 -1
  73. package/dist/client/AssistantChat.js +3 -1
  74. package/dist/client/error-format.js +15 -0
  75. package/dist/client/sse-event-processor.js +4 -0
  76. package/dist/collab/struct-routes.d.ts +1 -1
  77. package/dist/localization/core-messages/ar-SA.js +2 -0
  78. package/dist/localization/core-messages/de-DE.js +2 -0
  79. package/dist/localization/core-messages/en-US.d.ts +2 -0
  80. package/dist/localization/core-messages/en-US.js +2 -0
  81. package/dist/localization/core-messages/es-ES.js +2 -0
  82. package/dist/localization/core-messages/fr-FR.js +2 -0
  83. package/dist/localization/core-messages/hi-IN.js +2 -0
  84. package/dist/localization/core-messages/ja-JP.js +2 -0
  85. package/dist/localization/core-messages/ko-KR.js +2 -0
  86. package/dist/localization/core-messages/pt-BR.js +2 -0
  87. package/dist/localization/core-messages/zh-CN.js +2 -0
  88. package/dist/localization/core-messages/zh-TW.js +2 -0
  89. package/dist/localization/core-messages.d.ts +2 -0
  90. package/dist/observability/routes.d.ts +5 -5
  91. package/dist/resources/handlers.d.ts +1 -1
  92. package/dist/scripts/resources/read.js +4 -4
  93. package/dist/scripts/resources/save-memory.js +15 -2
  94. package/dist/scripts/resources/write.js +3 -3
  95. package/dist/secrets/routes.d.ts +9 -9
  96. package/dist/server/agent-chat/script-entries.d.ts +1 -0
  97. package/dist/server/agent-chat/script-entries.js +8 -0
  98. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  99. package/dist/server/transcribe-voice.d.ts +1 -1
  100. package/docs/content/onboarding.mdx +13 -2
  101. package/package.json +3 -3
@@ -69,6 +69,30 @@ Before creating or rewriting a draft, read the user's drafting settings with `pn
69
69
  - Follow `writingStyle` when present.
70
70
  - Keep generated copy natural and specific. Avoid generic AI email tropes, headings, and over-formal filler unless the user asks for that style.
71
71
 
72
+ ## Updating Durable Drafting Settings
73
+
74
+ Treat requests to permanently change, add to, or remove a writing-style rule as
75
+ settings changes, not drafting requests. Route them to `update-mail-settings`.
76
+ Do not call `manage-draft`, open the compose UI, or create a draft unless the user separately asks
77
+ to create or edit an email draft.
78
+
79
+ For a durable settings request:
80
+
81
+ 1. Read the current settings with `get-mail-settings`.
82
+ 2. Merge the requested rule into the existing `writingStyle`, preserving
83
+ unrelated instructions. For a removal, remove only the requested rule.
84
+ 3. If the requested change is ambiguous, conflicts with existing guidance, or
85
+ would materially rewrite the style, show the proposed merged wording and
86
+ ask the user to confirm before changing it.
87
+ 4. Call `update-mail-settings` with the complete merged `writingStyle` value.
88
+ 5. Re-read the settings with `get-mail-settings` and confirm the persisted result. Do not
89
+ report success based only on the update call response.
90
+
91
+ If the user asks both to change the durable style and to draft an email, update
92
+ the settings first, then create the separately requested draft using the
93
+ confirmed settings. A style-setting request alone must leave compose state
94
+ unchanged.
95
+
72
96
  ## How It Works
73
97
 
74
98
  1. **Write** `writeAppState("compose-{id}", draft)` — the shared application state row changes
@@ -17,7 +17,7 @@ function normalize(settings: Partial<UserSettings> | undefined, email: string) {
17
17
 
18
18
  export default defineAction({
19
19
  description:
20
- "Read the user's mail drafting settings, including configured signature and writing style.",
20
+ "Read the user's current mail drafting settings, including configured signature and writing style. Use this before updating a durable preference so unrelated settings can be preserved.",
21
21
  schema: z.object({}),
22
22
  http: { method: "GET" },
23
23
  readOnly: true,
@@ -11,16 +11,20 @@ const settingsSchema = z.object({
11
11
  signature: z
12
12
  .string()
13
13
  .optional()
14
- .describe("Markdown/plain-text signature to add to new drafts"),
14
+ .describe(
15
+ "Persistent Markdown/plain-text signature for new drafts. Preserve the existing signature unless the user asks to replace it.",
16
+ ),
15
17
  writingStyle: z
16
18
  .string()
17
19
  .optional()
18
- .describe("Short notes describing how generated drafts should sound"),
20
+ .describe(
21
+ "Persistent writing rules for generated drafts. Read the current setting first, merge the requested change, and preserve unrelated rules.",
22
+ ),
19
23
  });
20
24
 
21
25
  export default defineAction({
22
26
  description:
23
- "Update the user's mail drafting settings, including signature and writing style.",
27
+ "Update the user's persistent mail drafting settings, including signature and writing style. Use this for durable preferences, not email draft content. Read the current settings first and preserve fields the user did not ask to change.",
24
28
  schema: settingsSchema,
25
29
  run: async (args) => {
26
30
  const ownerEmail = getRequestUserEmail();
@@ -501,7 +501,9 @@ const messages = {
501
501
  signature: "Signature",
502
502
  importFromGmail: "Import from Gmail",
503
503
  signatureHelp:
504
- "Added to new drafts before quoted reply history. Markdown links and images are supported.",
504
+ "Added to new drafts before quoted reply history. Paste an image here or use Add image; Markdown links and images are supported.",
505
+ addSignatureImage: "Add image",
506
+ signatureImageUploadFailed: "Failed to upload signature image",
505
507
  writingStyle: "Writing style",
506
508
  writingStylePlaceholder: "Short, specific, warm. Avoid formal filler.",
507
509
  saveDraftingSettings: "Save drafting settings",
@@ -37,6 +37,7 @@ import {
37
37
  IconClock,
38
38
  IconPlayerPlay,
39
39
  IconSignature,
40
+ IconPhoto,
40
41
  IconFilter,
41
42
  IconInfoCircle,
42
43
  IconMessage2,
@@ -89,6 +90,7 @@ import {
89
90
  } from "@/hooks/use-automations";
90
91
  import { useSettings, useUpdateSettings } from "@/hooks/use-emails";
91
92
  import { useNavigationState } from "@/hooks/use-navigation-state";
93
+ import { openFilePicker, uploadFile } from "@/lib/upload";
92
94
  import { cn } from "@/lib/utils";
93
95
 
94
96
  import changelog from "../../CHANGELOG.md?raw";
@@ -1137,6 +1139,7 @@ function DraftingSection() {
1137
1139
  const queryClient = useQueryClient();
1138
1140
  const [signature, setSignature] = useState("");
1139
1141
  const [writingStyle, setWritingStyle] = useState("");
1142
+ const signatureSelectionRef = useRef({ start: 0, end: 0 });
1140
1143
  const importSignature = useActionMutation("import-gmail-signature", {
1141
1144
  onSuccess: (result) => {
1142
1145
  setSignature(result.signature);
@@ -1168,6 +1171,49 @@ function DraftingSection() {
1168
1171
  const isDirty =
1169
1172
  signature !== savedSignature || writingStyle !== savedWritingStyle;
1170
1173
 
1174
+ const insertSignatureImage = async (
1175
+ file: File,
1176
+ start: number,
1177
+ end: number,
1178
+ ) => {
1179
+ try {
1180
+ const result = await uploadFile(file);
1181
+ const markdown = `![${file.name.replace(/\.[^.]+$/, "")}](${result.url})`;
1182
+ setSignature(
1183
+ (current) =>
1184
+ `${current.slice(0, start)}${markdown}${current.slice(end)}`,
1185
+ );
1186
+ } catch {
1187
+ toast.error(t("settings.signatureImageUploadFailed"));
1188
+ }
1189
+ };
1190
+
1191
+ const handleSignaturePaste = (
1192
+ event: React.ClipboardEvent<HTMLTextAreaElement>,
1193
+ ) => {
1194
+ const image = Array.from(event.clipboardData.items)
1195
+ .find((item) => item.kind === "file" && item.type.startsWith("image/"))
1196
+ ?.getAsFile();
1197
+ if (!image) return;
1198
+ event.preventDefault();
1199
+ const target = event.currentTarget;
1200
+ void insertSignatureImage(
1201
+ image,
1202
+ target.selectionStart,
1203
+ target.selectionEnd,
1204
+ );
1205
+ };
1206
+
1207
+ const handleSignatureImage = async () => {
1208
+ const file = await openFilePicker("image/*");
1209
+ if (!file) return;
1210
+ await insertSignatureImage(
1211
+ file,
1212
+ signatureSelectionRef.current.start,
1213
+ signatureSelectionRef.current.end,
1214
+ );
1215
+ };
1216
+
1171
1217
  const handleSave = () => {
1172
1218
  updateSettings.mutate(
1173
1219
  {
@@ -1223,10 +1269,27 @@ function DraftingSection() {
1223
1269
  )}
1224
1270
  {t("settings.importFromGmail")}
1225
1271
  </Button>
1272
+ <Button
1273
+ type="button"
1274
+ variant="ghost"
1275
+ size="sm"
1276
+ className="h-6 px-2 text-[11px]"
1277
+ onClick={() => void handleSignatureImage()}
1278
+ >
1279
+ <IconPhoto className="h-3 w-3" />
1280
+ {t("settings.addSignatureImage")}
1281
+ </Button>
1226
1282
  </div>
1227
1283
  <Textarea
1228
1284
  value={signature}
1229
1285
  onChange={(event) => setSignature(event.target.value)}
1286
+ onPaste={handleSignaturePaste}
1287
+ onSelect={(event) => {
1288
+ signatureSelectionRef.current = {
1289
+ start: event.currentTarget.selectionStart,
1290
+ end: event.currentTarget.selectionEnd,
1291
+ };
1292
+ }}
1230
1293
  placeholder={"Best,\nSteve"}
1231
1294
  rows={5}
1232
1295
  className="resize-none px-3 py-2 text-[13px] placeholder:text-muted-foreground/40"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-17
4
+ ---
5
+
6
+ Gmail signatures now preserve images and support paste or upload in Mail
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-17
4
+ ---
5
+
6
+ Mail now updates durable drafting preferences without opening a draft
@@ -23,6 +23,7 @@ const INITIAL_TOOL_NAMES = [
23
23
  "refresh-list",
24
24
  "navigate",
25
25
  "get-mail-settings",
26
+ "update-mail-settings",
26
27
  "find-contact",
27
28
  "provider-api-catalog",
28
29
  "provider-api-docs",
@@ -172,6 +173,24 @@ Before drafting or rewriting email copy, run \`get-mail-settings\`.
172
173
  - Follow \`writingStyle\` when present.
173
174
  - Draft bodies use Markdown only. Avoid generic AI email tropes, headings, and over-formal filler unless the user explicitly asks for a formal template.
174
175
 
176
+ ## Durable Drafting Preferences — CRITICAL
177
+
178
+ Writing style, signature, and other drafting preferences are persistent mail
179
+ settings, not email drafts. If the user asks to add, change, strengthen,
180
+ remove, or remember a writing rule or preference (for example, "never use em
181
+ dashes"), do this instead of composing an email:
182
+
183
+ 1. Run \`get-mail-settings\`.
184
+ 2. Merge the requested change into the existing \`writingStyle\` or
185
+ \`signature\`, preserving unrelated instructions.
186
+ 3. Run \`update-mail-settings\` with the complete merged value.
187
+ 4. Confirm the returned setting was updated.
188
+
189
+ Do not call \`manage-draft\`, \`queue-email-draft\`, or \`send-email\` for a
190
+ preference-only request. Only create or edit an email when the user separately
191
+ asks for an email. If a request asks for both a preference change and an email,
192
+ update the preference first, then read it again and apply it to the email.
193
+
175
194
  When the user asks to draft/email a specific person (e.g., "email my wife", "draft an email to Alice"):
176
195
  - This is a NEW email \u2014 use manage-draft with --action=create and mode "compose", NOT "reply"
177
196
  - Look up the recipient's email from AGENTS.md contacts or ask the user
@@ -45,7 +45,16 @@ export function htmlSignatureToMarkdown(html: string): string {
45
45
  .replace(/<style[\s\S]*?<\/style>/gi, "");
46
46
 
47
47
  next = next.replace(/<br\s*\/?>/gi, "\n");
48
- next = next.replace(/<img\b[^>]*>/gi, "");
48
+ const imageTokens: string[] = [];
49
+ next = next.replace(/<img\b[^>]*>/gi, (tag) => {
50
+ const src = safeUrl(attr(tag, "src"));
51
+ if (!src) return "";
52
+ const alt = cleanMarkdown(attr(tag, "alt") || attr(tag, "title") || "");
53
+ const image = `![${alt.replace(/]/g, "\\]")}](${normalizeMarkdownUrl(src)})`;
54
+ const token = `\uE100${imageTokens.length}\uE101`;
55
+ imageTokens.push(image);
56
+ return token;
57
+ });
49
58
  next = next.replace(
50
59
  /<a\b[^>]*href\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)[^>]*>([\s\S]*?)<\/a>/gi,
51
60
  (match, label) => {
@@ -69,5 +78,8 @@ export function htmlSignatureToMarkdown(html: string): string {
69
78
  )
70
79
  .replace(/<[^>]+>/g, "");
71
80
 
72
- return cleanMarkdown(next);
81
+ return cleanMarkdown(next).replace(
82
+ /\uE100(\d+)\uE101/g,
83
+ (match, index: string) => imageTokens[Number(index)] ?? match,
84
+ );
73
85
  }
@@ -14,16 +14,7 @@ function splitQuotedContent(body: string): [string, string] {
14
14
  }
15
15
 
16
16
  export function normalizeSignature(signature?: string | null): string {
17
- return stripSignatureImages(signature ?? "").trim();
18
- }
19
-
20
- export function stripSignatureImages(signature: string): string {
21
- return signature
22
- .replace(
23
- /\[!\[[\s\S]*?\]\((?:https?:\/\/|data:image\/)[^)]*\)\]\([^)]*\)/gi,
24
- "",
25
- )
26
- .replace(/!\[[\s\S]*?\]\((?:https?:\/\/|data:image\/)[^)]*\)/gi, "")
17
+ return (signature ?? "")
27
18
  .split("\n")
28
19
  .map((line) => line.replace(/[ \t]+$/g, ""))
29
20
  .join("\n")
@@ -62,6 +62,8 @@ Rules:
62
62
  - Last slide should be a summary or call-to-action
63
63
  - Content should be concise and presentation-ready (not paragraphs)
64
64
  - Use bullet points for lists, keep each point brief
65
+ - Keep title, section, statement, and call-to-action slides centered with generous, even margins
66
+ - Do not use emoji as decorative icons or bullets; use plain text bullets or HTML/CSS shapes instead
65
67
  - Do not invent factual numbers, metrics, URLs, source attributions, dates, success rates, benchmarks, customer names, or case-study results. Only include concrete factual claims if they are present in the topic/context. If a useful metric is unknown, use qualitative wording, [metric TBD], or clearly label it as a draft assumption.
66
68
  - ${imageInstruction}
67
69
 
@@ -357,7 +357,7 @@ export function applyOperation(deck: any, op: Operation): void {
357
357
  if (slides.length === 0 && !op.allowEmpty) {
358
358
  slides.push({
359
359
  id: `slide-${Date.now()}-fallback`,
360
- content: `<div class="fmd-slide" style="padding: 80px 110px; display: flex; flex-direction: column; justify-content: center;"><div style="font-size: 28px; font-weight: 600; color: rgba(255,255,255,0.4);">Double-click to edit</div></div>`,
360
+ content: `<div class="fmd-slide" style="box-sizing: border-box; width: 100%; height: 100%; padding: 80px 110px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;"><div style="font-size: 28px; font-weight: 600; color: hsl(var(--muted-foreground) / 0.4);">Double-click to edit</div></div>`,
361
361
  notes: "",
362
362
  layout: "blank",
363
363
  });
@@ -26,6 +26,7 @@ export function ThemeToggle({ className }: { className?: string }) {
26
26
  <Button
27
27
  variant="ghost"
28
28
  size="icon"
29
+ aria-label={t("root.toggleTheme")}
29
30
  onClick={() => setTheme(isDark ? "light" : "dark")}
30
31
  className={cn("h-7 w-7 text-muted-foreground", className)}
31
32
  >
@@ -0,0 +1,59 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import type { ReactElement, ReactNode } from "react";
3
+
4
+ import {
5
+ ContextMenu,
6
+ ContextMenuContent,
7
+ ContextMenuItem,
8
+ ContextMenuSeparator,
9
+ ContextMenuTrigger,
10
+ } from "@/components/ui/context-menu";
11
+
12
+ interface SlideThumbnailContextMenuProps {
13
+ children: ReactElement;
14
+ canDelete?: boolean;
15
+ onSelect: () => void;
16
+ onDuplicate: () => void;
17
+ onDelete: () => void;
18
+ /** Optional extra items for the owning editor to add later. */
19
+ childrenAfterActions?: ReactNode;
20
+ }
21
+
22
+ /**
23
+ * Keep slide actions discoverable without removing the compact hover buttons.
24
+ * The trigger stays the thumbnail itself, so a right-click works anywhere on
25
+ * the preview and the menu remains usable with a keyboard once the thumbnail
26
+ * has focus.
27
+ */
28
+ export function SlideThumbnailContextMenu({
29
+ children,
30
+ canDelete = true,
31
+ onSelect,
32
+ onDuplicate,
33
+ onDelete,
34
+ childrenAfterActions,
35
+ }: SlideThumbnailContextMenuProps) {
36
+ const t = useT();
37
+
38
+ return (
39
+ <ContextMenu>
40
+ <ContextMenuTrigger asChild onContextMenu={onSelect}>
41
+ {children}
42
+ </ContextMenuTrigger>
43
+ <ContextMenuContent>
44
+ <ContextMenuItem onSelect={onDuplicate}>
45
+ {t("editorSidebar.duplicateSlide")}
46
+ </ContextMenuItem>
47
+ <ContextMenuSeparator />
48
+ {childrenAfterActions}
49
+ <ContextMenuItem
50
+ disabled={!canDelete}
51
+ onSelect={onDelete}
52
+ className="text-destructive focus:text-destructive"
53
+ >
54
+ {t("editorSidebar.deleteSlide")}
55
+ </ContextMenuItem>
56
+ </ContextMenuContent>
57
+ </ContextMenu>
58
+ );
59
+ }
@@ -431,6 +431,7 @@ button[title="Open agent sidebar"] {
431
431
 
432
432
  /* Full-bleed wrapper for blank-layout slides */
433
433
  .fmd-slide {
434
+ position: relative;
434
435
  width: 100%;
435
436
  height: 100%;
436
437
  display: flex;
@@ -6,16 +6,45 @@
6
6
  * value here makes a successful-looking agent edit a no-op in the renderer.
7
7
  */
8
8
  export function normalizeSlidePadding(html: string): string {
9
- if (!html.includes('class="fmd-slide"')) return html;
9
+ for (const match of html.matchAll(/<div\b[^>]*>/gi)) {
10
+ const openingTag = match[0];
11
+ const classMatch = /\bclass\s*=\s*(["'])(.*?)\1/i.exec(openingTag);
10
12
 
11
- return html.replace(
12
- /(<div\b[^>]*\bclass="fmd-slide"[^>]*\bstyle=")([^"]*)(")/,
13
- (_match, before, style, after) => {
14
- const hasPadding = /(?:^|;)\s*padding\s*:/i.test(style);
15
- const nextStyle = hasPadding
16
- ? style
17
- : `padding: 80px 110px;${style.startsWith(" ") ? "" : " "}${style}`;
18
- return `${before}${nextStyle}${after}`;
19
- },
20
- );
13
+ if (!classMatch || !/\bfmd-slide\b/i.test(classMatch[2])) continue;
14
+
15
+ const styleMatch = /\bstyle\s*=\s*(["'])(.*?)\1/i.exec(openingTag);
16
+ if (styleMatch) {
17
+ const style = styleMatch[2];
18
+ if (/(?:^|;)\s*padding\s*:/i.test(style)) return html;
19
+
20
+ const nextStyle = `padding: 80px 110px;${
21
+ style.startsWith(" ") ? "" : " "
22
+ }${style}`;
23
+ const nextStyleAttribute = styleMatch[0].replace(style, nextStyle);
24
+ const nextOpeningTag = openingTag.replace(
25
+ styleMatch[0],
26
+ nextStyleAttribute,
27
+ );
28
+
29
+ return (
30
+ html.slice(0, match.index) +
31
+ nextOpeningTag +
32
+ html.slice(match.index + openingTag.length)
33
+ );
34
+ }
35
+
36
+ const classEnd = classMatch.index + classMatch[0].length;
37
+ const nextOpeningTag =
38
+ openingTag.slice(0, classEnd) +
39
+ ' style="padding: 80px 110px;"' +
40
+ openingTag.slice(classEnd);
41
+
42
+ return (
43
+ html.slice(0, match.index) +
44
+ nextOpeningTag +
45
+ html.slice(match.index + openingTag.length)
46
+ );
47
+ }
48
+
49
+ return html;
21
50
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-17
4
+ ---
5
+
6
+ Slides now keep generated layouts stable, surface both-axis overflow, and make slide and text-box editing easier to discover.
@@ -107,14 +107,22 @@ export function deriveDeckTitleFromSlideContent(
107
107
  }
108
108
  }
109
109
 
110
- const styledTextPattern =
111
- /<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]*>([\s\S]*?)<\/\1>/gi;
112
- for (const match of content.matchAll(styledTextPattern)) {
110
+ const styledOpeningPattern =
111
+ /<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]*>/gi;
112
+ for (const match of content.matchAll(styledOpeningPattern)) {
113
113
  const fontSize = Number.parseFloat(
114
114
  match[3]?.match(/(?:^|;)\s*font-size\s*:\s*([\d.]+)px/i)?.[1] ?? "0",
115
115
  );
116
116
  if (fontSize < 28) continue;
117
- const text = usableCandidate(match[4] ?? "");
117
+
118
+ const bodyStart = (match.index ?? 0) + match[0].length;
119
+ const closingTag = new RegExp(`</${match[1]}\\s*>`, "i").exec(
120
+ content.slice(bodyStart),
121
+ );
122
+ const body = closingTag
123
+ ? content.slice(bodyStart, bodyStart + closingTag.index)
124
+ : "";
125
+ const text = usableCandidate(body);
118
126
  if (text) candidates.push({ score: fontSize, text });
119
127
  }
120
128
 
@@ -21,7 +21,7 @@ import { isInBackgroundFunctionRuntime } from "../durable-background.js";
21
21
  import { BUILDER_MODEL_CONFIG } from "../model-config.js";
22
22
  import { getBuilderGatewayRequestHeaders } from "./builder-gateway-headers.js";
23
23
  import { gatewayVisitorFacingError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./credential-errors.js";
24
- import { classifyTerminalErrorCode, describeErrorWithCauses, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./error-detail.js";
24
+ import { classifyTerminalErrorCode, describeErrorWithCauses, isBuilderGatewayInternalErrorMessage, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./error-detail.js";
25
25
  import { FIRST_STREAM_EVENT_TIMEOUT_MS } from "./first-event-timeout.js";
26
26
  import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
27
27
  import { splitSystemPromptForCache, stablePrefixCacheControl, } from "./prompt-cache.js";
@@ -340,6 +340,11 @@ function isTransientGatewayFailure(rawMessage, status) {
340
340
  if (status !== undefined && RETRYABLE_GATEWAY_STATUSES.has(status)) {
341
341
  return true;
342
342
  }
343
+ // The gateway's unhandled-500 envelope, which reaches the in-stream error
344
+ // frame with no status at all. Without this it read as terminal there while
345
+ // the identical body read as retryable when it arrived as an HTTP 500.
346
+ if (isBuilderGatewayInternalErrorMessage(rawMessage))
347
+ return true;
343
348
  return TRANSIENT_UPSTREAM_PATTERN.test(rawMessage);
344
349
  }
345
350
  /**
@@ -35,6 +35,23 @@ export declare function isProviderConnectionError(err: unknown): boolean;
35
35
  * for every other engine, which still delivers its own message intact.
36
36
  */
37
37
  export declare function isContextOverflowMessage(message: string): boolean;
38
+ /**
39
+ * The Builder gateway's own 500 envelope, which is the whole message: an
40
+ * apology sentence plus a correlation id, e.g. "Sorry, we ran into an issue
41
+ * processing your request. ERROR ID: 0f3c...". The apology prose varies; the
42
+ * correlation id does not, so that is what the predicate below anchors on.
43
+ */
44
+ export declare const BUILDER_GATEWAY_INTERNAL_ERROR_CODE = "builder_gateway_internal_error";
45
+ /**
46
+ * The gateway attaches that envelope to an unhandled 500, and it arrives two
47
+ * ways: as an HTTP body, which is already retried because 500 is in the
48
+ * engine's retryable status set, and as an in-stream error frame after the
49
+ * gateway has already answered 200, where there is no status to read and the
50
+ * prose carries no keyword any other predicate here matches. Naming it is what
51
+ * makes the second path behave like the first instead of dying uncoded on the
52
+ * first attempt.
53
+ */
54
+ export declare function isBuilderGatewayInternalErrorMessage(message: string): boolean;
38
55
  /** The overflow codes a provider or gateway may report instead of prose. */
39
56
  export declare function isContextOverflowCode(code: string | undefined): boolean;
40
57
  /** Classification fields an AI SDK provider failure carries. */
@@ -83,6 +83,28 @@ export function isContextOverflowMessage(message) {
83
83
  msg.includes("input token count exceeds") ||
84
84
  msg.includes("request too large"));
85
85
  }
86
+ /**
87
+ * The Builder gateway's own 500 envelope, which is the whole message: an
88
+ * apology sentence plus a correlation id, e.g. "Sorry, we ran into an issue
89
+ * processing your request. ERROR ID: 0f3c...". The apology prose varies; the
90
+ * correlation id does not, so that is what the predicate below anchors on.
91
+ */
92
+ export const BUILDER_GATEWAY_INTERNAL_ERROR_CODE = "builder_gateway_internal_error";
93
+ const BUILDER_GATEWAY_ERROR_ID_PATTERN = /\berror id:\s*([0-9a-f]+)\b/i;
94
+ const BUILDER_GATEWAY_ERROR_ID_MIN_CHARS = 8;
95
+ /**
96
+ * The gateway attaches that envelope to an unhandled 500, and it arrives two
97
+ * ways: as an HTTP body, which is already retried because 500 is in the
98
+ * engine's retryable status set, and as an in-stream error frame after the
99
+ * gateway has already answered 200, where there is no status to read and the
100
+ * prose carries no keyword any other predicate here matches. Naming it is what
101
+ * makes the second path behave like the first instead of dying uncoded on the
102
+ * first attempt.
103
+ */
104
+ export function isBuilderGatewayInternalErrorMessage(message) {
105
+ const match = BUILDER_GATEWAY_ERROR_ID_PATTERN.exec(message);
106
+ return (match !== null && match[1].length >= BUILDER_GATEWAY_ERROR_ID_MIN_CHARS);
107
+ }
86
108
  /** The overflow codes a provider or gateway may report instead of prose. */
87
109
  export function isContextOverflowCode(code) {
88
110
  const normalized = (code ?? "").toLowerCase();
@@ -203,5 +225,10 @@ export function classifyTerminalErrorCode(message) {
203
225
  if (/(?:err_)?ssl|tlsv?\d|tls handshake|ssl routines|econnreset|econnrefused|und_err_socket|socket hang up/i.test(message)) {
204
226
  return "provider_network_error";
205
227
  }
228
+ // Last, so a gateway 500 whose body happens to quote a more specific upstream
229
+ // failure keeps that classification instead of collapsing to this one.
230
+ if (isBuilderGatewayInternalErrorMessage(message)) {
231
+ return BUILDER_GATEWAY_INTERNAL_ERROR_CODE;
232
+ }
206
233
  return undefined;
207
234
  }
@@ -28,7 +28,7 @@ import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroun
28
28
  import { applyContextXrayTransformForIteration } from "./engine/context-directives-transform.js";
29
29
  import { attemptContinuationDispatch } from "./engine/continuation-dispatch-retry.js";
30
30
  import { formatLlmCredentialErrorMessage, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
31
- import { isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./engine/error-detail.js";
31
+ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./engine/error-detail.js";
32
32
  import { resolveEngine, explicitEngineName, registerBuiltinEngines, getStoredModelForEngine, normalizeModelForEngine, isResolvedEngineUsableForRequest, } from "./engine/index.js";
33
33
  import { resolveEmptyResponseRetryMaxOutputTokens, resolveMainChatMaxOutputTokens, resolveMaxOutputTokensForEngine, } from "./engine/output-tokens.js";
34
34
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
@@ -42,7 +42,7 @@ import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js
42
42
  import { startRun, subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterCompletedToolWithoutAssistantFinal, } from "./run-manager.js";
43
43
  import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, } from "./run-store.js";
44
44
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
45
- import { consumeAgentToolApproval, createAgentToolApproval, } from "./tool-approval-store.js";
45
+ import { consumeAgentToolApproval, createAgentToolApproval, resolveAgentToolApprovalTurnId, } from "./tool-approval-store.js";
46
46
  import { findCompletedJournalEntry, } from "./tool-call-journal.js";
47
47
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
48
48
  import { describeToolResultImages, extractAgentImagesFromActionResult, } from "./tool-result-images.js";
@@ -880,6 +880,9 @@ export function isRetryableError(err) {
880
880
  return true;
881
881
  }
882
882
  return (code === "builder_gateway_error" ||
883
+ // The gateway's unhandled-500 envelope arriving in-stream, where there is no
884
+ // status to read. Same failure as `http_500` below, so same verdict.
885
+ code === BUILDER_GATEWAY_INTERNAL_ERROR_CODE ||
883
886
  code === "builder_gateway_network_error" ||
884
887
  code === "provider_network_error" ||
885
888
  code === "http_429" ||
@@ -5177,6 +5180,8 @@ export function isRecoverableContinuationError(event) {
5177
5180
  // the server read the sentence instead, which a Builder-credits deployment
5178
5181
  // replaces with one visitor line.
5179
5182
  code === "http_500" ||
5183
+ // The same 500, delivered in-stream with no status attached.
5184
+ code === BUILDER_GATEWAY_INTERNAL_ERROR_CODE ||
5180
5185
  code === "http_502" ||
5181
5186
  code === "http_503" ||
5182
5187
  code === "http_504" ||
@@ -6976,12 +6981,25 @@ export function createProductionAgentHandler(options) {
6976
6981
  const isChainedBackgroundContinuation = isBackgroundWorker && backgroundContinuationCount > 0;
6977
6982
  const runId = backgroundRunMarker?.runId ?? generateRunId();
6978
6983
  const effectiveThreadId = threadId ?? runId;
6984
+ const resolvedApprovalTurnId = !isBackgroundWorker &&
6985
+ ownerEmail &&
6986
+ threadId &&
6987
+ requestedApprovedToolCalls?.length
6988
+ ? await resolveAgentToolApprovalTurnId({
6989
+ ownerEmail,
6990
+ orgId: getRequestOrgId() ?? null,
6991
+ threadId,
6992
+ requestedTurnId: requestTurnId,
6993
+ approvalKeys: requestedApprovedToolCalls,
6994
+ })
6995
+ : null;
6979
6996
  const effectiveTurnId = typeof backgroundRunMarker?.turnId === "string" &&
6980
6997
  backgroundRunMarker.turnId.trim()
6981
6998
  ? backgroundRunMarker.turnId.trim()
6982
- : typeof requestTurnId === "string" && requestTurnId.trim()
6983
- ? requestTurnId.trim()
6984
- : runId;
6999
+ : (resolvedApprovalTurnId ??
7000
+ (typeof requestTurnId === "string" && requestTurnId.trim()
7001
+ ? requestTurnId.trim()
7002
+ : runId));
6985
7003
  const approvalStoreBinding = (binding) => {
6986
7004
  if (!ownerEmail) {
6987
7005
  throw new Error("Cannot create or consume an approval without an authenticated owner");
@@ -1,5 +1,6 @@
1
1
  import { formatChatErrorText, normalizeChatError, } from "../client/error-format.js";
2
2
  import { isCredentialGapCodeAgentEvent, normalizeCodeAgentTranscript, } from "../code-agents/transcript-normalizer.js";
3
+ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE } from "./engine/error-detail.js";
3
4
  const INTERRUPTED_TOOL_RESULT = "Interrupted before this tool returned a result.";
4
5
  export const ASSISTANT_RUN_DURATION_METADATA_KEY = "agentNativeRunDurationMs";
5
6
  const MAX_STORED_ATTACHMENT_CHARS = 60_000;
@@ -20,6 +21,10 @@ function isInternalContinuationError(event) {
20
21
  code === "http_408" ||
21
22
  code === "http_429" ||
22
23
  code === "http_500" ||
24
+ // The gateway's unhandled-500 envelope arriving in-stream. Without this the
25
+ // turn stored Builder's internal correlation id as the assistant's visible
26
+ // answer instead of a continuation.
27
+ code === BUILDER_GATEWAY_INTERNAL_ERROR_CODE ||
23
28
  code === "http_502" ||
24
29
  code === "http_503" ||
25
30
  code === "http_504" ||
@@ -34,6 +34,7 @@ export declare const AGENT_TOOL_APPROVAL_TABLE_SQL: {
34
34
  };
35
35
  export declare const AGENT_TOOL_APPROVAL_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding\n ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)";
36
36
  export declare const AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_logical\n ON agent_tool_approvals(owner_email, org_id, thread_id, turn_id, tool_name, approval_key_hash, status)";
37
+ export declare const AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_recovery\n ON agent_tool_approvals(owner_email, org_id, thread_id, approval_key_hash, status, turn_id)";
37
38
  /**
38
39
  * Durable approval grants are created and consumed on request paths, but their
39
40
  * schema belongs to the release migration boundary in production.