@agent-native/core 0.148.1 → 0.149.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
- package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
- package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +6 -1
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
- package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
- package/corpus/templates/content/actions/_property-utils.ts +11 -1
- package/corpus/templates/content/actions/view-screen.ts +62 -12
- package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +3 -0
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
- package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
- package/corpus/templates/design/actions/get-design-template.ts +178 -0
- package/corpus/templates/design/actions/list-designs.ts +3 -1
- package/corpus/templates/design/actions/view-screen.ts +31 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +78 -0
- package/corpus/templates/design/app/lib/design-authors.ts +64 -0
- package/corpus/templates/design/app/pages/Index.tsx +114 -4
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
- package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
- package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/AGENTS.md +2 -2
- package/corpus/templates/forms/actions/create-form.ts +1 -1
- package/corpus/templates/forms/actions/update-form.ts +5 -5
- package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/slides/AGENTS.md +4 -3
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
- package/corpus/templates/slides/actions/patch-deck.ts +18 -2
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
- package/corpus/templates/slides/app/pages/Index.tsx +124 -135
- package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/dist/agent/engine/registry.d.ts +8 -2
- package/dist/agent/engine/registry.js +63 -29
- package/dist/agent/production-agent.js +7 -0
- package/dist/agent/run-manager.d.ts +5 -4
- package/dist/agent/run-manager.js +6 -5
- package/dist/cli/claude-code-participant.d.ts +2 -1
- package/dist/cli/claude-code-participant.js +19 -0
- package/dist/cli/code-agent-executor.js +223 -0
- package/dist/cli/create-workspace.js +3 -3
- package/dist/cli/create.d.ts +3 -2
- package/dist/cli/create.js +20 -24
- package/dist/client/AgentPanel.js +8 -4
- package/dist/client/AssistantChat.js +34 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -0
- package/dist/client/agent-chat-adapter.js +10 -2
- package/dist/client/chat/markdown-renderer.d.ts +1 -0
- package/dist/client/chat/markdown-renderer.js +4 -0
- package/dist/client/chat/message-components.d.ts +1 -0
- package/dist/client/chat/message-components.js +9 -1
- package/dist/client/chat/tool-call-display.d.ts +8 -0
- package/dist/client/chat/tool-call-display.js +30 -21
- package/dist/client/chat-first/app-pane.js +1 -1
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/chat-history.d.ts +1 -1
- package/dist/client/chat-first/chat-history.js +2 -0
- package/dist/client/chat-first/primary-nav.js +1 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +2 -2
- package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
- package/dist/client/settings/SettingsTabsPage.js +2 -2
- package/dist/client/sse-event-processor.d.ts +8 -0
- package/dist/client/sse-event-processor.js +13 -6
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +5 -7
- package/dist/deploy/build.js +19 -12
- package/dist/localization/default-messages.js +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/scripts/call-agent.js +3 -3
- package/dist/scripts/describe-workspace-apps.js +1 -1
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/context-tools.js +1 -1
- package/dist/server/agent-chat/prompt-resources.js +1 -1
- package/dist/server/agent-chat-plugin.js +1 -1
- package/dist/server/auth.js +190 -42
- package/dist/server/better-auth-migrations.js +53 -0
- package/dist/server/http-response-telemetry.js +7 -1
- package/dist/server/onboarding-html.js +119 -33
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/password-policy.d.ts +2 -1
- package/dist/shared/password-policy.js +2 -1
- package/dist/styles/agent-conversation.css +20 -3
- package/dist/styles/agent-native.css +14 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
- package/dist/templates/default/app/i18n/de-DE.ts +1 -1
- package/dist/templates/default/app/i18n/en-US.ts +1 -1
- package/dist/templates/default/app/i18n/es-ES.ts +1 -1
- package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
- package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
- package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
- package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
- package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
- package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
- package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
- package/docs/content/cloneable-saas.mdx +6 -6
- package/docs/content/deployment.mdx +10 -8
- package/docs/content/environment-variables.mdx +4 -3
- package/package.json +2 -2
- package/src/templates/default/app/i18n/ar-SA.ts +1 -1
- package/src/templates/default/app/i18n/de-DE.ts +1 -1
- package/src/templates/default/app/i18n/en-US.ts +1 -1
- package/src/templates/default/app/i18n/es-ES.ts +1 -1
- package/src/templates/default/app/i18n/fr-FR.ts +1 -1
- package/src/templates/default/app/i18n/hi-IN.ts +1 -1
- package/src/templates/default/app/i18n/ja-JP.ts +1 -1
- package/src/templates/default/app/i18n/ko-KR.ts +1 -1
- package/src/templates/default/app/i18n/pt-BR.ts +1 -1
- package/src/templates/default/app/i18n/zh-CN.ts +1 -1
- package/src/templates/default/app/i18n/zh-TW.ts +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
import actionsRegistry from "../../.generated/actions-registry.js";
|
|
8
8
|
|
|
9
|
-
const FORMS_SYSTEM_PROMPT = `## Agent-Native Forms
|
|
9
|
+
export const FORMS_SYSTEM_PROMPT = `## Agent-Native Forms
|
|
10
10
|
|
|
11
11
|
You are the in-app agent for Agent-Native Forms, a chat-first form builder and response workspace. Help users create, edit, publish, share, and analyze forms through the registered actions. The actions are the source of truth for database access, validation, permissions, UI sync, and business logic.
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ Core rules:
|
|
|
15
15
|
- Never hardcode API keys, tokens, webhook URLs, signing secrets, customer data, or credential-looking literals. Use placeholders or configured integrations.
|
|
16
16
|
- Inspect the current state when it matters. Use \`view-screen\` when the active form, selected field, publish state, response table, or current route is unclear.
|
|
17
17
|
- Use \`create-form\`, \`update-form\`, \`patch-form-fields\`, \`restore-form\`, and \`delete-form\` for form lifecycle and field edits. Treat \`delete-form\` as Archive by default; pass \`purge: true\` only when the user explicitly asks to permanently delete a form and its responses.
|
|
18
|
-
- When the
|
|
18
|
+
- When a form is published, copy the returned \`publicUrl\` verbatim into the response. Never derive a public link from \`slug\` or remove the \`/f/\` path segment; if \`publicUrl\` is missing, say that the public link could not be retrieved instead of inventing one. For an anonymous feedback form or survey, create the complete form atomically with \`status: "published"\` and verify the saved status and fields before claiming it is live.
|
|
19
19
|
- Use \`preview-form\` when the user asks about a form's setup, fields, configuration, publish state, or response count. It renders a native chat summary.
|
|
20
20
|
- When the user wants email notifications for new responses, set \`settings.emailOnNewResponses: true\` with \`create-form\` or \`update-form\`; notifications go to the form owner's account email.
|
|
21
21
|
- When the user wants Slack, Discord, Google Sheets, or webhook notifications, explain that Forms already supports them from the form builder's Integrations tab and navigate there with \`tab: "integrations"\`. Slack uses an Incoming Webhook URL; Google Sheets uses a deployed Apps Script \`/exec\` URL that parses \`e.postData.contents\`. Do not redirect users to the managed Messaging integration for these form destinations.
|
|
@@ -42,9 +42,10 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
|
|
|
42
42
|
Drive API needs, use `provider-api-catalog`, `provider-api-docs`, and
|
|
43
43
|
`provider-api-request`; auth comes from the user's Google Docs OAuth. Stage
|
|
44
44
|
large scans with `stageAs` and analyze them via `query-staged-dataset`.
|
|
45
|
-
- `import-google-slides-reference` accepts
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
- `import-google-slides-reference` accepts a Picker `fileId` or `presentationUrl`;
|
|
46
|
+
pasted URLs may need a one-time Google reconnect for Drive export. Preserve
|
|
47
|
+
imported PPTX timing metadata, including by-paragraph reveals, on slide
|
|
48
|
+
records.
|
|
48
49
|
- For images, use `generate-image-api` with provenance; show results as
|
|
49
50
|
``.
|
|
50
51
|
- Ask a sibling app's agent with a natural-language `call-agent` message by
|
|
@@ -64,9 +64,7 @@ async function exportGoogleSlidesAsPptx(fileId: string, accessToken: string) {
|
|
|
64
64
|
);
|
|
65
65
|
|
|
66
66
|
if (!response.ok) {
|
|
67
|
-
throw
|
|
68
|
-
`Google Drive returned HTTP ${response.status} while exporting the presentation.`,
|
|
69
|
-
);
|
|
67
|
+
throw googleSlidesExportError(response.status);
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
const bytes = await response.arrayBuffer();
|
|
@@ -80,14 +78,15 @@ async function exportGoogleSlidesAsPptx(fileId: string, accessToken: string) {
|
|
|
80
78
|
interface GoogleSlidesImageElement {
|
|
81
79
|
objectId?: string;
|
|
82
80
|
size?: {
|
|
83
|
-
width?: { magnitude?: number };
|
|
84
|
-
height?: { magnitude?: number };
|
|
81
|
+
width?: { magnitude?: number; unit?: string };
|
|
82
|
+
height?: { magnitude?: number; unit?: string };
|
|
85
83
|
};
|
|
86
84
|
transform?: {
|
|
87
85
|
scaleX?: number;
|
|
88
86
|
scaleY?: number;
|
|
89
87
|
translateX?: number;
|
|
90
88
|
translateY?: number;
|
|
89
|
+
unit?: string;
|
|
91
90
|
};
|
|
92
91
|
image?: {
|
|
93
92
|
contentUrl?: string;
|
|
@@ -124,6 +123,41 @@ function finiteNumber(value: number | undefined): number | undefined {
|
|
|
124
123
|
return value != null && Number.isFinite(value) ? value : undefined;
|
|
125
124
|
}
|
|
126
125
|
|
|
126
|
+
const EMU_PER_POINT = 12700;
|
|
127
|
+
|
|
128
|
+
export function googleSlidesMeasurementToEmu(
|
|
129
|
+
value: number | undefined,
|
|
130
|
+
unit?: string,
|
|
131
|
+
): number | undefined {
|
|
132
|
+
const finite = finiteNumber(value);
|
|
133
|
+
if (finite == null) return undefined;
|
|
134
|
+
return unit === "PT" ? finite * EMU_PER_POINT : finite;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function googleSlidesExportError(
|
|
138
|
+
status: number,
|
|
139
|
+
): Error & { statusCode?: number } {
|
|
140
|
+
if (status === 401) {
|
|
141
|
+
return Object.assign(
|
|
142
|
+
new Error(
|
|
143
|
+
"Google Drive connection expired. Connect Google again, then retry the import.",
|
|
144
|
+
),
|
|
145
|
+
{ statusCode: 401 },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (status === 403) {
|
|
149
|
+
return Object.assign(
|
|
150
|
+
new Error(
|
|
151
|
+
"Google Drive denied access to this presentation. Connect Google again to enable pasted-link imports, or choose the deck through Google Picker.",
|
|
152
|
+
),
|
|
153
|
+
{ statusCode: 403 },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return new Error(
|
|
157
|
+
`Google Drive returned HTTP ${status} while exporting the presentation.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
/**
|
|
128
162
|
* Google Drive's PPTX export can omit image page elements that remain present
|
|
129
163
|
* in the native Slides document. Read those native objects as a fidelity
|
|
@@ -135,7 +169,7 @@ async function fetchGoogleSlidesImageFallbacks(
|
|
|
135
169
|
presentation: ParsedPresentation,
|
|
136
170
|
): Promise<ImportedImageFallback[]> {
|
|
137
171
|
const fields =
|
|
138
|
-
"slides(pageElements(objectId,size,transform,image(contentUrl,imageProperties(cropProperties))))";
|
|
172
|
+
"slides(pageElements(objectId,size(width(magnitude,unit),height(magnitude,unit)),transform(scaleX,scaleY,translateX,translateY,unit),image(contentUrl,imageProperties(cropProperties))))";
|
|
139
173
|
const response = await ssrfSafeFetch(
|
|
140
174
|
`https://slides.googleapis.com/v1/presentations/${encodeURIComponent(fileId)}?fields=${encodeURIComponent(fields)}`,
|
|
141
175
|
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
|
@@ -156,12 +190,24 @@ async function fetchGoogleSlidesImageFallbacks(
|
|
|
156
190
|
).entries()) {
|
|
157
191
|
for (const [imageIndex, element] of (slide.pageElements ?? []).entries()) {
|
|
158
192
|
const contentUrl = element.image?.contentUrl;
|
|
159
|
-
const baseWidth =
|
|
160
|
-
|
|
193
|
+
const baseWidth = googleSlidesMeasurementToEmu(
|
|
194
|
+
element.size?.width?.magnitude,
|
|
195
|
+
element.size?.width?.unit,
|
|
196
|
+
);
|
|
197
|
+
const baseHeight = googleSlidesMeasurementToEmu(
|
|
198
|
+
element.size?.height?.magnitude,
|
|
199
|
+
element.size?.height?.unit,
|
|
200
|
+
);
|
|
161
201
|
const scaleX = finiteNumber(element.transform?.scaleX) ?? 1;
|
|
162
202
|
const scaleY = finiteNumber(element.transform?.scaleY) ?? 1;
|
|
163
|
-
const x =
|
|
164
|
-
|
|
203
|
+
const x = googleSlidesMeasurementToEmu(
|
|
204
|
+
element.transform?.translateX,
|
|
205
|
+
element.transform?.unit,
|
|
206
|
+
);
|
|
207
|
+
const y = googleSlidesMeasurementToEmu(
|
|
208
|
+
element.transform?.translateY,
|
|
209
|
+
element.transform?.unit,
|
|
210
|
+
);
|
|
165
211
|
const width =
|
|
166
212
|
baseWidth == null ? undefined : baseWidth * Math.abs(scaleX);
|
|
167
213
|
const height =
|
|
@@ -370,6 +370,18 @@ export function resolveDeckColumnUpdates(
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* The source-preservation guards (`assertSourceImportOperationsPreserved`,
|
|
375
|
+
* `assertSourceSlidePreserved`) exist for one failure mode: an agent asked to
|
|
376
|
+
* "make it prettier" silently dropping the original PDF/PPTX artwork or
|
|
377
|
+
* factual copy. A human editing their own imported deck in the browser isn't
|
|
378
|
+
* that failure mode, and the browser editor has no way to pass
|
|
379
|
+
* `preserveSource` — so these guards must only run for agent callers.
|
|
380
|
+
*/
|
|
381
|
+
export function isAgentPatchCaller(caller: string | undefined): boolean {
|
|
382
|
+
return caller === "tool" || caller === "mcp" || caller === "a2a";
|
|
383
|
+
}
|
|
384
|
+
|
|
373
385
|
// ---------------------------------------------------------------------------
|
|
374
386
|
// Action definition
|
|
375
387
|
// ---------------------------------------------------------------------------
|
|
@@ -402,8 +414,9 @@ export default defineAction({
|
|
|
402
414
|
),
|
|
403
415
|
}),
|
|
404
416
|
agentInputSchema: AgentPatchDeckInputSchema,
|
|
405
|
-
run: async ({ deckId, operations, creativeContext }) => {
|
|
417
|
+
run: async ({ deckId, operations, creativeContext }, ctx) => {
|
|
406
418
|
await assertAccess("deck", deckId, "editor");
|
|
419
|
+
const isAgentCaller = isAgentPatchCaller(ctx?.caller);
|
|
407
420
|
|
|
408
421
|
return withDeckLock(deckId, async () => {
|
|
409
422
|
const db = getDb();
|
|
@@ -435,9 +448,12 @@ export default defineAction({
|
|
|
435
448
|
}
|
|
436
449
|
|
|
437
450
|
const sourceImport = sourceImportForDeck(deck.sourceImport);
|
|
438
|
-
|
|
451
|
+
if (isAgentCaller) {
|
|
452
|
+
assertSourceImportOperationsPreserved(sourceImport, operations);
|
|
453
|
+
}
|
|
439
454
|
for (const op of operations) {
|
|
440
455
|
if (
|
|
456
|
+
!isAgentCaller ||
|
|
441
457
|
op.op !== "patch-slide" ||
|
|
442
458
|
(op.fields.content === undefined && op.fields.notes === undefined)
|
|
443
459
|
) {
|
|
@@ -385,6 +385,29 @@ function useSlideAutofit(
|
|
|
385
385
|
|
|
386
386
|
let raf = 0;
|
|
387
387
|
let disposed = false;
|
|
388
|
+
// Measuring costs a full-document reflow per slide (every descendant is
|
|
389
|
+
// read with getBoundingClientRect, interleaved with style writes). A deck
|
|
390
|
+
// with dozens of slides mounts that many renderers at once, so off-screen
|
|
391
|
+
// thumbnails are left unmeasured until they scroll into view. Without an
|
|
392
|
+
// IntersectionObserver there is nothing to defer against, so measure
|
|
393
|
+
// eagerly as before.
|
|
394
|
+
const canDefer = typeof IntersectionObserver !== "undefined";
|
|
395
|
+
// Resolved synchronously rather than waiting for the observer's first
|
|
396
|
+
// callback: a slide that is already on screen must be measured on this
|
|
397
|
+
// pass, and nothing may depend on a callback that a given environment
|
|
398
|
+
// might never deliver. This reads one rect, not one per descendant.
|
|
399
|
+
const isNearViewport = () => {
|
|
400
|
+
const rect = root.getBoundingClientRect();
|
|
401
|
+
const margin = 200;
|
|
402
|
+
return (
|
|
403
|
+
rect.bottom >= -margin &&
|
|
404
|
+
rect.right >= -margin &&
|
|
405
|
+
rect.top <= (window.innerHeight || 0) + margin &&
|
|
406
|
+
rect.left <= (window.innerWidth || 0) + margin
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
let visible = !canDefer || isNearViewport();
|
|
410
|
+
let measurePending = false;
|
|
388
411
|
|
|
389
412
|
const resetTarget = (target: HTMLElement) => {
|
|
390
413
|
target.style.setProperty("--fmd-fit-scale", "1");
|
|
@@ -482,6 +505,10 @@ function useSlideAutofit(
|
|
|
482
505
|
|
|
483
506
|
const scheduleMeasure = () => {
|
|
484
507
|
if (disposed) return;
|
|
508
|
+
if (!visible) {
|
|
509
|
+
measurePending = true;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
485
512
|
cancelAnimationFrame(raf);
|
|
486
513
|
raf = requestAnimationFrame(measureNow);
|
|
487
514
|
};
|
|
@@ -502,11 +529,30 @@ function useSlideAutofit(
|
|
|
502
529
|
root.addEventListener("load", scheduleMeasure, true);
|
|
503
530
|
document.fonts?.ready.then(scheduleMeasure).catch(() => {});
|
|
504
531
|
|
|
532
|
+
// `rootMargin` measures a thumbnail just before it scrolls in, so the fit
|
|
533
|
+
// transform is already applied by the time it is on screen.
|
|
534
|
+
const visibilityObserver = canDefer
|
|
535
|
+
? new IntersectionObserver(
|
|
536
|
+
(entries) => {
|
|
537
|
+
const isVisible = entries.some((entry) => entry.isIntersecting);
|
|
538
|
+
if (isVisible === visible) return;
|
|
539
|
+
visible = isVisible;
|
|
540
|
+
if (visible && measurePending) {
|
|
541
|
+
measurePending = false;
|
|
542
|
+
scheduleMeasure();
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
{ rootMargin: "200px" },
|
|
546
|
+
)
|
|
547
|
+
: null;
|
|
548
|
+
visibilityObserver?.observe(root);
|
|
549
|
+
|
|
505
550
|
return () => {
|
|
506
551
|
disposed = true;
|
|
507
552
|
cancelAnimationFrame(raf);
|
|
508
553
|
resizeObserver.disconnect();
|
|
509
554
|
mutationObserver.disconnect();
|
|
555
|
+
visibilityObserver?.disconnect();
|
|
510
556
|
root.removeEventListener("load", scheduleMeasure, true);
|
|
511
557
|
};
|
|
512
558
|
}, [canvasWidth, canvasHeight, fitKey, ref]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sendToAgentChatAndConfirm } from "@agent-native/core/client/agent-chat";
|
|
1
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
3
|
import {
|
|
3
4
|
IconBold,
|
|
@@ -6,11 +7,15 @@ import {
|
|
|
6
7
|
IconStrikethrough,
|
|
7
8
|
IconLink,
|
|
8
9
|
IconPalette,
|
|
10
|
+
IconPencilStar,
|
|
9
11
|
IconCheck,
|
|
10
12
|
IconX,
|
|
13
|
+
IconArrowUp,
|
|
14
|
+
IconLoader2,
|
|
11
15
|
} from "@tabler/icons-react";
|
|
12
16
|
import { useEffect, useState, useRef } from "react";
|
|
13
17
|
import { createPortal } from "react-dom";
|
|
18
|
+
import { toast } from "sonner";
|
|
14
19
|
|
|
15
20
|
import {
|
|
16
21
|
Tooltip,
|
|
@@ -22,6 +27,17 @@ import { shortcutLabel } from "@/lib/utils";
|
|
|
22
27
|
interface BlockBubbleMenuProps {
|
|
23
28
|
/** The element currently in contentEditable mode. Menu only shows while selection is inside it. */
|
|
24
29
|
editingEl: HTMLElement | null;
|
|
30
|
+
/** Slide the edited block belongs to, so an AI revision can target it. */
|
|
31
|
+
slideId?: string;
|
|
32
|
+
/** Deck that owns the slide — pins the revision to the right deck. */
|
|
33
|
+
deckId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Ends the inline edit session and persists whatever is in the DOM now.
|
|
36
|
+
* Required before handing work to the agent: otherwise the still-open
|
|
37
|
+
* contentEditable serializes its stale text on the user's next click and
|
|
38
|
+
* overwrites the revision the agent just wrote.
|
|
39
|
+
*/
|
|
40
|
+
onCommitInlineEdit?: () => void;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
interface Position {
|
|
@@ -44,32 +60,89 @@ const COLORS = [
|
|
|
44
60
|
"#EF4444",
|
|
45
61
|
];
|
|
46
62
|
|
|
63
|
+
/** Shown above the input so the user can confirm what the agent will rewrite. */
|
|
64
|
+
const AI_TARGET_PREVIEW_LIMIT = 160;
|
|
65
|
+
|
|
66
|
+
const AI_SEND_BUTTON_CLASS =
|
|
67
|
+
// guard:allow-raw-color — same accent as the link Apply button below.
|
|
68
|
+
"rounded p-1.5 text-[#609FF8] hover:bg-accent disabled:pointer-events-none disabled:opacity-40";
|
|
69
|
+
|
|
70
|
+
export function buildReviseSelectionPrompt({
|
|
71
|
+
selectedText,
|
|
72
|
+
instruction,
|
|
73
|
+
slideId,
|
|
74
|
+
deckId,
|
|
75
|
+
}: {
|
|
76
|
+
selectedText: string;
|
|
77
|
+
instruction: string;
|
|
78
|
+
slideId?: string;
|
|
79
|
+
deckId?: string;
|
|
80
|
+
}): string {
|
|
81
|
+
// The deck is named explicitly rather than left to "the current slide": the
|
|
82
|
+
// request is queued, and if the user opens another deck before the agent
|
|
83
|
+
// runs, an implicit target would pair this slide id with the wrong deck.
|
|
84
|
+
const target = [
|
|
85
|
+
deckId ? `Deck id: \`${deckId}\`` : null,
|
|
86
|
+
slideId ? `Slide id: \`${slideId}\`` : null,
|
|
87
|
+
].filter((line) => line !== null);
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
`Revise this exact text:`,
|
|
91
|
+
``,
|
|
92
|
+
`"""`,
|
|
93
|
+
selectedText,
|
|
94
|
+
`"""`,
|
|
95
|
+
``,
|
|
96
|
+
`How to revise it: ${instruction}`,
|
|
97
|
+
...(target.length > 0 ? [``, ...target] : []),
|
|
98
|
+
``,
|
|
99
|
+
`Read that slide first with \`view-screen\`, then make one bounded \`update-slide --fullContent\` edit that replaces only the quoted text. Leave the surrounding HTML, inline styles, and layout untouched, and keep the replacement close to the original length so the slide still fits its canvas.`,
|
|
100
|
+
]
|
|
101
|
+
.filter((line) => line !== null)
|
|
102
|
+
.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
47
105
|
/**
|
|
48
106
|
* Floating formatting toolbar for contentEditable text blocks. Shows on
|
|
49
107
|
* non-empty selection inside the editing element and applies inline
|
|
50
108
|
* formatting (bold, italic, underline, strike, link, color) directly to
|
|
51
109
|
* the DOM. Designed to work with the in-place per-block editing in
|
|
52
110
|
* SlideEditor — it never mutates anything outside the editing element.
|
|
111
|
+
*
|
|
112
|
+
* The "Revise with AI" action is the exception: it does not touch the DOM.
|
|
113
|
+
* It hands the selected text plus the user's instruction to the agent, which
|
|
114
|
+
* rewrites the slide through `update-slide`.
|
|
53
115
|
*/
|
|
54
|
-
export function BlockBubbleMenu({
|
|
116
|
+
export function BlockBubbleMenu({
|
|
117
|
+
editingEl,
|
|
118
|
+
slideId,
|
|
119
|
+
deckId,
|
|
120
|
+
onCommitInlineEdit,
|
|
121
|
+
}: BlockBubbleMenuProps) {
|
|
55
122
|
const t = useT();
|
|
56
123
|
const [pos, setPos] = useState<Position | null>(null);
|
|
57
124
|
const [showColors, setShowColors] = useState(false);
|
|
58
125
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
59
126
|
const [linkValue, setLinkValue] = useState("");
|
|
127
|
+
const [showAiInput, setShowAiInput] = useState(false);
|
|
128
|
+
const [aiInstruction, setAiInstruction] = useState("");
|
|
129
|
+
const [aiTargetText, setAiTargetText] = useState("");
|
|
130
|
+
const [aiSending, setAiSending] = useState(false);
|
|
60
131
|
const savedRangeRef = useRef<Range | null>(null);
|
|
61
132
|
// True while a popup/input has the user's focus — keeps the menu pinned
|
|
62
133
|
// even when the contentEditable selection collapses behind the scenes.
|
|
63
134
|
const interactingRef = useRef(false);
|
|
64
135
|
useEffect(() => {
|
|
65
|
-
interactingRef.current = showColors || showLinkInput;
|
|
66
|
-
}, [showColors, showLinkInput]);
|
|
136
|
+
interactingRef.current = showColors || showLinkInput || showAiInput;
|
|
137
|
+
}, [showColors, showLinkInput, showAiInput]);
|
|
67
138
|
|
|
68
139
|
// Hide menu when the editing element changes
|
|
69
140
|
useEffect(() => {
|
|
70
141
|
setPos(null);
|
|
71
142
|
setShowColors(false);
|
|
72
143
|
setShowLinkInput(false);
|
|
144
|
+
setShowAiInput(false);
|
|
145
|
+
setAiInstruction("");
|
|
73
146
|
}, [editingEl]);
|
|
74
147
|
|
|
75
148
|
// Track selection and position the menu
|
|
@@ -158,6 +231,61 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
|
|
|
158
231
|
setLinkValue("");
|
|
159
232
|
};
|
|
160
233
|
|
|
234
|
+
const openAiInput = () => {
|
|
235
|
+
if (showAiInput) {
|
|
236
|
+
setShowAiInput(false);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Snapshot the text now: opening the input moves focus out of the
|
|
240
|
+
// contentEditable and the live selection collapses.
|
|
241
|
+
const selected = savedRangeRef.current?.toString().trim() ?? "";
|
|
242
|
+
if (!selected) return;
|
|
243
|
+
setAiTargetText(selected);
|
|
244
|
+
setAiInstruction("");
|
|
245
|
+
interactingRef.current = true;
|
|
246
|
+
setShowAiInput(true);
|
|
247
|
+
setShowColors(false);
|
|
248
|
+
setShowLinkInput(false);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const submitAiRevision = async () => {
|
|
252
|
+
const instruction = aiInstruction.trim();
|
|
253
|
+
if (!instruction || !aiTargetText || aiSending) return;
|
|
254
|
+
|
|
255
|
+
// Close the inline edit first. The block is still a live contentEditable
|
|
256
|
+
// session; leaving it open means the next click away serializes the old
|
|
257
|
+
// text over whatever the agent writes.
|
|
258
|
+
onCommitInlineEdit?.();
|
|
259
|
+
|
|
260
|
+
setAiSending(true);
|
|
261
|
+
try {
|
|
262
|
+
const delivery = await sendToAgentChatAndConfirm({
|
|
263
|
+
message: buildReviseSelectionPrompt({
|
|
264
|
+
selectedText: aiTargetText,
|
|
265
|
+
instruction,
|
|
266
|
+
slideId,
|
|
267
|
+
deckId,
|
|
268
|
+
}),
|
|
269
|
+
submit: true,
|
|
270
|
+
chatTarget: "local",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!delivery.delivered) {
|
|
274
|
+
// Keep the typed instruction so the user can retry without retyping.
|
|
275
|
+
toast.error(t("raw.sendToAgent"), {
|
|
276
|
+
description: delivery.reason ?? "The agent did not receive this.",
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
toast.success(t("raw.sentToAgent"), { description: instruction });
|
|
282
|
+
setShowAiInput(false);
|
|
283
|
+
setAiInstruction("");
|
|
284
|
+
} finally {
|
|
285
|
+
setAiSending(false);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
161
289
|
return createPortal(
|
|
162
290
|
<div
|
|
163
291
|
data-block-bubble-menu="true"
|
|
@@ -168,6 +296,13 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
|
|
|
168
296
|
e.preventDefault();
|
|
169
297
|
}}
|
|
170
298
|
>
|
|
299
|
+
<ToolbarButton
|
|
300
|
+
icon={IconPencilStar}
|
|
301
|
+
tooltip="Revise with AI"
|
|
302
|
+
onClick={openAiInput}
|
|
303
|
+
active={showAiInput}
|
|
304
|
+
/>
|
|
305
|
+
<div className="w-px h-4 bg-border mx-0.5" />
|
|
171
306
|
<ToolbarButton
|
|
172
307
|
icon={IconBold}
|
|
173
308
|
tooltip={`Bold (${shortcutLabel("cmd+b")})`}
|
|
@@ -199,6 +334,7 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
|
|
|
199
334
|
if (!showColors) interactingRef.current = true;
|
|
200
335
|
setShowColors((v) => !v);
|
|
201
336
|
setShowLinkInput(false);
|
|
337
|
+
setShowAiInput(false);
|
|
202
338
|
}}
|
|
203
339
|
active={showColors}
|
|
204
340
|
/>
|
|
@@ -229,9 +365,64 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
|
|
|
229
365
|
if (!showLinkInput) interactingRef.current = true;
|
|
230
366
|
setShowLinkInput((v) => !v);
|
|
231
367
|
setShowColors(false);
|
|
368
|
+
setShowAiInput(false);
|
|
232
369
|
}}
|
|
233
370
|
active={showLinkInput}
|
|
234
371
|
/>
|
|
372
|
+
{showAiInput && (
|
|
373
|
+
<div
|
|
374
|
+
data-ai-revise-input="true"
|
|
375
|
+
// Own mousedown handler: the toolbar above blocks the default to keep
|
|
376
|
+
// the contentEditable focused, which would also stop this input from
|
|
377
|
+
// ever receiving a caret.
|
|
378
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
379
|
+
className="absolute top-full left-1/2 -translate-x-1/2 mt-1 w-80 p-2 rounded-lg bg-popover border border-border shadow-2xl shadow-black/60"
|
|
380
|
+
>
|
|
381
|
+
<p className="mb-1.5 line-clamp-2 text-[11px] leading-snug text-muted-foreground">
|
|
382
|
+
{aiTargetText.length > AI_TARGET_PREVIEW_LIMIT
|
|
383
|
+
? `${aiTargetText.slice(0, AI_TARGET_PREVIEW_LIMIT)}…`
|
|
384
|
+
: aiTargetText}
|
|
385
|
+
</p>
|
|
386
|
+
<div className="flex items-end gap-1">
|
|
387
|
+
<textarea
|
|
388
|
+
value={aiInstruction}
|
|
389
|
+
onChange={(e) => setAiInstruction(e.target.value)}
|
|
390
|
+
onKeyDown={(e) => {
|
|
391
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
void submitAiRevision();
|
|
394
|
+
} else if (e.key === "Escape") {
|
|
395
|
+
e.preventDefault();
|
|
396
|
+
setShowAiInput(false);
|
|
397
|
+
}
|
|
398
|
+
}}
|
|
399
|
+
placeholder={t("raw.tellAgentDo")}
|
|
400
|
+
rows={2}
|
|
401
|
+
disabled={aiSending}
|
|
402
|
+
className="flex-1 resize-none rounded border border-border bg-muted px-2 py-1.5 text-xs text-foreground outline-none focus:border-ring disabled:opacity-60"
|
|
403
|
+
autoFocus
|
|
404
|
+
/>
|
|
405
|
+
<Tooltip>
|
|
406
|
+
<TooltipTrigger asChild>
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
onClick={() => void submitAiRevision()}
|
|
410
|
+
disabled={!aiInstruction.trim() || aiSending}
|
|
411
|
+
aria-label={t("raw.sendToAgent")}
|
|
412
|
+
className={AI_SEND_BUTTON_CLASS}
|
|
413
|
+
>
|
|
414
|
+
{aiSending ? (
|
|
415
|
+
<IconLoader2 className="w-3.5 h-3.5 animate-spin" />
|
|
416
|
+
) : (
|
|
417
|
+
<IconArrowUp className="w-3.5 h-3.5" />
|
|
418
|
+
)}
|
|
419
|
+
</button>
|
|
420
|
+
</TooltipTrigger>
|
|
421
|
+
<TooltipContent>{t("raw.sendToAgent")}</TooltipContent>
|
|
422
|
+
</Tooltip>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
235
426
|
{showLinkInput && (
|
|
236
427
|
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 flex items-center gap-1 p-1 rounded-lg bg-popover border border-border shadow-2xl shadow-black/60">
|
|
237
428
|
<input
|