@agent-native/core 0.160.1 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/bigquery.ts +57 -0
- package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
- package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
- package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
- package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +7 -3
- package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
- package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +1 -1
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
- package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
- package/corpus/templates/content/AGENTS.md +2 -0
- package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
- package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
- package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
- package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
- package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
- package/corpus/templates/content/parity/matrix.md +30 -30
- package/corpus/templates/content/parity/matrix.ts +4 -1
- package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
- package/corpus/templates/content/shared/database-block-actions.ts +82 -0
- package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
- package/corpus/templates/content/shared/nfm.ts +3 -2
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
- package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
- package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
- package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
- package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
- package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
- package/corpus/templates/mail/shared/signature.ts +1 -10
- package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
- package/corpus/templates/slides/actions/patch-deck.ts +1 -1
- package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
- package/corpus/templates/slides/app/global.css +1 -0
- package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
- package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
- package/corpus/templates/slides/shared/deck-title.ts +12 -4
- package/dist/agent/production-agent.js +17 -4
- package/dist/agent/tool-approval-migrations.d.ts +1 -0
- package/dist/agent/tool-approval-migrations.js +10 -0
- package/dist/agent/tool-approval-store.d.ts +12 -0
- package/dist/agent/tool-approval-store.js +46 -1
- package/dist/agent/types.d.ts +3 -1
- package/dist/client/AssistantChat.js +3 -1
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/resources/read.js +4 -4
- package/dist/scripts/resources/save-memory.js +15 -2
- package/dist/scripts/resources/write.js +3 -3
- package/dist/server/agent-chat/script-entries.d.ts +1 -0
- package/dist/server/agent-chat/script-entries.js +8 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/onboarding.mdx +13 -2
- 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(
|
|
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(
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|
|
@@ -107,14 +107,22 @@ export function deriveDeckTitleFromSlideContent(
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const
|
|
111
|
-
/<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]
|
|
112
|
-
for (const match of content.matchAll(
|
|
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
|
-
|
|
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";
|
|
@@ -6976,12 +6976,25 @@ export function createProductionAgentHandler(options) {
|
|
|
6976
6976
|
const isChainedBackgroundContinuation = isBackgroundWorker && backgroundContinuationCount > 0;
|
|
6977
6977
|
const runId = backgroundRunMarker?.runId ?? generateRunId();
|
|
6978
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;
|
|
6979
6991
|
const effectiveTurnId = typeof backgroundRunMarker?.turnId === "string" &&
|
|
6980
6992
|
backgroundRunMarker.turnId.trim()
|
|
6981
6993
|
? backgroundRunMarker.turnId.trim()
|
|
6982
|
-
:
|
|
6983
|
-
|
|
6984
|
-
|
|
6994
|
+
: (resolvedApprovalTurnId ??
|
|
6995
|
+
(typeof requestTurnId === "string" && requestTurnId.trim()
|
|
6996
|
+
? requestTurnId.trim()
|
|
6997
|
+
: runId));
|
|
6985
6998
|
const approvalStoreBinding = (binding) => {
|
|
6986
6999
|
if (!ownerEmail) {
|
|
6987
7000
|
throw new Error("Cannot create or consume an approval without an authenticated owner");
|
|
@@ -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.
|
|
@@ -35,6 +35,8 @@ export const AGENT_TOOL_APPROVAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_age
|
|
|
35
35
|
ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)`;
|
|
36
36
|
export const AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_logical
|
|
37
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)`;
|
|
38
40
|
/**
|
|
39
41
|
* Durable approval grants are created and consumed on request paths, but their
|
|
40
42
|
* schema belongs to the release migration boundary in production.
|
|
@@ -58,4 +60,12 @@ ${AGENT_TOOL_APPROVAL_INDEX_SQL}`,
|
|
|
58
60
|
sqlite: AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL,
|
|
59
61
|
},
|
|
60
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
|
+
},
|
|
61
71
|
];
|
|
@@ -9,6 +9,18 @@ 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
26
|
* Atomically consume the server-created approval for one logical tool call.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { getDbExec, isPostgres, retryOnDdlRace } from "../db/client.js";
|
|
3
3
|
import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
4
|
-
import { AGENT_TOOL_APPROVAL_INDEX_SQL, AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL, AGENT_TOOL_APPROVAL_TABLE_SQL, } from "./tool-approval-migrations.js";
|
|
4
|
+
import { AGENT_TOOL_APPROVAL_INDEX_SQL, AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL, AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL, AGENT_TOOL_APPROVAL_TABLE_SQL, } from "./tool-approval-migrations.js";
|
|
5
5
|
const APPROVAL_TTL_MS = 15 * 60_000;
|
|
6
6
|
const APPROVAL_CLEANUP_AGE_MS = 24 * 60 * 60_000;
|
|
7
7
|
let initPromise;
|
|
@@ -13,12 +13,14 @@ async function ensureAgentToolApprovalTable() {
|
|
|
13
13
|
await ensureTableExists("agent_tool_approvals", createSql);
|
|
14
14
|
await ensureIndexExists("idx_agent_tool_approvals_binding", AGENT_TOOL_APPROVAL_INDEX_SQL);
|
|
15
15
|
await ensureIndexExists("idx_agent_tool_approvals_logical", AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL);
|
|
16
|
+
await ensureIndexExists("idx_agent_tool_approvals_recovery", AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL);
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
19
|
const client = getDbExec();
|
|
19
20
|
await retryOnDdlRace(() => client.execute(createSql));
|
|
20
21
|
await retryOnDdlRace(() => client.execute(AGENT_TOOL_APPROVAL_INDEX_SQL));
|
|
21
22
|
await retryOnDdlRace(() => client.execute(AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL));
|
|
23
|
+
await retryOnDdlRace(() => client.execute(AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL));
|
|
22
24
|
})().catch((error) => {
|
|
23
25
|
initPromise = undefined;
|
|
24
26
|
throw error;
|
|
@@ -29,6 +31,49 @@ async function ensureAgentToolApprovalTable() {
|
|
|
29
31
|
export function hashAgentToolApprovalKey(approvalKey) {
|
|
30
32
|
return createHash("sha256").update(approvalKey).digest("hex");
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Recover the durable scope for an approval continuation when a transport
|
|
36
|
+
* drops the original turn id. A single pending logical turn is safe to
|
|
37
|
+
* recover; multiple turns are deliberately ambiguous and stay unmatched.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveAgentToolApprovalTurnId(binding) {
|
|
40
|
+
const approvalKeys = [...new Set(binding.approvalKeys)].slice(0, 200);
|
|
41
|
+
if (!binding.threadId || approvalKeys.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
await ensureAgentToolApprovalTable();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const hashes = approvalKeys.map(hashAgentToolApprovalKey);
|
|
46
|
+
const placeholders = hashes.map(() => "?").join(", ");
|
|
47
|
+
const result = await getDbExec().execute({
|
|
48
|
+
sql: `SELECT turn_id FROM agent_tool_approvals
|
|
49
|
+
WHERE owner_email = ?
|
|
50
|
+
AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
|
|
51
|
+
AND ((thread_id IS NULL AND CAST(? AS TEXT) IS NULL) OR thread_id = ?)
|
|
52
|
+
AND approval_key_hash IN (${placeholders})
|
|
53
|
+
AND status = 'pending'
|
|
54
|
+
AND expires_at > ?
|
|
55
|
+
AND turn_id IS NOT NULL`,
|
|
56
|
+
args: [
|
|
57
|
+
binding.ownerEmail,
|
|
58
|
+
binding.orgId ?? null,
|
|
59
|
+
binding.orgId ?? null,
|
|
60
|
+
binding.threadId,
|
|
61
|
+
binding.threadId,
|
|
62
|
+
...hashes,
|
|
63
|
+
now,
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
const turnIds = new Set((result.rows ?? [])
|
|
67
|
+
.map((row) => {
|
|
68
|
+
const value = row.turn_id;
|
|
69
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
70
|
+
})
|
|
71
|
+
.filter((turnId) => turnId !== null));
|
|
72
|
+
const requestedTurnId = binding.requestedTurnId?.trim();
|
|
73
|
+
if (requestedTurnId && turnIds.has(requestedTurnId))
|
|
74
|
+
return requestedTurnId;
|
|
75
|
+
return turnIds.size === 1 ? [...turnIds][0] : null;
|
|
76
|
+
}
|
|
32
77
|
export async function createAgentToolApproval(binding) {
|
|
33
78
|
await ensureAgentToolApprovalTable();
|
|
34
79
|
const now = Date.now();
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -232,7 +232,9 @@ export interface AgentChatRequest {
|
|
|
232
232
|
* `approval_required`; the client re-issues the turn (typically an empty
|
|
233
233
|
* continuation) with the approved call's key here so the gate lets it run.
|
|
234
234
|
* Keys not present here keep the action paused. The model never sees or sets
|
|
235
|
-
* this — it is supplied by the human's approve affordance.
|
|
235
|
+
* this — it is supplied by the human's approve affordance. Clients should
|
|
236
|
+
* preserve the original turnId; the server can recover one uniquely pending
|
|
237
|
+
* durable grant when a transport drops it, but refuses ambiguous matches.
|
|
236
238
|
*/
|
|
237
239
|
approvedToolCalls?: string[];
|
|
238
240
|
}
|