@agent-native/core 0.160.0 → 0.161.0

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 (97) 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/production-agent.js +27 -15
  64. package/dist/agent/tool-approval-migrations.d.ts +2 -0
  65. package/dist/agent/tool-approval-migrations.js +20 -0
  66. package/dist/agent/tool-approval-store.d.ts +17 -3
  67. package/dist/agent/tool-approval-store.js +56 -6
  68. package/dist/agent/types.d.ts +3 -1
  69. package/dist/cli/code-agent-connector.js +79 -7
  70. package/dist/cli/portal-transfer.d.ts +43 -0
  71. package/dist/cli/portal-transfer.js +201 -0
  72. package/dist/client/AssistantChat.js +3 -1
  73. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  74. package/dist/localization/core-messages/ar-SA.js +1 -0
  75. package/dist/localization/core-messages/de-DE.js +1 -0
  76. package/dist/localization/core-messages/en-US.d.ts +1 -0
  77. package/dist/localization/core-messages/en-US.js +1 -0
  78. package/dist/localization/core-messages/es-ES.js +1 -0
  79. package/dist/localization/core-messages/fr-FR.js +1 -0
  80. package/dist/localization/core-messages/hi-IN.js +1 -0
  81. package/dist/localization/core-messages/ja-JP.js +1 -0
  82. package/dist/localization/core-messages/ko-KR.js +1 -0
  83. package/dist/localization/core-messages/pt-BR.js +1 -0
  84. package/dist/localization/core-messages/zh-CN.js +1 -0
  85. package/dist/localization/core-messages/zh-TW.js +1 -0
  86. package/dist/localization/core-messages.d.ts +1 -0
  87. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  88. package/dist/resources/handlers.d.ts +1 -1
  89. package/dist/scripts/resources/read.js +4 -4
  90. package/dist/scripts/resources/save-memory.js +15 -2
  91. package/dist/scripts/resources/write.js +3 -3
  92. package/dist/server/agent-chat/script-entries.d.ts +1 -0
  93. package/dist/server/agent-chat/script-entries.js +8 -0
  94. package/dist/server/realtime-token.d.ts +1 -1
  95. package/dist/server/transcribe-voice.d.ts +1 -1
  96. package/docs/content/onboarding.mdx +13 -2
  97. package/package.json +1 -1
@@ -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
 
@@ -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";
@@ -6923,6 +6923,11 @@ export function createProductionAgentHandler(options) {
6923
6923
  .filter((key) => typeof key === "string")
6924
6924
  .slice(0, 200)
6925
6925
  : undefined;
6926
+ // The durable approval row is the authorization boundary. Do not require
6927
+ // the client to reproduce the original structured history exactly: the UI
6928
+ // may truncate tool arguments and intentionally assigns fresh replay ids.
6929
+ // The loop still consumes only a matching server-created grant for the
6930
+ // current owner/org/thread/turn/tool/input tuple.
6926
6931
  const exactApprovedToolCall = findApprovedStructuredToolCall(structuredHistory, requestedApprovedToolCalls);
6927
6932
  const firstRequestPayloadDetail = buildFirstRequestPayloadDetail({
6928
6933
  isFirstRequest: history.length === 0,
@@ -6971,12 +6976,25 @@ export function createProductionAgentHandler(options) {
6971
6976
  const isChainedBackgroundContinuation = isBackgroundWorker && backgroundContinuationCount > 0;
6972
6977
  const runId = backgroundRunMarker?.runId ?? generateRunId();
6973
6978
  const effectiveThreadId = threadId ?? runId;
6979
+ const resolvedApprovalTurnId = !isBackgroundWorker &&
6980
+ ownerEmail &&
6981
+ threadId &&
6982
+ requestedApprovedToolCalls?.length
6983
+ ? await resolveAgentToolApprovalTurnId({
6984
+ ownerEmail,
6985
+ orgId: getRequestOrgId() ?? null,
6986
+ threadId,
6987
+ requestedTurnId: requestTurnId,
6988
+ approvalKeys: requestedApprovedToolCalls,
6989
+ })
6990
+ : null;
6974
6991
  const effectiveTurnId = typeof backgroundRunMarker?.turnId === "string" &&
6975
6992
  backgroundRunMarker.turnId.trim()
6976
6993
  ? backgroundRunMarker.turnId.trim()
6977
- : typeof requestTurnId === "string" && requestTurnId.trim()
6978
- ? requestTurnId.trim()
6979
- : runId;
6994
+ : (resolvedApprovalTurnId ??
6995
+ (typeof requestTurnId === "string" && requestTurnId.trim()
6996
+ ? requestTurnId.trim()
6997
+ : runId));
6980
6998
  const approvalStoreBinding = (binding) => {
6981
6999
  if (!ownerEmail) {
6982
7000
  throw new Error("Cannot create or consume an approval without an authenticated owner");
@@ -7001,14 +7019,7 @@ export function createProductionAgentHandler(options) {
7001
7019
  return consumeAgentToolApproval(approvalStoreBinding(binding));
7002
7020
  },
7003
7021
  };
7004
- const exactApprovedToolEntry = exactApprovedToolCall
7005
- ? requestActions[exactApprovedToolCall.name]
7006
- : undefined;
7007
- const approvedToolCallsForExecution = exactApprovedToolCall && exactApprovedToolEntry?.needsApproval
7008
- ? [
7009
- toolCallCacheKey(exactApprovedToolCall.name, exactApprovedToolCall.input),
7010
- ]
7011
- : undefined;
7022
+ const approvedToolCallsForExecution = requestedApprovedToolCalls;
7012
7023
  if (isBackgroundWorker &&
7013
7024
  (await isTurnAborted(effectiveThreadId, effectiveTurnId))) {
7014
7025
  await markRunAborted(runId, "user").catch(() => { });
@@ -7788,9 +7799,10 @@ export function createProductionAgentHandler(options) {
7788
7799
  ...(threadId
7789
7800
  ? { threadId: effectiveThreadId, turnId: effectiveTurnId }
7790
7801
  : {}),
7791
- // Human-in-the-loop approval grants for this turn (sanitized the
7792
- // request is untrusted; only the exact structured call is passed to
7793
- // the loop, where the durable grant is consumed atomically.
7802
+ // Human-in-the-loop approval grants for this turn. The request is
7803
+ // untrusted; the durable approval consumer below validates every key
7804
+ // against the authenticated owner/org/thread/turn and consumes it
7805
+ // atomically for the exact tool/input tuple.
7794
7806
  ...(approvedToolCallsForExecution
7795
7807
  ? { approvedToolCalls: approvedToolCallsForExecution }
7796
7808
  : {}),
@@ -33,6 +33,8 @@ export declare const AGENT_TOOL_APPROVAL_TABLE_SQL: {
33
33
  )`;
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
+ 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)";
36
38
  /**
37
39
  * Durable approval grants are created and consumed on request paths, but their
38
40
  * schema belongs to the release migration boundary in production.
@@ -33,6 +33,10 @@ export const AGENT_TOOL_APPROVAL_TABLE_SQL = {
33
33
  };
34
34
  export const AGENT_TOOL_APPROVAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding
35
35
  ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)`;
36
+ export const AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_logical
37
+ ON agent_tool_approvals(owner_email, org_id, thread_id, turn_id, tool_name, approval_key_hash, status)`;
38
+ export const AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_recovery
39
+ ON agent_tool_approvals(owner_email, org_id, thread_id, approval_key_hash, status, turn_id)`;
36
40
  /**
37
41
  * Durable approval grants are created and consumed on request paths, but their
38
42
  * schema belongs to the release migration boundary in production.
@@ -48,4 +52,20 @@ ${AGENT_TOOL_APPROVAL_INDEX_SQL}`,
48
52
  ${AGENT_TOOL_APPROVAL_INDEX_SQL}`,
49
53
  },
50
54
  },
55
+ {
56
+ version: 2,
57
+ name: "agent-tool-approvals-logical-binding-index",
58
+ sql: {
59
+ postgres: AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL,
60
+ sqlite: AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL,
61
+ },
62
+ },
63
+ {
64
+ version: 3,
65
+ name: "agent-tool-approvals-recovery-index",
66
+ sql: {
67
+ postgres: AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL,
68
+ sqlite: AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL,
69
+ },
70
+ },
51
71
  ];
@@ -9,10 +9,24 @@ export interface AgentToolApprovalBinding {
9
9
  expiresAt?: number;
10
10
  }
11
11
  export declare function hashAgentToolApprovalKey(approvalKey: string): string;
12
+ /**
13
+ * Recover the durable scope for an approval continuation when a transport
14
+ * drops the original turn id. A single pending logical turn is safe to
15
+ * recover; multiple turns are deliberately ambiguous and stay unmatched.
16
+ */
17
+ export declare function resolveAgentToolApprovalTurnId(binding: {
18
+ ownerEmail: string;
19
+ orgId?: string | null;
20
+ threadId?: string | null;
21
+ requestedTurnId?: string | null;
22
+ approvalKeys: readonly string[];
23
+ }): Promise<string | null>;
12
24
  export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<void>;
13
25
  /**
14
- * Atomically consume the server-created approval for one exact tool call.
15
- * Client history and approval keys are only lookup input; the pending row is
16
- * the authorization boundary and cannot be manufactured by the client.
26
+ * Atomically consume the server-created approval for one logical tool call.
27
+ * The model's call id is transport metadata and can change when Dispatch
28
+ * reconstructs a paused turn, so it is deliberately not part of authorization.
29
+ * The pending row remains the boundary: client history and approval keys alone
30
+ * cannot manufacture a grant.
17
31
  */
18
32
  export declare function consumeAgentToolApproval(binding: AgentToolApprovalBinding): Promise<boolean>;