@agent-native/core 0.126.0 → 0.127.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +71 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  11. package/corpus/core/src/client/agent-chat.ts +19 -1
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  14. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  15. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  16. package/corpus/core/src/client/use-chat-models.ts +54 -37
  17. package/corpus/core/src/localization/default-messages.ts +11 -0
  18. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  19. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  20. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  21. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  22. package/corpus/core/src/server/agent-discovery.ts +1 -1
  23. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  24. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  25. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  26. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  27. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  28. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  29. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  30. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  31. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  32. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  33. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  34. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  35. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  36. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  37. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  38. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  39. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  40. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  41. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  42. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  43. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  44. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  45. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  46. package/corpus/toolkit/CHANGELOG.md +6 -0
  47. package/corpus/toolkit/package.json +1 -1
  48. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  49. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  50. package/dist/a2a/index.d.ts +2 -0
  51. package/dist/a2a/index.d.ts.map +1 -1
  52. package/dist/a2a/index.js +1 -0
  53. package/dist/a2a/index.js.map +1 -1
  54. package/dist/agent/production-agent.d.ts +2 -2
  55. package/dist/agent/production-agent.d.ts.map +1 -1
  56. package/dist/agent/production-agent.js +6 -3
  57. package/dist/agent/production-agent.js.map +1 -1
  58. package/dist/agent/run-manager.d.ts +11 -0
  59. package/dist/agent/run-manager.d.ts.map +1 -1
  60. package/dist/agent/run-manager.js +34 -1
  61. package/dist/agent/run-manager.js.map +1 -1
  62. package/dist/agent/run-store.d.ts +8 -0
  63. package/dist/agent/run-store.d.ts.map +1 -1
  64. package/dist/agent/run-store.js +16 -0
  65. package/dist/agent/run-store.js.map +1 -1
  66. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  67. package/dist/client/MultiTabAssistantChat.js +60 -31
  68. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  69. package/dist/client/agent-chat.d.ts +6 -0
  70. package/dist/client/agent-chat.d.ts.map +1 -1
  71. package/dist/client/agent-chat.js +13 -1
  72. package/dist/client/agent-chat.js.map +1 -1
  73. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  74. package/dist/client/chat/tool-call-display.js +14 -0
  75. package/dist/client/chat/tool-call-display.js.map +1 -1
  76. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  77. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  78. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  79. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  80. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  81. package/dist/client/settings/AgentsSection.js +250 -30
  82. package/dist/client/settings/AgentsSection.js.map +1 -1
  83. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  84. package/dist/client/settings/SecretsSection.js +16 -2
  85. package/dist/client/settings/SecretsSection.js.map +1 -1
  86. package/dist/client/use-chat-models.d.ts.map +1 -1
  87. package/dist/client/use-chat-models.js +46 -27
  88. package/dist/client/use-chat-models.js.map +1 -1
  89. package/dist/collab/routes.d.ts +1 -1
  90. package/dist/localization/default-messages.d.ts +9 -0
  91. package/dist/localization/default-messages.d.ts.map +1 -1
  92. package/dist/localization/default-messages.js +9 -0
  93. package/dist/localization/default-messages.js.map +1 -1
  94. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  95. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  96. package/dist/mcp/ask-app-task-handle.js +136 -0
  97. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  98. package/dist/mcp/builtin-tools.d.ts +3 -3
  99. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  100. package/dist/mcp/builtin-tools.js +115 -38
  101. package/dist/mcp/builtin-tools.js.map +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  104. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  105. package/dist/server/agent-capabilities.d.ts +7 -1
  106. package/dist/server/agent-capabilities.d.ts.map +1 -1
  107. package/dist/server/agent-capabilities.js +1 -0
  108. package/dist/server/agent-capabilities.js.map +1 -1
  109. package/dist/server/agent-chat-plugin.d.ts +7 -0
  110. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  111. package/dist/server/agent-chat-plugin.js +16 -1
  112. package/dist/server/agent-chat-plugin.js.map +1 -1
  113. package/dist/server/agent-discovery.d.ts +1 -0
  114. package/dist/server/agent-discovery.d.ts.map +1 -1
  115. package/dist/server/agent-discovery.js +1 -1
  116. package/dist/server/agent-discovery.js.map +1 -1
  117. package/dist/server/agent-peer-probe.d.ts +37 -0
  118. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  119. package/dist/server/agent-peer-probe.js +90 -0
  120. package/dist/server/agent-peer-probe.js.map +1 -0
  121. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  122. package/dist/server/core-routes-plugin.js +53 -0
  123. package/dist/server/core-routes-plugin.js.map +1 -1
  124. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  125. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  126. package/dist/shared/mcp-embed-headers.js +4 -0
  127. package/dist/shared/mcp-embed-headers.js.map +1 -1
  128. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  129. package/docs/content/external-agents-catalog.mdx +14 -11
  130. package/docs/content/mcp-protocol.mdx +9 -4
  131. package/package.json +2 -2
  132. package/src/a2a/index.ts +2 -0
  133. package/src/agent/production-agent.ts +6 -1
  134. package/src/agent/run-manager.ts +37 -0
  135. package/src/agent/run-store.ts +18 -0
  136. package/src/client/MultiTabAssistantChat.tsx +75 -43
  137. package/src/client/agent-chat.ts +19 -1
  138. package/src/client/chat/tool-call-display.tsx +25 -0
  139. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  140. package/src/client/settings/AgentsSection.tsx +565 -71
  141. package/src/client/settings/SecretsSection.tsx +71 -36
  142. package/src/client/use-chat-models.ts +54 -37
  143. package/src/localization/default-messages.ts +11 -0
  144. package/src/mcp/ask-app-task-handle.ts +183 -0
  145. package/src/mcp/builtin-tools.ts +146 -34
  146. package/src/server/agent-capabilities.ts +8 -1
  147. package/src/server/agent-chat-plugin.ts +17 -0
  148. package/src/server/agent-discovery.ts +1 -1
  149. package/src/server/agent-peer-probe.ts +133 -0
  150. package/src/server/core-routes-plugin.ts +67 -0
  151. package/src/shared/mcp-embed-headers.ts +4 -0
  152. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
@@ -3,6 +3,7 @@ import {
3
3
  IconChevronLeft,
4
4
  IconChevronRight,
5
5
  IconMaximize,
6
+ IconNotes,
6
7
  IconX,
7
8
  } from "@tabler/icons-react";
8
9
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
@@ -20,6 +21,8 @@ import {
20
21
  resolveSlideAnimationElement,
21
22
  } from "@/lib/slide-animation-elements";
22
23
 
24
+ import { openPresentChannel, type PresentMessage } from "./present-channel";
25
+
23
26
  interface PresentationViewProps {
24
27
  slides: Slide[];
25
28
  deckId: string;
@@ -271,6 +274,58 @@ export default function PresentationView({
271
274
  }
272
275
  }, [navigate, deckId, isShared]);
273
276
 
277
+ // Presenter window: it owns no navigation state of its own, it just sends
278
+ // commands and mirrors whatever we echo back — so build steps stay
279
+ // authoritative here.
280
+ const channelRef = useRef<BroadcastChannel | null>(null);
281
+ const goNextRef = useRef(goNext);
282
+ const goPrevRef = useRef(goPrev);
283
+ goNextRef.current = goNext;
284
+ goPrevRef.current = goPrev;
285
+
286
+ const broadcastState = useCallback(() => {
287
+ channelRef.current?.postMessage({
288
+ type: "state",
289
+ index: currentIndex,
290
+ } satisfies PresentMessage);
291
+ }, [currentIndex]);
292
+ const broadcastStateRef = useRef(broadcastState);
293
+ broadcastStateRef.current = broadcastState;
294
+
295
+ useEffect(() => {
296
+ const channel = openPresentChannel(deckId);
297
+ channelRef.current = channel;
298
+ if (!channel) return;
299
+ channel.onmessage = (event: MessageEvent<PresentMessage>) => {
300
+ const message = event.data;
301
+ if (message?.type === "hello") {
302
+ broadcastStateRef.current();
303
+ } else if (message?.type === "command") {
304
+ if (message.command === "next") goNextRef.current();
305
+ else goPrevRef.current();
306
+ }
307
+ };
308
+ return () => {
309
+ channel.close();
310
+ channelRef.current = null;
311
+ };
312
+ }, [deckId]);
313
+
314
+ useEffect(() => {
315
+ broadcastState();
316
+ }, [broadcastState]);
317
+
318
+ const openPresenterWindow = useCallback(() => {
319
+ const url = new URL(window.location.href);
320
+ url.searchParams.set("presenter", "1");
321
+ url.searchParams.set("slide", String(currentIndex + 1));
322
+ window.open(
323
+ url.toString(),
324
+ `slides-presenter-${deckId}`,
325
+ "width=1200,height=760",
326
+ );
327
+ }, [currentIndex, deckId]);
328
+
274
329
  useEffect(() => {
275
330
  const handleKey = (e: KeyboardEvent) => {
276
331
  switch (e.key) {
@@ -294,6 +349,11 @@ export default function PresentationView({
294
349
  document.exitFullscreen().catch(() => {});
295
350
  }
296
351
  break;
352
+ case "s":
353
+ case "S":
354
+ e.preventDefault();
355
+ openPresenterWindow();
356
+ break;
297
357
  case "Escape":
298
358
  exit();
299
359
  break;
@@ -301,7 +361,7 @@ export default function PresentationView({
301
361
  };
302
362
  window.addEventListener("keydown", handleKey);
303
363
  return () => window.removeEventListener("keydown", handleKey);
304
- }, [goNext, goPrev, exit]);
364
+ }, [goNext, goPrev, exit, openPresenterWindow]);
305
365
 
306
366
  // Try to enter fullscreen. Browsers require a user gesture; the click that
307
367
  // navigated to /present often counts, but Safari/Firefox sometimes block
@@ -501,13 +561,23 @@ export default function PresentationView({
501
561
  </button>
502
562
  </div>
503
563
 
504
- <button
505
- onClick={exit}
506
- className="p-3 sm:p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
507
- aria-label={t("presentation.exitPresentation")}
508
- >
509
- <IconX className="w-5 h-5 sm:w-4 sm:h-4 text-white" />
510
- </button>
564
+ <div className="flex items-center gap-2">
565
+ <button
566
+ onClick={openPresenterWindow}
567
+ className="p-3 sm:p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors cursor-pointer"
568
+ aria-label={t("presentation.presenterView")}
569
+ title={t("presentation.presenterView")}
570
+ >
571
+ <IconNotes className="w-5 h-5 sm:w-4 sm:h-4 text-white" />
572
+ </button>
573
+ <button
574
+ onClick={exit}
575
+ className="p-3 sm:p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
576
+ aria-label={t("presentation.exitPresentation")}
577
+ >
578
+ <IconX className="w-5 h-5 sm:w-4 sm:h-4 text-white" />
579
+ </button>
580
+ </div>
511
581
  </div>
512
582
 
513
583
  {/* Progress bar */}
@@ -0,0 +1,179 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconChevronLeft, IconChevronRight, IconX } from "@tabler/icons-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import SlideRenderer from "@/components/deck/SlideRenderer";
6
+ import type { Slide } from "@/context/DeckContext";
7
+ import type { AspectRatio } from "@/lib/aspect-ratios";
8
+
9
+ import { openPresentChannel, type PresentMessage } from "./present-channel";
10
+
11
+ interface PresenterViewProps {
12
+ slides: Slide[];
13
+ deckId: string;
14
+ startIndex?: number;
15
+ aspectRatio?: AspectRatio;
16
+ }
17
+
18
+ function formatElapsed(seconds: number) {
19
+ const mins = Math.floor(seconds / 60);
20
+ const secs = seconds % 60;
21
+ return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
22
+ }
23
+
24
+ export default function PresenterView({
25
+ slides,
26
+ deckId,
27
+ startIndex = 0,
28
+ aspectRatio,
29
+ }: PresenterViewProps) {
30
+ const t = useT();
31
+ const [index, setIndex] = useState(startIndex);
32
+ const [elapsed, setElapsed] = useState(0);
33
+ const channelRef = useRef<BroadcastChannel | null>(null);
34
+
35
+ useEffect(() => {
36
+ const channel = openPresentChannel(deckId);
37
+ channelRef.current = channel;
38
+ if (!channel) return;
39
+ channel.onmessage = (event: MessageEvent<PresentMessage>) => {
40
+ if (event.data?.type === "state") setIndex(event.data.index);
41
+ };
42
+ channel.postMessage({ type: "hello" } satisfies PresentMessage);
43
+ return () => {
44
+ channel.close();
45
+ channelRef.current = null;
46
+ };
47
+ }, [deckId]);
48
+
49
+ useEffect(() => {
50
+ const timer = window.setInterval(() => setElapsed((s) => s + 1), 1000);
51
+ return () => window.clearInterval(timer);
52
+ }, []);
53
+
54
+ const send = useCallback((message: PresentMessage) => {
55
+ channelRef.current?.postMessage(message);
56
+ }, []);
57
+
58
+ const goNext = useCallback(
59
+ () => send({ type: "command", command: "next" }),
60
+ [send],
61
+ );
62
+ const goPrev = useCallback(
63
+ () => send({ type: "command", command: "prev" }),
64
+ [send],
65
+ );
66
+
67
+ useEffect(() => {
68
+ const handleKey = (e: KeyboardEvent) => {
69
+ switch (e.key) {
70
+ case "ArrowRight":
71
+ case "ArrowDown":
72
+ case " ":
73
+ e.preventDefault();
74
+ goNext();
75
+ break;
76
+ case "ArrowLeft":
77
+ case "ArrowUp":
78
+ e.preventDefault();
79
+ goPrev();
80
+ break;
81
+ }
82
+ };
83
+ window.addEventListener("keydown", handleKey);
84
+ return () => window.removeEventListener("keydown", handleKey);
85
+ }, [goNext, goPrev]);
86
+
87
+ const safeSlides = useMemo(
88
+ () => (Array.isArray(slides) ? slides.filter(Boolean) : []),
89
+ [slides],
90
+ );
91
+ const current = safeSlides[index];
92
+ const next = safeSlides[index + 1];
93
+ const notes = current?.notes?.trim();
94
+
95
+ return (
96
+ <div className="fixed inset-0 flex flex-col bg-[hsl(240,6%,6%)] text-white">
97
+ <header className="flex items-center justify-between gap-4 border-b border-white/10 px-5 py-3">
98
+ <span className="font-mono text-sm text-white/50">
99
+ {index + 1} / {safeSlides.length}
100
+ </span>
101
+ <span className="font-mono text-2xl tabular-nums text-white/80">
102
+ {formatElapsed(elapsed)}
103
+ </span>
104
+ <div className="flex items-center gap-2">
105
+ <button
106
+ onClick={goPrev}
107
+ disabled={index === 0}
108
+ className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-30"
109
+ aria-label={t("presentation.previousSlide")}
110
+ >
111
+ <IconChevronLeft className="h-4 w-4" />
112
+ </button>
113
+ <button
114
+ onClick={goNext}
115
+ disabled={index >= safeSlides.length - 1}
116
+ className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-30"
117
+ aria-label={t("presentation.nextSlide")}
118
+ >
119
+ <IconChevronRight className="h-4 w-4" />
120
+ </button>
121
+ <button
122
+ onClick={() => window.close()}
123
+ className="cursor-pointer rounded-lg bg-white/10 p-2 hover:bg-white/20"
124
+ aria-label={t("presentation.closePresenterView")}
125
+ >
126
+ <IconX className="h-4 w-4" />
127
+ </button>
128
+ </div>
129
+ </header>
130
+
131
+ <div className="flex min-h-0 flex-1 flex-col gap-4 p-4">
132
+ <div className="flex gap-4">
133
+ <div className="w-2/3 overflow-hidden rounded-lg bg-black">
134
+ {current && (
135
+ <SlideRenderer
136
+ slide={current}
137
+ thumbnail
138
+ aspectRatio={aspectRatio}
139
+ />
140
+ )}
141
+ </div>
142
+ <div className="flex w-1/3 flex-col gap-2">
143
+ <div className="font-mono text-[11px] uppercase tracking-widest text-white/40">
144
+ {t("presentation.upNext")}
145
+ </div>
146
+ {next ? (
147
+ <div className="overflow-hidden rounded-lg bg-black">
148
+ <SlideRenderer
149
+ slide={next}
150
+ thumbnail
151
+ aspectRatio={aspectRatio}
152
+ />
153
+ </div>
154
+ ) : (
155
+ <div className="rounded-lg bg-white/[0.04] p-4 text-sm text-white/40">
156
+ {t("presentation.endOfDeck")}
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+
162
+ <div className="min-h-0 flex-1 overflow-y-auto rounded-lg bg-white/[0.04] p-5">
163
+ <div className="mb-2 font-mono text-[11px] uppercase tracking-widest text-white/40">
164
+ {t("presentation.speakerNotes")}
165
+ </div>
166
+ {notes ? (
167
+ <p className="whitespace-pre-wrap text-lg leading-relaxed text-white/90">
168
+ {notes}
169
+ </p>
170
+ ) : (
171
+ <p className="text-sm text-white/40">
172
+ {t("presentation.noNotesForSlide")}
173
+ </p>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cross-window channel between the fullscreen presentation and the presenter
3
+ * window. The presenter window never advances on its own — it sends commands
4
+ * and renders whatever state the presentation echoes back, so build steps
5
+ * (which move the step, not the slide) stay authoritative in one place.
6
+ */
7
+ export type PresentMessage =
8
+ | { type: "state"; index: number }
9
+ | { type: "command"; command: "next" | "prev" }
10
+ | { type: "hello" };
11
+
12
+ export function openPresentChannel(deckId: string): BroadcastChannel | null {
13
+ if (typeof BroadcastChannel === "undefined") return null;
14
+ return new BroadcastChannel(`slides-present:${deckId}`);
15
+ }
@@ -256,7 +256,10 @@ const messages = {
256
256
  downloadHtml: "Download as HTML",
257
257
  exportPdf: "Export as PDF",
258
258
  exportPptx: "Export as PPTX",
259
- downloadGoogleSlides: "Download for Google Slides",
259
+ openInGoogleSlides: "Open in Google Slides",
260
+ googleSlidesCreated: "Opened in Google Slides",
261
+ googleSlidesCreatedHint:
262
+ "A copy of this deck was created in your Google Drive.",
260
263
  duplicateDeck: "Duplicate deck",
261
264
  },
262
265
  share: {
@@ -467,6 +470,12 @@ const messages = {
467
470
  slides: "Slides",
468
471
  },
469
472
  presentation: {
473
+ presenterView: "Presenter view",
474
+ speakerNotes: "Speaker notes",
475
+ noNotesForSlide: "No notes for this slide",
476
+ upNext: "Up next",
477
+ endOfDeck: "End of deck",
478
+ closePresenterView: "Close presenter view",
470
479
  noSlides: "No slides to present",
471
480
  previousSlide: "Previous slide",
472
481
  nextSlide: "Next slide",
@@ -0,0 +1,67 @@
1
+ import { appBasePath } from "@agent-native/core/client/api-path";
2
+
3
+ import type { AspectRatio } from "./aspect-ratios";
4
+ import { buildDeckPptxBlob } from "./export-pptx-client";
5
+
6
+ interface GoogleSlidesExportSlide {
7
+ id: string;
8
+ notes?: string;
9
+ }
10
+
11
+ export type GoogleSlidesExportResult =
12
+ | { url: string }
13
+ /** Drive was unavailable, so the PPTX was downloaded for a manual import. */
14
+ | { url: null; downloaded: true; reason: string };
15
+
16
+ function triggerBlobDownload(blob: Blob, filename: string) {
17
+ const url = URL.createObjectURL(blob);
18
+ const a = document.createElement("a");
19
+ a.href = url;
20
+ a.download = filename;
21
+ a.rel = "noopener";
22
+ document.body.appendChild(a);
23
+ a.click();
24
+ a.remove();
25
+ window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
26
+ }
27
+
28
+ /**
29
+ * Creates a native Google Slides deck in the user's Drive when their Google
30
+ * account is connected. Without a connection — or if Drive rejects the upload —
31
+ * the PPTX is downloaded instead so the user can import it by hand, and the
32
+ * reason is reported rather than swallowed.
33
+ */
34
+ export async function exportDeckToGoogleSlides(
35
+ deckTitle: string,
36
+ slides: GoogleSlidesExportSlide[],
37
+ aspectRatio?: AspectRatio,
38
+ ): Promise<GoogleSlidesExportResult> {
39
+ const { blob, filename } = await buildDeckPptxBlob(
40
+ deckTitle,
41
+ slides,
42
+ aspectRatio,
43
+ );
44
+
45
+ const form = new FormData();
46
+ form.append("file", blob, filename);
47
+ form.append("title", deckTitle);
48
+
49
+ const res = await fetch(`${appBasePath()}/api/exports/google-slides`, {
50
+ method: "POST",
51
+ body: form,
52
+ });
53
+
54
+ const payload = (await res.json().catch(() => null)) as {
55
+ url?: string;
56
+ error?: string;
57
+ } | null;
58
+
59
+ if (res.ok && payload?.url) return { url: payload.url };
60
+
61
+ triggerBlobDownload(blob, filename);
62
+ return {
63
+ url: null,
64
+ downloaded: true,
65
+ reason: payload?.error ?? `HTTP ${res.status}`,
66
+ };
67
+ }
@@ -313,11 +313,11 @@ function widenNoWrapTextElements(root: HTMLElement) {
313
313
  }
314
314
  }
315
315
 
316
- export async function exportDeckAsPptx(
316
+ export async function buildDeckPptxBlob(
317
317
  deckTitle: string,
318
318
  slides: PptxExportSlide[],
319
319
  aspectRatio?: AspectRatio,
320
- ): Promise<void> {
320
+ ): Promise<{ blob: Blob; filename: string }> {
321
321
  const { exportToPptx } = await importExportModule(
322
322
  () => import("dom-to-pptx"),
323
323
  );
@@ -359,10 +359,23 @@ export async function exportDeckAsPptx(
359
359
  );
360
360
 
361
361
  const blob = await addSpeakerNotesToPptxBlob(initialBlob, slides);
362
- triggerBlobDownload(blob, safePptxName(deckTitle));
362
+ return { blob, filename: safePptxName(deckTitle) };
363
363
  } finally {
364
364
  for (const clone of exportClones) {
365
365
  clone.cleanup();
366
366
  }
367
367
  }
368
368
  }
369
+
370
+ export async function exportDeckAsPptx(
371
+ deckTitle: string,
372
+ slides: PptxExportSlide[],
373
+ aspectRatio?: AspectRatio,
374
+ ): Promise<void> {
375
+ const { blob, filename } = await buildDeckPptxBlob(
376
+ deckTitle,
377
+ slides,
378
+ aspectRatio,
379
+ );
380
+ triggerBlobDownload(blob, filename);
381
+ }
@@ -62,6 +62,7 @@ import {
62
62
  } from "@/hooks/use-slide-comments";
63
63
  import type { AspectRatio } from "@/lib/aspect-ratios";
64
64
  import { getPreset } from "@/lib/design-systems";
65
+ import { exportDeckToGoogleSlides } from "@/lib/export-google-slides-client";
65
66
  import { exportDeckAsPdf } from "@/lib/export-pdf-client";
66
67
  import { exportDeckAsPptx } from "@/lib/export-pptx-client";
67
68
  import {
@@ -854,6 +855,16 @@ export default function DeckEditor() {
854
855
  }
855
856
  await exportDeckAsPptx(deck.title, slides, deck.aspectRatio);
856
857
  }}
858
+ onExportGoogleSlides={async () => {
859
+ const slides = deck.slides.map((s) => ({
860
+ id: s.id,
861
+ notes: s.notes,
862
+ }));
863
+ if (slides.length === 0) {
864
+ throw new Error(t("deckEditor.deckHasNoSlides"));
865
+ }
866
+ return exportDeckToGoogleSlides(deck.title, slides, deck.aspectRatio);
867
+ }}
857
868
  aspectRatio={deck.aspectRatio}
858
869
  designSystemTitle={designSystemTitle}
859
870
  onSetAspectRatio={(ratio: AspectRatio) => {
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import { useParams, Navigate, useSearchParams } from "react-router";
4
4
 
5
5
  import PresentationView from "@/components/presentation/PresentationView";
6
+ import PresenterView from "@/components/presentation/PresenterView";
6
7
  import { useDecks } from "@/context/DeckContext";
7
8
  import type { Deck } from "@/context/DeckContext";
8
9
 
@@ -45,10 +46,14 @@ export default function Presentation() {
45
46
  };
46
47
  }, [contextDeck, id, loading]);
47
48
 
48
- if (loading || fallbackState === "loading") {
49
+ if (!id) return <Navigate to="/" replace />;
50
+ // "Not fetched yet" is not "not found": on a cold load of this URL the deck
51
+ // context is empty and the fallback fetch has not run, so redirecting on a
52
+ // falsy deck bounced every direct/presenter/share link back to the index.
53
+ if (!deck && fallbackState !== "missing") {
49
54
  return <div className="h-screen bg-black" />;
50
55
  }
51
- if (!deck || !id || fallbackState === "missing") {
56
+ if (!deck) {
52
57
  return <Navigate to="/" replace />;
53
58
  }
54
59
 
@@ -58,8 +63,11 @@ export default function Presentation() {
58
63
  ? Math.max(0, parsedSlide - 1)
59
64
  : 0;
60
65
 
66
+ const View =
67
+ searchParams.get("presenter") === "1" ? PresenterView : PresentationView;
68
+
61
69
  return (
62
- <PresentationView
70
+ <View
63
71
  slides={Array.isArray(deck.slides) ? deck.slides : []}
64
72
  deckId={id}
65
73
  startIndex={startSlide}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-28
4
+ ---
5
+
6
+ Export to Google Slides now creates the deck directly in your Google Drive when your Google account is connected, instead of only downloading a PPTX to import by hand.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-28
4
+ ---
5
+
6
+ Opening a presentation link directly no longer bounces back to the deck list.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-28
4
+ ---
5
+
6
+ Presenter view: open a second window while presenting to see speaker notes, the next slide, and a timer, with both windows kept in sync.
@@ -0,0 +1,90 @@
1
+ import { getSession, runWithRequestContext } from "@agent-native/core/server";
2
+ import {
3
+ defineEventHandler,
4
+ readMultipartFormData,
5
+ setResponseStatus,
6
+ } from "h3";
7
+
8
+ import { getGoogleDocsAccessToken } from "../../../lib/google-docs-oauth.js";
9
+
10
+ const PPTX_CONTENT_TYPE =
11
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation";
12
+ const GOOGLE_SLIDES_MIME = "application/vnd.google-apps.presentation";
13
+ const UPLOAD_URL =
14
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink";
15
+
16
+ /**
17
+ * Uploads a browser-generated PPTX into the user's Drive, letting Drive convert
18
+ * it to a native Google Slides deck. The PPTX comes from the client because the
19
+ * browser export renders the real slide DOM — the server-side pptxgenjs export
20
+ * is a lower-fidelity fallback and would ship a visibly worse deck to Google.
21
+ */
22
+ export default defineEventHandler(async (event) => {
23
+ const session = await getSession(event).catch(() => null);
24
+ if (!session?.email) {
25
+ setResponseStatus(event, 401);
26
+ return { error: "Unauthorized" };
27
+ }
28
+
29
+ const parts = (await readMultipartFormData(event)) ?? [];
30
+ const file = parts.find((part) => part.name === "file");
31
+ const titlePart = parts.find((part) => part.name === "title");
32
+ const title = titlePart
33
+ ? new TextDecoder().decode(titlePart.data).trim() || "Untitled deck"
34
+ : "Untitled deck";
35
+
36
+ if (!file?.data?.length) {
37
+ setResponseStatus(event, 400);
38
+ return { error: "file required" };
39
+ }
40
+
41
+ // Same request context the actions run in — Google's client credentials can
42
+ // be org-scoped vault secrets, and resolving them without the org reports the
43
+ // integration as unconfigured.
44
+ const account = await runWithRequestContext(
45
+ { userEmail: session.email, orgId: session.orgId },
46
+ () => getGoogleDocsAccessToken(session.email),
47
+ );
48
+ if (!account) {
49
+ setResponseStatus(event, 409);
50
+ return {
51
+ error: "No connected Google account.",
52
+ code: "google-not-connected",
53
+ };
54
+ }
55
+
56
+ const boundary = `an-slides-${Math.random().toString(36).slice(2)}`;
57
+ const body = new Blob([
58
+ `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n`,
59
+ JSON.stringify({ name: title, mimeType: GOOGLE_SLIDES_MIME }),
60
+ `\r\n--${boundary}\r\nContent-Type: ${PPTX_CONTENT_TYPE}\r\n\r\n`,
61
+ new Uint8Array(file.data),
62
+ `\r\n--${boundary}--`,
63
+ ]);
64
+
65
+ const response = await fetch(UPLOAD_URL, {
66
+ method: "POST",
67
+ headers: {
68
+ Authorization: `Bearer ${account.accessToken}`,
69
+ "Content-Type": `multipart/related; boundary=${boundary}`,
70
+ },
71
+ body,
72
+ });
73
+
74
+ const result = (await response.json().catch(() => null)) as {
75
+ id?: string;
76
+ webViewLink?: string;
77
+ error?: { message?: string };
78
+ } | null;
79
+
80
+ if (!response.ok || !result?.webViewLink) {
81
+ setResponseStatus(event, 502);
82
+ return {
83
+ error:
84
+ result?.error?.message ??
85
+ `Google Drive returned HTTP ${response.status} while creating the deck.`,
86
+ };
87
+ }
88
+
89
+ return { url: result.webViewLink, accountEmail: account.accountEmail };
90
+ });
@@ -1,5 +1,11 @@
1
1
  # @agent-native/toolkit
2
2
 
3
+ ## 0.10.9
4
+
5
+ ### Patch Changes
6
+
7
+ - cbc6936: Show only the connect actions in the composer model picker when no LLM provider is configured, instead of a list of unpickable "needs API key" models, and surface Builder connect failures instead of leaving the "Connect Builder.io" button looking dead when the popup is blocked.
8
+
3
9
  ## 0.10.8
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/toolkit",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "Reusable app-building UI and helpers for Agent-Native apps.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {