@agent-native/core 0.149.0 → 0.149.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
- package/corpus/templates/analytics/app/global.css +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +37 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/brain/app/global.css +1 -1
- package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +274 -137
- package/corpus/templates/clips/desktop/src/styles.css +35 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/crm/app/global.css +1 -1
- package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -0
- package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
- package/corpus/templates/slides/AGENTS.md +11 -10
- package/corpus/templates/slides/actions/add-deck.ts +2 -0
- package/corpus/templates/slides/actions/add-slide.ts +15 -1
- package/corpus/templates/slides/actions/create-deck.ts +18 -6
- package/corpus/templates/slides/actions/get-deck.ts +29 -1
- package/corpus/templates/slides/actions/patch-deck.ts +139 -10
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- 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 +10 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
- package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
- package/corpus/templates/slides/app/pages/Index.tsx +256 -98
- 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-ai-editing-avatar-badge.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -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/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/corpus/templates/slides/shared/api.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +135 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
- package/dist/agent/engine/ai-sdk-engine.js +1 -1
- package/dist/agent/production-agent.d.ts +21 -1
- package/dist/agent/production-agent.js +129 -10
- package/dist/agent/tool-approval-store.d.ts +18 -0
- package/dist/agent/tool-approval-store.js +122 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/cli/code-agent-executor.js +11 -2
- package/dist/client/AgentPanel.js +31 -6
- package/dist/client/AssistantChat.d.ts +3 -2
- package/dist/client/AssistantChat.js +7 -20
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +11 -0
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/chat/markdown-renderer.js +14 -2
- package/dist/client/chat/message-components.d.ts +6 -2
- package/dist/client/chat/message-components.js +25 -47
- package/dist/client/chat/tool-call-display.d.ts +3 -5
- package/dist/client/chat/tool-call-display.js +7 -212
- package/dist/client/chat-first/apps-rail.js +3 -3
- package/dist/client/chat-first/browser-pane.js +4 -1
- package/dist/client/chat-first/chat-history.d.ts +3 -1
- package/dist/client/chat-first/chat-history.js +2 -2
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +52 -41
- package/dist/client/chat-first/types.d.ts +3 -0
- package/dist/client/chat-first.d.ts +10 -0
- package/dist/client/chat-first.js +37 -2
- package/dist/client/components/ui/message-scroller.js +4 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +9 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/use-chat-models.js +156 -80
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/auth.js +262 -31
- package/dist/server/onboarding-html.js +56 -9
- package/dist/shared/reasoning-effort.d.ts +8 -8
- package/dist/shared/reasoning-effort.js +10 -10
- package/dist/shared/streaming-text-smoothing.js +41 -11
- package/dist/styles/agent-native.css +19 -32
- package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/docs/content/template-chat.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hostnames that never belong to a legitimate slide image but are classic
|
|
5
|
+
* SSRF targets. Cloud metadata endpoints resolve to a link-local address
|
|
6
|
+
* that `isPrivateAddress` already rejects; they are listed here so the
|
|
7
|
+
* request is refused before a DNS query is issued.
|
|
8
|
+
*/
|
|
9
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
10
|
+
"localhost",
|
|
11
|
+
"metadata.google.internal",
|
|
12
|
+
"metadata",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const BLOCKED_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];
|
|
16
|
+
|
|
17
|
+
/** Largest image we are willing to buffer and hand back to the browser. */
|
|
18
|
+
export const MAX_PROXIED_IMAGE_BYTES = 15 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
/** Redirect hops to follow. Each hop is re-validated before it is fetched. */
|
|
21
|
+
export const MAX_PROXY_REDIRECTS = 3;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether an IP literal points somewhere inside our own infrastructure.
|
|
25
|
+
* Anything unroutable, loopback, link-local, or RFC1918 is refused, as is
|
|
26
|
+
* any address we cannot parse — unknown means unsafe here.
|
|
27
|
+
*/
|
|
28
|
+
export function isPrivateAddress(address: string): boolean {
|
|
29
|
+
const version = net.isIP(address);
|
|
30
|
+
|
|
31
|
+
if (version === 4) {
|
|
32
|
+
const parts = address.split(".").map((part) => Number(part));
|
|
33
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const [a, b] = parts;
|
|
37
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
38
|
+
if (a === 169 && b === 254) return true;
|
|
39
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
40
|
+
if (a === 192 && b === 168) return true;
|
|
41
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
42
|
+
if (a >= 224) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (version === 6) {
|
|
47
|
+
// Classify on the expanded hextets, never on the text. `::ffff:127.0.0.1`
|
|
48
|
+
// and `::ffff:7f00:1` are the same address, and a prefix-matching check
|
|
49
|
+
// sees only the first.
|
|
50
|
+
const hextets = expandIpv6(address);
|
|
51
|
+
if (!hextets) return true;
|
|
52
|
+
|
|
53
|
+
const embedsIpv4 =
|
|
54
|
+
hextets.slice(0, 5).every((part) => part === 0) &&
|
|
55
|
+
(hextets[5] === 0xffff || hextets[5] === 0);
|
|
56
|
+
if (embedsIpv4 && (hextets[6] !== 0 || hextets[7] !== 0)) {
|
|
57
|
+
const ipv4 = [
|
|
58
|
+
hextets[6] >> 8,
|
|
59
|
+
hextets[6] & 0xff,
|
|
60
|
+
hextets[7] >> 8,
|
|
61
|
+
hextets[7] & 0xff,
|
|
62
|
+
].join(".");
|
|
63
|
+
return isPrivateAddress(ipv4);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Unspecified (::) and loopback (::1).
|
|
67
|
+
if (hextets.every((part) => part === 0)) return true;
|
|
68
|
+
if (hextets.slice(0, 7).every((part) => part === 0) && hextets[7] === 1) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Unique-local fc00::/7, link-local fe80::/10, multicast ff00::/8.
|
|
72
|
+
if ((hextets[0] & 0xfe00) === 0xfc00) return true;
|
|
73
|
+
if ((hextets[0] & 0xffc0) === 0xfe80) return true;
|
|
74
|
+
if ((hextets[0] & 0xff00) === 0xff00) return true;
|
|
75
|
+
// NAT64 well-known prefix 64:ff9b::/96 tunnels an IPv4 destination.
|
|
76
|
+
if (hextets[0] === 0x0064 && hextets[1] === 0xff9b) {
|
|
77
|
+
const ipv4 = [
|
|
78
|
+
hextets[6] >> 8,
|
|
79
|
+
hextets[6] & 0xff,
|
|
80
|
+
hextets[7] >> 8,
|
|
81
|
+
hextets[7] & 0xff,
|
|
82
|
+
].join(".");
|
|
83
|
+
return isPrivateAddress(ipv4);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Expand any textual IPv6 form into its 8 hextets, including `::` elision and
|
|
93
|
+
* a dotted IPv4 tail. Returns null when the input is not parseable.
|
|
94
|
+
*/
|
|
95
|
+
export function expandIpv6(address: string): number[] | null {
|
|
96
|
+
// Zone indices ("fe80::1%eth0") are routing hints, not part of the address.
|
|
97
|
+
const bare = address.toLowerCase().split("%")[0];
|
|
98
|
+
if (!bare) return null;
|
|
99
|
+
|
|
100
|
+
let head = bare;
|
|
101
|
+
let tail = "";
|
|
102
|
+
const elision = bare.indexOf("::");
|
|
103
|
+
if (elision !== -1) {
|
|
104
|
+
head = bare.slice(0, elision);
|
|
105
|
+
tail = bare.slice(elision + 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parseGroups = (segment: string): string[] =>
|
|
109
|
+
segment.length === 0 ? [] : segment.split(":");
|
|
110
|
+
|
|
111
|
+
const headGroups = parseGroups(head);
|
|
112
|
+
const tailGroups = parseGroups(tail);
|
|
113
|
+
const all = [...headGroups, ...tailGroups];
|
|
114
|
+
|
|
115
|
+
// A dotted IPv4 tail occupies the final two hextets.
|
|
116
|
+
const last = all[all.length - 1];
|
|
117
|
+
let ipv4Tail: number[] | null = null;
|
|
118
|
+
if (last?.includes(".")) {
|
|
119
|
+
if (net.isIP(last) !== 4) return null;
|
|
120
|
+
const octets = last.split(".").map((part) => Number(part));
|
|
121
|
+
if (
|
|
122
|
+
octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
123
|
+
)
|
|
124
|
+
return null;
|
|
125
|
+
ipv4Tail = [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
|
|
126
|
+
if (tailGroups.length > 0) tailGroups.pop();
|
|
127
|
+
else headGroups.pop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const toHextets = (groups: string[]): number[] | null => {
|
|
131
|
+
const out: number[] = [];
|
|
132
|
+
for (const group of groups) {
|
|
133
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
|
|
134
|
+
out.push(parseInt(group, 16));
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const headParts = toHextets(headGroups);
|
|
140
|
+
const tailParts = toHextets(tailGroups);
|
|
141
|
+
if (!headParts || !tailParts) return null;
|
|
142
|
+
|
|
143
|
+
const explicit = [...headParts, ...tailParts, ...(ipv4Tail ? ipv4Tail : [])]
|
|
144
|
+
.length;
|
|
145
|
+
if (explicit > 8) return null;
|
|
146
|
+
|
|
147
|
+
if (elision === -1) {
|
|
148
|
+
if (explicit !== 8) return null;
|
|
149
|
+
return [...headParts, ...(ipv4Tail ?? [])];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const fill = new Array(8 - explicit).fill(0) as number[];
|
|
153
|
+
return [...headParts, ...fill, ...tailParts, ...(ipv4Tail ?? [])];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse a caller-supplied image URL, refusing anything that is not a plain
|
|
158
|
+
* public http(s) resource. Returns null rather than throwing so callers can
|
|
159
|
+
* answer with a single 400.
|
|
160
|
+
*/
|
|
161
|
+
export function parseProxyableImageUrl(raw: string): URL | null {
|
|
162
|
+
if (!raw) return null;
|
|
163
|
+
|
|
164
|
+
let url: URL;
|
|
165
|
+
try {
|
|
166
|
+
url = new URL(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
// coercion-ok: null is this function's documented "refused" result and
|
|
169
|
+
// the caller answers 400; an unparseable URL carries no other detail.
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
174
|
+
// Credentials in the URL would be replayed by the server on the user's
|
|
175
|
+
// behalf against a host they may not control.
|
|
176
|
+
if (url.username || url.password) return null;
|
|
177
|
+
|
|
178
|
+
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
179
|
+
if (!host) return null;
|
|
180
|
+
if (BLOCKED_HOSTNAMES.has(host)) return null;
|
|
181
|
+
if (BLOCKED_SUFFIXES.some((suffix) => host.endsWith(suffix))) return null;
|
|
182
|
+
|
|
183
|
+
const literal = host.startsWith("[") ? host.slice(1, -1) : host;
|
|
184
|
+
if (net.isIP(literal) && isPrivateAddress(literal)) return null;
|
|
185
|
+
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A hostname is no longer validated here before fetching. Resolving once for
|
|
190
|
+
// the check and again for the connection let a DNS-rebinding host answer
|
|
191
|
+
// differently each time, so the address check now lives in the socket's
|
|
192
|
+
// `lookup` — see `publicOnlyLookup` in fetch-remote-image.ts.
|
|
@@ -52,9 +52,11 @@ export default createAgentChatPlugin({
|
|
|
52
52
|
prepareRequest: prepareSlidesChatAttachments,
|
|
53
53
|
systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow.
|
|
54
54
|
|
|
55
|
-
When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio.
|
|
55
|
+
When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true and one patch-slide operation with fields.content for every source slide ID; the action rejects partial coverage. Do not split a full-deck restyle into arbitrary batches or use one-by-one update-slide calls - reserve update-slide for targeted one-slide edits. After the patch succeeds, verify with get-deck using compact=true so the verification does not retransmit every slide's HTML. Do not claim a partial or initial pass. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
When the active Slides editor is already showing the deck you just changed, do not include an "Open the updated deck" or similar link in the final response. The deck is already open and the action updated it in place; say that plainly instead. Only provide an open-deck link when the user is elsewhere, the active deck is different, or the user explicitly asks for a link.
|
|
58
|
+
|
|
59
|
+
For source-faithful PDF slides, keep the original full-page image and style around it with restrained design-system chrome such as a frame, edge treatment, caption, or safe overlay that does not obscure source content; never OCR-reconstruct the page from extracted text. For PPTX slides, preserve the imported positioned HTML and every uploaded source image. The patch-deck and update-slide actions enforce these preservation rules by default; pass preserveSource=false only when the user explicitly requests a rewrite of that slide.
|
|
58
60
|
|
|
59
61
|
If the deck has designSystemId, call get-design-system before writing and follow its exact agentContext tokens, assets, and custom instructions. If the user asks for on-brand styling and no design system is linked, call get-workspace-defaults, link its usable design system with patch-deck, then call get-design-system. Do not improvise a generic Builder-like palette when configured Builder.io design-system context is available.
|
|
60
62
|
|
|
@@ -68,7 +70,7 @@ Provider-specific Slides actions are shortcuts, not limits. If a first-class act
|
|
|
68
70
|
|
|
69
71
|
Slides' Google Drive provider API uses the user's connected Google Docs OAuth account. Picker imports keep the per-file drive.file path, while pasted Google Slides links use Drive export access and may ask the user to reconnect Google. For large Drive file lists or metadata sweeps, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.
|
|
70
72
|
|
|
71
|
-
When a Google Drive or Google Slides request needs authentication, tell the user to use the Connect Google button
|
|
73
|
+
When a Google Drive or Google Slides request needs authentication, tell the user to use the Connect Google button in the Google Slides import step. Do not expose internal routes, API endpoints, OAuth setup instructions, client IDs, or keys in the response, and do not ask the user to configure an API manually.`,
|
|
72
74
|
mentionProviders: async () => {
|
|
73
75
|
const { getDb } = await import("../db/index.js");
|
|
74
76
|
const { decks, deckShares } = await import("../db/schema.js");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getSession } from "@agent-native/core/server";
|
|
2
|
+
import { defineEventHandler, getQuery, setResponseStatus } from "h3";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
fetchRemoteImage,
|
|
6
|
+
type RemoteImageFailure,
|
|
7
|
+
} from "../../lib/fetch-remote-image.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Re-serve a remote image from our own origin.
|
|
11
|
+
*
|
|
12
|
+
* PDF/PPTX export rasterizes the slide DOM through a canvas, and the browser
|
|
13
|
+
* blanks out any image whose host does not send `Access-Control-Allow-Origin`.
|
|
14
|
+
* No client-side flag can override that, so images on hosts without CORS have
|
|
15
|
+
* to come back through us to be same-origin.
|
|
16
|
+
*
|
|
17
|
+
* This is an authenticated, image-only, size-capped fetcher — not a general
|
|
18
|
+
* proxy. See `fetch-remote-image.ts` for the address pinning that keeps it
|
|
19
|
+
* from being turned into an SSRF primitive.
|
|
20
|
+
*/
|
|
21
|
+
const FAILURE_STATUS: Record<RemoteImageFailure, number> = {
|
|
22
|
+
"unsupported-url": 400,
|
|
23
|
+
"blocked-address": 400,
|
|
24
|
+
"fetch-failed": 502,
|
|
25
|
+
"too-many-redirects": 502,
|
|
26
|
+
"not-an-image": 415,
|
|
27
|
+
"too-large": 413,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FAILURE_MESSAGE: Record<RemoteImageFailure, string> = {
|
|
31
|
+
"unsupported-url": "Unsupported image URL",
|
|
32
|
+
"blocked-address": "Unsupported image URL",
|
|
33
|
+
"fetch-failed": "Could not fetch image",
|
|
34
|
+
"too-many-redirects": "Too many redirects",
|
|
35
|
+
"not-an-image": "Not an image",
|
|
36
|
+
"too-large": "Image too large",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default defineEventHandler(async (event) => {
|
|
40
|
+
const session = await getSession(event);
|
|
41
|
+
if (!session?.email) {
|
|
42
|
+
setResponseStatus(event, 401);
|
|
43
|
+
return { error: "Unauthorized" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const raw = getQuery(event).url;
|
|
47
|
+
if (typeof raw !== "string") {
|
|
48
|
+
setResponseStatus(event, 400);
|
|
49
|
+
return { error: "Missing url" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await fetchRemoteImage(raw);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
setResponseStatus(event, FAILURE_STATUS[result.reason]);
|
|
55
|
+
return { error: FAILURE_MESSAGE[result.reason] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
event.node?.res?.setHeader("Content-Type", result.contentType);
|
|
59
|
+
event.node?.res?.setHeader("Content-Length", String(result.body.byteLength));
|
|
60
|
+
event.node?.res?.setHeader("Cache-Control", "private, max-age=3600");
|
|
61
|
+
// The canvas reads these pixels back, so the response must be explicitly
|
|
62
|
+
// usable cross-origin even though it is served from our own host.
|
|
63
|
+
event.node?.res?.setHeader("Access-Control-Allow-Origin", "*");
|
|
64
|
+
return result.body;
|
|
65
|
+
});
|
|
@@ -137,7 +137,7 @@ function normalizeString(value: unknown, fallback: string): string {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function normalizeElementPath(value: unknown): number[] | undefined {
|
|
140
|
-
if (!Array.isArray(value)) return undefined;
|
|
140
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
141
141
|
const path = value.filter(
|
|
142
142
|
(part): part is number =>
|
|
143
143
|
typeof part === "number" &&
|
|
@@ -155,6 +155,7 @@ function normalizeSlideAnimation(
|
|
|
155
155
|
if (!isRecord(value)) return null;
|
|
156
156
|
|
|
157
157
|
const elementPath = normalizeElementPath(value.elementPath);
|
|
158
|
+
if (value.elementPath !== undefined && !elementPath) return null;
|
|
158
159
|
const rawElementIndex = value.elementIndex;
|
|
159
160
|
const hasElementIndex =
|
|
160
161
|
typeof rawElementIndex === "number" &&
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const GENERATED_TITLE_PLACEHOLDERS = new Set([
|
|
2
|
+
"deck",
|
|
3
|
+
"date",
|
|
4
|
+
"image slide title",
|
|
5
|
+
"presentation title",
|
|
6
|
+
"section",
|
|
7
|
+
"section title",
|
|
8
|
+
"slide title",
|
|
9
|
+
"untitled",
|
|
10
|
+
"untitled deck",
|
|
11
|
+
"your name",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generated deck ids should never become user-facing titles. Keep this
|
|
16
|
+
* deliberately narrow so normal titles with spaces and punctuation remain
|
|
17
|
+
* valid, while catching opaque mixed-case tokens such as H3sVsnns-TEVUOpz9w.
|
|
18
|
+
*/
|
|
19
|
+
const OPAQUE_DECK_TITLE_PATTERN =
|
|
20
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z0-9_-]{12,64}$/;
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_DECK_TITLE = "Untitled Deck";
|
|
23
|
+
|
|
24
|
+
export function isOpaqueDeckTitle(value: unknown): value is string {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === "string" && OPAQUE_DECK_TITLE_PATTERN.test(value.trim())
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isGeneratedDeckTitle(value: unknown): value is string {
|
|
31
|
+
if (typeof value !== "string") return false;
|
|
32
|
+
const title = value.trim();
|
|
33
|
+
return (
|
|
34
|
+
GENERATED_TITLE_PLACEHOLDERS.has(title.toLowerCase()) ||
|
|
35
|
+
isOpaqueDeckTitle(title)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeHtmlEntities(value: string): string {
|
|
40
|
+
const decodeCodePoint = (raw: string, radix: number): string => {
|
|
41
|
+
const codePoint = Number.parseInt(raw, radix);
|
|
42
|
+
return Number.isNaN(codePoint) || codePoint > 0x10ffff
|
|
43
|
+
? ""
|
|
44
|
+
: String.fromCodePoint(codePoint);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return value
|
|
48
|
+
.replace(/ /gi, " ")
|
|
49
|
+
.replace(/&/gi, "&")
|
|
50
|
+
.replace(/</gi, "<")
|
|
51
|
+
.replace(/>/gi, ">")
|
|
52
|
+
.replace(/"/gi, '"')
|
|
53
|
+
.replace(/(?:'|')/gi, "'")
|
|
54
|
+
.replace(/&#x([0-9a-f]+);?/gi, (_, hex: string) => decodeCodePoint(hex, 16))
|
|
55
|
+
.replace(/&#(\d+);?/g, (_, digits: string) => decodeCodePoint(digits, 10));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function plainText(value: string): string {
|
|
59
|
+
return decodeHtmlEntities(
|
|
60
|
+
value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, " "),
|
|
61
|
+
)
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function usableCandidate(value: string): string | null {
|
|
67
|
+
const candidate = plainText(value).replace(/^[•●▪‣\-\s]+/, "");
|
|
68
|
+
if (!candidate || candidate.length > 140) return null;
|
|
69
|
+
if (GENERATED_TITLE_PLACEHOLDERS.has(candidate.toLowerCase())) return null;
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recover a deck title from the largest title-like text in its first slide.
|
|
75
|
+
* This is intentionally HTML-string based because the action runs on the
|
|
76
|
+
* server without a browser DOM.
|
|
77
|
+
*/
|
|
78
|
+
export function deriveDeckTitleFromSlideContent(
|
|
79
|
+
content: unknown,
|
|
80
|
+
): string | null {
|
|
81
|
+
if (typeof content !== "string" || !content.trim()) return null;
|
|
82
|
+
|
|
83
|
+
const candidates: Array<{ score: number; text: string }> = [];
|
|
84
|
+
const headingPattern = /<(h[1-6])\b[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
85
|
+
for (const match of content.matchAll(headingPattern)) {
|
|
86
|
+
const text = usableCandidate(match[2] ?? "");
|
|
87
|
+
if (text) {
|
|
88
|
+
const level = Number.parseInt(match[1].slice(1), 10);
|
|
89
|
+
candidates.push({ score: 1000 - level * 10, text });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const styledTextPattern =
|
|
94
|
+
/<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
95
|
+
for (const match of content.matchAll(styledTextPattern)) {
|
|
96
|
+
const fontSize = Number.parseFloat(
|
|
97
|
+
match[3]?.match(/(?:^|;)\s*font-size\s*:\s*([\d.]+)px/i)?.[1] ?? "0",
|
|
98
|
+
);
|
|
99
|
+
if (fontSize < 28) continue;
|
|
100
|
+
const text = usableCandidate(match[4] ?? "");
|
|
101
|
+
if (text) candidates.push({ score: fontSize, text });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return candidates.sort((a, b) => b.score - a.score)[0]?.text ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return a human-readable replacement only when the requested title is a
|
|
109
|
+
* generated placeholder or opaque id. A meaningful existing title wins when
|
|
110
|
+
* a stale full-payload save tries to replace it with a generated value.
|
|
111
|
+
*/
|
|
112
|
+
export function repairGeneratedDeckTitle(
|
|
113
|
+
requestedTitle: unknown,
|
|
114
|
+
firstSlideContent: unknown,
|
|
115
|
+
existingTitle?: unknown,
|
|
116
|
+
): string | null {
|
|
117
|
+
if (!isGeneratedDeckTitle(requestedTitle)) return null;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
deriveDeckTitleFromSlideContent(firstSlideContent) ??
|
|
121
|
+
(typeof existingTitle === "string" &&
|
|
122
|
+
existingTitle.trim() &&
|
|
123
|
+
!isGeneratedDeckTitle(existingTitle)
|
|
124
|
+
? existingTitle
|
|
125
|
+
: null)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertHumanReadableDeckTitle(title: string): void {
|
|
130
|
+
if (isOpaqueDeckTitle(title)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Deck title must be a concise, human-readable title; generated ids are not valid titles.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -89,7 +89,10 @@ export function TaskListRow({
|
|
|
89
89
|
onAnimationEnd={completion.handleRowAnimationEnd}
|
|
90
90
|
dataAttributes={{ "data-task-id": item.id }}
|
|
91
91
|
className={cn(
|
|
92
|
-
|
|
92
|
+
// Scoped to the phases that actually animate. Applied unconditionally
|
|
93
|
+
// it promoted a layer for every row in the list and never retired any
|
|
94
|
+
// of them, for an animation that only runs while a row completes.
|
|
95
|
+
completionPhase !== "idle" && "will-change-[opacity,transform]",
|
|
93
96
|
displayDone && completionPhase === "idle" && !selected && "opacity-60",
|
|
94
97
|
completionPhase === "completing" &&
|
|
95
98
|
hideAfterComplete &&
|
|
@@ -138,7 +138,7 @@ function googleThinkingBudget(effort) {
|
|
|
138
138
|
return -1;
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
-
* Map
|
|
141
|
+
* Map an effort level to Gemini 3.x thinkingLevel string.
|
|
142
142
|
* Gemini 3 models (gemini-3.*) reject thinkingBudget and require thinkingLevel
|
|
143
143
|
* with values 'low' | 'medium' | 'high'. Gemini 3.0 only supports 'low'/'high';
|
|
144
144
|
* Gemini 3.1+ adds 'medium'. We always emit 'medium' for medium effort since it
|
|
@@ -266,7 +266,7 @@ export interface ProductionAgentOptions {
|
|
|
266
266
|
model?: string;
|
|
267
267
|
/** App/template id used for org-scoped per-app model defaults. */
|
|
268
268
|
appId?: string;
|
|
269
|
-
/** Default
|
|
269
|
+
/** Default effort for requests that do not supply an override. */
|
|
270
270
|
reasoningEffort?: ReasoningEffort;
|
|
271
271
|
/** Provider-specific options passed through to the engine */
|
|
272
272
|
providerOptions?: EngineMessage extends never ? never : any;
|
|
@@ -632,8 +632,16 @@ export interface ExecuteAgentToolCallOptions {
|
|
|
632
632
|
threadId?: string;
|
|
633
633
|
turnId?: string;
|
|
634
634
|
approvedToolCalls?: string[];
|
|
635
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
636
|
+
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
635
637
|
send?: (event: AgentChatEvent) => void;
|
|
636
638
|
}
|
|
639
|
+
export interface AgentApprovalBinding {
|
|
640
|
+
toolName: string;
|
|
641
|
+
input: unknown;
|
|
642
|
+
callId: string;
|
|
643
|
+
approvalKey: string;
|
|
644
|
+
}
|
|
637
645
|
/**
|
|
638
646
|
* Execute one externally-selected tool through the exact same guarded runtime
|
|
639
647
|
* as a normal model-selected tool call. Realtime voice and other duplex
|
|
@@ -658,6 +666,11 @@ export declare function buildFirstRequestPayloadDetail(input: {
|
|
|
658
666
|
availableToolCount: number;
|
|
659
667
|
}): string;
|
|
660
668
|
export declare function toolCallCacheKey(toolName: string, input: unknown): string;
|
|
669
|
+
export declare function findApprovedStructuredToolCall(history: AgentChatStructuredMessage[] | undefined, approvedToolCalls: readonly string[] | undefined): {
|
|
670
|
+
name: string;
|
|
671
|
+
input: unknown;
|
|
672
|
+
callId: string;
|
|
673
|
+
} | undefined;
|
|
661
674
|
export declare function repeatedSourceSweepGuardMessage(opts: {
|
|
662
675
|
toolName: string;
|
|
663
676
|
priorCalls: number;
|
|
@@ -769,6 +782,13 @@ export declare function runAgentLoop(opts: {
|
|
|
769
782
|
* `approval_required`. See `AgentChatRequest.approvedToolCalls`.
|
|
770
783
|
*/
|
|
771
784
|
approvedToolCalls?: string[];
|
|
785
|
+
/**
|
|
786
|
+
* Server-side approval persistence hooks. The HTTP transport supplies these
|
|
787
|
+
* so client-supplied history is never the authorization source; direct loop
|
|
788
|
+
* callers may omit them when they own the surrounding approval boundary.
|
|
789
|
+
*/
|
|
790
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
791
|
+
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
772
792
|
/**
|
|
773
793
|
* In-loop processor seam (see `processors.ts`). Each processor can observe
|
|
774
794
|
* streamed chunks, observe model responses around tool execution, and
|