@agent-native/core 0.162.0 → 0.163.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 (132) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
  3. package/corpus/templates/analytics/server/plugins/collab.ts +1 -0
  4. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
  5. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
  6. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
  7. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  8. package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
  9. package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
  10. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
  11. package/corpus/templates/content/actions/create-content-database.ts +1 -1
  12. package/corpus/templates/content/actions/create-document.ts +1 -1
  13. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
  14. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
  16. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
  17. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
  18. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
  19. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
  20. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
  21. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
  22. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
  23. package/corpus/templates/slides/DEVELOPING.md +17 -0
  24. package/corpus/templates/slides/actions/get-deck-access-status.ts +67 -0
  25. package/corpus/templates/slides/actions/request-deck-access.ts +569 -0
  26. package/corpus/templates/slides/agent-native.json +3 -0
  27. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
  28. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
  29. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
  30. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  31. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
  32. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -0
  33. package/corpus/templates/slides/app/hooks/use-deck-access.ts +51 -0
  34. package/corpus/templates/slides/app/i18n/en-US.ts +12 -0
  35. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +7 -0
  36. package/corpus/templates/slides/app/pages/DeckEditor.tsx +173 -33
  37. package/corpus/templates/slides/changelog/2026-08-18-private-deck-links-now-explain-access-and-let-viewers-notify.md +6 -0
  38. package/corpus/templates/slides/server/db/schema.ts +10 -0
  39. package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
  40. package/corpus/templates/slides/server/plugins/db.ts +17 -0
  41. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
  42. package/dist/app-config/schema.d.ts +1 -0
  43. package/dist/app-config/workspace.d.ts +1 -0
  44. package/dist/app-config/workspace.js +8 -0
  45. package/dist/chat-threads/schema.d.ts +2 -2
  46. package/dist/client/analytics.d.ts +1 -0
  47. package/dist/client/analytics.js +19 -5
  48. package/dist/client/org/TeamPage.js +15 -2
  49. package/dist/client/org/hooks.d.ts +2 -0
  50. package/dist/client/org/hooks.js +12 -0
  51. package/dist/client/org/index.d.ts +2 -2
  52. package/dist/client/org/index.js +1 -1
  53. package/dist/client/sharing/ShareButton.js +43 -18
  54. package/dist/client/sharing/useShareButtonController.d.ts +12 -2
  55. package/dist/client/sharing/useShareButtonController.js +38 -8
  56. package/dist/collab/awareness.d.ts +2 -2
  57. package/dist/collab/routes.d.ts +1 -1
  58. package/dist/collab/storage.d.ts +2 -0
  59. package/dist/collab/storage.js +7 -0
  60. package/dist/config.js +11 -1
  61. package/dist/dashboard-storage/schema.d.ts +2 -2
  62. package/dist/data-programs/schema.d.ts +2 -2
  63. package/dist/deploy/build.d.ts +1 -1
  64. package/dist/deploy/build.js +89 -12
  65. package/dist/extensions/schema.d.ts +2 -2
  66. package/dist/extensions/store.js +3 -0
  67. package/dist/localization/core-messages/ar-SA.js +1 -0
  68. package/dist/localization/core-messages/de-DE.js +1 -0
  69. package/dist/localization/core-messages/en-US.d.ts +1 -0
  70. package/dist/localization/core-messages/en-US.js +1 -0
  71. package/dist/localization/core-messages/es-ES.js +1 -0
  72. package/dist/localization/core-messages/fr-FR.js +1 -0
  73. package/dist/localization/core-messages/hi-IN.js +1 -0
  74. package/dist/localization/core-messages/ja-JP.js +1 -0
  75. package/dist/localization/core-messages/ko-KR.js +1 -0
  76. package/dist/localization/core-messages/pt-BR.js +1 -0
  77. package/dist/localization/core-messages/zh-CN.js +1 -0
  78. package/dist/localization/core-messages/zh-TW.js +1 -0
  79. package/dist/localization/core-messages.d.ts +1 -0
  80. package/dist/localization/default-messages.d.ts +4 -0
  81. package/dist/localization/default-messages.js +4 -0
  82. package/dist/observability/routes.d.ts +1 -1
  83. package/dist/observability/tracking-identity.js +2 -0
  84. package/dist/org/handlers.d.ts +6 -1
  85. package/dist/org/handlers.js +43 -0
  86. package/dist/org/index.d.ts +3 -3
  87. package/dist/org/index.js +2 -2
  88. package/dist/org/migrations.js +33 -0
  89. package/dist/org/plugin.js +10 -1
  90. package/dist/org/schema.d.ts +315 -0
  91. package/dist/org/schema.js +20 -1
  92. package/dist/org/types.d.ts +3 -0
  93. package/dist/org/workspace-app-access.d.ts +10 -0
  94. package/dist/org/workspace-app-access.js +179 -0
  95. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  96. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  97. package/dist/secrets/routes.d.ts +9 -9
  98. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  99. package/dist/server/auth.js +14 -1
  100. package/dist/server/collab-plugin.d.ts +6 -0
  101. package/dist/server/collab-plugin.js +27 -6
  102. package/dist/server/deploy-environment.d.ts +1 -1
  103. package/dist/server/deploy-environment.js +40 -2
  104. package/dist/server/poll.js +6 -0
  105. package/dist/server/realtime-token.d.ts +1 -1
  106. package/dist/server/sentry-config.js +8 -4
  107. package/dist/server/sentry.js +4 -0
  108. package/dist/sharing/access.d.ts +2 -2
  109. package/dist/sharing/access.js +56 -3
  110. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  111. package/dist/sharing/actions/list-resource-shares.js +26 -1
  112. package/dist/sharing/actions/share-resource.d.ts +1 -1
  113. package/dist/sharing/actions/share-resource.js +23 -4
  114. package/dist/sharing/actions/unshare-resource.d.ts +1 -1
  115. package/dist/sharing/actions/unshare-resource.js +1 -1
  116. package/dist/sharing/registry.d.ts +12 -0
  117. package/dist/sharing/schema.d.ts +4 -4
  118. package/dist/sharing/schema.js +2 -2
  119. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  120. package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  121. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
  122. package/dist/tracking/registry.js +8 -6
  123. package/dist/vite/client.js +5 -0
  124. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
  125. package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
  126. package/dist/workspace-connections/groups.d.ts +2 -0
  127. package/dist/workspace-connections/groups.js +16 -5
  128. package/docs/content/deployment.mdx +19 -5
  129. package/package.json +1 -1
  130. package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  131. package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  132. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
@@ -1,5 +1,6 @@
1
1
  import { AgentToggleButton } from "@agent-native/core/client/agent-chat";
2
2
  import { useT } from "@agent-native/core/client/i18n";
3
+ import { NotificationsBell } from "@agent-native/core/client/notifications";
3
4
  import { RunsTray } from "@agent-native/core/client/progress";
4
5
  import {
5
6
  useHeaderTitle,
@@ -68,6 +69,7 @@ export function Header() {
68
69
  </div>
69
70
  <div className="flex items-center gap-2 shrink-0">
70
71
  {actions}
72
+ <NotificationsBell pollMs={30_000} />
71
73
  <RunsTray pollMs={0} />
72
74
  <AgentToggleButton />
73
75
  </div>
@@ -0,0 +1,51 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
5
+ import { toast } from "sonner";
6
+
7
+ export type DeckAccessStatusResponse = {
8
+ exists: boolean;
9
+ hasAccess: boolean;
10
+ signedIn: boolean;
11
+ viewerEmail: string | null;
12
+ viewerName: string | null;
13
+ role: "owner" | "viewer" | "commenter" | "editor" | "admin" | null;
14
+ visibility: "private" | "org" | "public" | null;
15
+ };
16
+
17
+ export function useDeckAccessStatus(deckId?: string) {
18
+ return useActionQuery<DeckAccessStatusResponse>(
19
+ "get-deck-access-status",
20
+ { deckId: deckId ?? "" },
21
+ {
22
+ enabled: Boolean(deckId),
23
+ retry: false,
24
+ },
25
+ );
26
+ }
27
+
28
+ export type RequestDeckAccessResult = {
29
+ ok: true;
30
+ alreadyHasAccess: boolean;
31
+ notifiedOwner: boolean;
32
+ requestId?: string;
33
+ message: string;
34
+ };
35
+
36
+ function showActionError(message: string) {
37
+ return (error: Error) => {
38
+ toast.error(
39
+ error.message
40
+ ? error.message.replace(/^Action [\w-]+ failed:\s*/, "")
41
+ : message,
42
+ );
43
+ };
44
+ }
45
+
46
+ export function useRequestDeckAccess() {
47
+ return useActionMutation<RequestDeckAccessResult, { deckId: string }>(
48
+ "request-deck-access",
49
+ { onError: showActionError("Failed to request access") },
50
+ );
51
+ }
@@ -565,6 +565,18 @@ const messages = {
565
565
  "This link points to a team presentation. Join the team shown above and the deck will open here automatically.",
566
566
  deckUnavailableDescription:
567
567
  "This deck may have been removed, or your account does not have access to it.",
568
+ privateDeckTitle: "This deck is private",
569
+ privateDeckDescription:
570
+ "Ask the owner for access to open this deck. They’ll be notified when you send a request.",
571
+ signedInAs: "You're signed in as",
572
+ accessRequestSentDescription:
573
+ "The owner has been notified. You can open this deck once access is granted.",
574
+ accessRequestRecordedDescription:
575
+ "Your request has been recorded. You can open this deck once access is granted.",
576
+ requestAccessPending: "Sending request...",
577
+ accessRequestSent: "Request sent",
578
+ requestAccess: "Request access",
579
+ signInToRequestAccess: "Sign in to request access",
568
580
  backToDecks: "Back to Decks",
569
581
  tryAgain: "Try again",
570
582
  imageUploadFailed: "Image upload failed",
@@ -12,6 +12,7 @@ export function shouldShowDeckEditorSkeleton({
12
12
  accessCheckKey,
13
13
  checkedAccessKey,
14
14
  retrying,
15
+ privateDeckAccessConfirmed,
15
16
  }: {
16
17
  deckFound: boolean;
17
18
  decksLoading: boolean;
@@ -19,7 +20,13 @@ export function shouldShowDeckEditorSkeleton({
19
20
  accessCheckKey: string | null;
20
21
  checkedAccessKey: string | null;
21
22
  retrying: boolean;
23
+ privateDeckAccessConfirmed: boolean;
22
24
  }): boolean {
25
+ // A private deck intentionally returns 404 from the content action. The
26
+ // metadata-only access check is the source of truth for this state, so do
27
+ // not leave the user on the editor skeleton while that protected fetch
28
+ // settles.
29
+ if (privateDeckAccessConfirmed) return false;
23
30
  if (decksLoading) return true;
24
31
  if (deckFound || !accessCheckKey) return false;
25
32
  return orgLoading || checkedAccessKey !== accessCheckKey || retrying;
@@ -1,5 +1,8 @@
1
1
  import { useGuidedQuestionFlow } from "@agent-native/core/client/agent-chat";
2
- import { appBasePath } from "@agent-native/core/client/api-path";
2
+ import {
3
+ agentNativePath,
4
+ appBasePath,
5
+ } from "@agent-native/core/client/api-path";
3
6
  import {
4
7
  useCollaborativeDoc,
5
8
  emailToColor,
@@ -8,6 +11,7 @@ import {
8
11
  import { useSession } from "@agent-native/core/client/hooks";
9
12
  import { useT } from "@agent-native/core/client/i18n";
10
13
  import { useOrg } from "@agent-native/core/client/org";
14
+ import { buildSignInReturnHref } from "@agent-native/core/client/ui";
11
15
  import {
12
16
  DndContext,
13
17
  closestCenter,
@@ -17,9 +21,13 @@ import {
17
21
  DragEndEvent,
18
22
  } from "@dnd-kit/core";
19
23
  import {
24
+ IconAt,
20
25
  IconArrowLeft,
21
26
  IconLock,
27
+ IconLoader2,
28
+ IconLogin2,
22
29
  IconRefresh,
30
+ IconUserPlus,
23
31
  IconUsersGroup,
24
32
  } from "@tabler/icons-react";
25
33
  import { nanoid } from "nanoid";
@@ -67,6 +75,11 @@ import {
67
75
  useSaveState,
68
76
  } from "@/context/DeckContext";
69
77
  import { useAgentGenerating } from "@/hooks/use-agent-generating";
78
+ import {
79
+ useDeckAccessStatus,
80
+ useRequestDeckAccess,
81
+ type DeckAccessStatusResponse,
82
+ } from "@/hooks/use-deck-access";
70
83
  import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
71
84
  import { useDeckPresence } from "@/hooks/use-deck-presence";
72
85
  import { useDeckRole } from "@/hooks/use-deck-role";
@@ -109,37 +122,70 @@ import { shortcutLabel } from "@/lib/utils";
109
122
  type EditorSidePanel = "comments" | null;
110
123
 
111
124
  function MissingDeckAccessPane({
125
+ accessStatus,
126
+ accessStatusError,
127
+ accessStatusLoading,
112
128
  hasTeamJoinOption,
113
129
  orgLoading,
114
130
  orgError,
131
+ requestAccessPending,
132
+ accessRequestSent,
133
+ accessRequestNotified,
134
+ signedIn,
135
+ viewerEmail,
115
136
  refreshing,
137
+ onRequestAccess,
138
+ onSignIn,
116
139
  onRetry,
117
140
  onBack,
118
141
  }: {
142
+ accessStatus: DeckAccessStatusResponse | null;
143
+ accessStatusError: boolean;
144
+ accessStatusLoading: boolean;
119
145
  hasTeamJoinOption: boolean;
120
146
  orgLoading: boolean;
121
147
  orgError: boolean;
148
+ requestAccessPending: boolean;
149
+ accessRequestSent: boolean;
150
+ accessRequestNotified: boolean;
151
+ signedIn: boolean;
152
+ viewerEmail: string | null;
122
153
  refreshing: boolean;
154
+ onRequestAccess: () => void;
155
+ onSignIn: () => void;
123
156
  onRetry: () => void;
124
157
  onBack: () => void;
125
158
  }) {
126
159
  const t = useT();
160
+ const privateDeck = Boolean(
161
+ accessStatus?.exists &&
162
+ !accessStatus.hasAccess &&
163
+ accessStatus.visibility === "private",
164
+ );
165
+ const checkingAccess = !privateDeck && (accessStatusLoading || orgLoading);
166
+ const accessCheckFailed = accessStatusError || orgError;
127
167
  const Icon =
128
- hasTeamJoinOption || orgLoading || orgError ? IconUsersGroup : IconLock;
129
- const title = orgLoading
168
+ privateDeck || (!hasTeamJoinOption && !checkingAccess && !accessCheckFailed)
169
+ ? IconLock
170
+ : IconUsersGroup;
171
+ const title = checkingAccess
130
172
  ? t("deckEditor.lookingForDeck")
131
- : orgError
132
- ? t("deckEditor.teamAccessCheckFailed")
133
- : hasTeamJoinOption
134
- ? t("deckEditor.joinTeamToOpen")
135
- : t("deckEditor.deckUnavailable");
136
- const description = orgLoading
173
+ : privateDeck
174
+ ? t("deckEditor.privateDeckTitle")
175
+ : accessCheckFailed
176
+ ? t("deckEditor.teamAccessCheckFailed")
177
+ : hasTeamJoinOption
178
+ ? t("deckEditor.joinTeamToOpen")
179
+ : t("deckEditor.deckUnavailable");
180
+ const description = checkingAccess
137
181
  ? t("deckEditor.checkingSharedAccess")
138
- : orgError
139
- ? t("deckEditor.verifySharedAccessFailed")
140
- : hasTeamJoinOption
141
- ? t("deckEditor.joinTeamDescription")
142
- : t("deckEditor.deckUnavailableDescription");
182
+ : privateDeck
183
+ ? t("deckEditor.privateDeckDescription")
184
+ : accessCheckFailed
185
+ ? t("deckEditor.verifySharedAccessFailed")
186
+ : hasTeamJoinOption
187
+ ? t("deckEditor.joinTeamDescription")
188
+ : t("deckEditor.deckUnavailableDescription");
143
189
 
144
190
  return (
145
191
  <div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
@@ -153,21 +199,64 @@ function MissingDeckAccessPane({
153
199
  </div>
154
200
  </div>
155
201
  <p className="text-sm leading-6 text-muted-foreground">{description}</p>
156
- <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
157
- <Button type="button" variant="outline" onClick={onBack}>
158
- <IconArrowLeft className="size-4" />
159
- {t("deckEditor.backToDecks")}
160
- </Button>
161
- <Button
162
- type="button"
163
- onClick={onRetry}
164
- disabled={refreshing || orgLoading}
165
- >
166
- <IconRefresh
167
- className={refreshing ? "size-4 animate-spin" : "size-4"}
168
- />
169
- {t("deckEditor.tryAgain")}
170
- </Button>
202
+ {privateDeck && viewerEmail ? (
203
+ <div className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/35 px-3 py-2 text-sm">
204
+ <IconAt className="size-4 shrink-0 text-muted-foreground" />
205
+ <span className="min-w-0 truncate text-muted-foreground">
206
+ {t("deckEditor.signedInAs")}{" "}
207
+ <span className="font-medium text-foreground">{viewerEmail}</span>
208
+ </span>
209
+ </div>
210
+ ) : null}
211
+ {privateDeck && accessRequestSent ? (
212
+ <div className="mt-3 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300">
213
+ {t(
214
+ accessRequestNotified
215
+ ? "deckEditor.accessRequestSentDescription"
216
+ : "deckEditor.accessRequestRecordedDescription",
217
+ )}
218
+ </div>
219
+ ) : null}
220
+ <div className="mt-5 flex flex-col gap-2">
221
+ {privateDeck ? (
222
+ <Button
223
+ type="button"
224
+ onClick={signedIn ? onRequestAccess : onSignIn}
225
+ disabled={requestAccessPending || accessRequestSent}
226
+ >
227
+ {requestAccessPending ? (
228
+ <IconLoader2 className="size-4 animate-spin" />
229
+ ) : signedIn ? (
230
+ <IconUserPlus className="size-4" />
231
+ ) : (
232
+ <IconLogin2 className="size-4" />
233
+ )}
234
+ {requestAccessPending
235
+ ? t("deckEditor.requestAccessPending")
236
+ : accessRequestSent
237
+ ? t("deckEditor.accessRequestSent")
238
+ : signedIn
239
+ ? t("deckEditor.requestAccess")
240
+ : t("deckEditor.signInToRequestAccess")}
241
+ </Button>
242
+ ) : null}
243
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
244
+ <Button type="button" variant="outline" onClick={onBack}>
245
+ <IconArrowLeft className="size-4" />
246
+ {t("deckEditor.backToDecks")}
247
+ </Button>
248
+ <Button
249
+ type="button"
250
+ variant={privateDeck ? "ghost" : "default"}
251
+ onClick={onRetry}
252
+ disabled={refreshing || checkingAccess}
253
+ >
254
+ <IconRefresh
255
+ className={refreshing ? "size-4 animate-spin" : "size-4"}
256
+ />
257
+ {t("deckEditor.tryAgain")}
258
+ </Button>
259
+ </div>
171
260
  </div>
172
261
  </div>
173
262
  </div>
@@ -200,6 +289,7 @@ export default function DeckEditor() {
200
289
  const { id } = useParams<{ id: string }>();
201
290
  const navigate = useNavigate();
202
291
  const [searchParams, setSearchParams] = useSearchParams();
292
+ const { session, isLoading: sessionLoading } = useSession();
203
293
  const {
204
294
  getDeck,
205
295
  reloadDecks,
@@ -219,6 +309,8 @@ export default function DeckEditor() {
219
309
  loading,
220
310
  loadError,
221
311
  } = useDecks();
312
+ const deckAccessStatusQuery = useDeckAccessStatus(id);
313
+ const requestDeckAccessMutation = useRequestDeckAccess();
222
314
  const [activeSlideId, setActiveSlideId] = useState<string | null>(null);
223
315
  const [inlineEditActive, setInlineEditActive] = useState(false);
224
316
  const [addSlideGenerating, setAddSlideGenerating] = useState(false);
@@ -294,6 +386,10 @@ export default function DeckEditor() {
294
386
  const [wideContextToolbarSlot, setWideContextToolbarSlot] =
295
387
  useState<HTMLDivElement | null>(null);
296
388
  const [retryingMissingDeck, setRetryingMissingDeck] = useState(false);
389
+ const [accessRequestSentDeckId, setAccessRequestSentDeckId] = useState<
390
+ string | null
391
+ >(null);
392
+ const [accessRequestNotified, setAccessRequestNotified] = useState(false);
297
393
  const [checkedDeckAccessKey, setCheckedDeckAccessKey] = useState<
298
394
  string | null
299
395
  >(null);
@@ -349,6 +445,7 @@ export default function DeckEditor() {
349
445
  const uploadInputRef = useRef<HTMLInputElement>(null);
350
446
 
351
447
  const deck = getDeck(id || "");
448
+ const deckAccessStatus = deckAccessStatusQuery.data ?? null;
352
449
  const fitDims = getAspectRatioDims(deck?.aspectRatio);
353
450
  const currentDeckAccessKey = deckAccessCheckKey(id, org?.orgId);
354
451
  const hasTeamJoinOption =
@@ -511,6 +608,32 @@ export default function DeckEditor() {
511
608
  }
512
609
  }, [refetchOrg, reloadDecks]);
513
610
 
611
+ const openSignIn = useCallback(() => {
612
+ window.location.href = buildSignInReturnHref();
613
+ }, []);
614
+
615
+ const requestDeckAccess = useCallback(() => {
616
+ if (!id) return;
617
+ requestDeckAccessMutation.mutate(
618
+ { deckId: id },
619
+ {
620
+ onSuccess: (result) => {
621
+ setAccessRequestSentDeckId(id);
622
+ setAccessRequestNotified(result.notifiedOwner);
623
+ toast.success(result.message);
624
+ if (result.alreadyHasAccess) void reloadDecks();
625
+ },
626
+ },
627
+ );
628
+ }, [id, reloadDecks, requestDeckAccessMutation]);
629
+
630
+ useEffect(() => {
631
+ if (accessRequestSentDeckId && accessRequestSentDeckId !== id) {
632
+ setAccessRequestSentDeckId(null);
633
+ setAccessRequestNotified(false);
634
+ }
635
+ }, [accessRequestSentDeckId, id]);
636
+
514
637
  // The final generation write can race the last sync event. Pull the
515
638
  // authoritative open deck when the run settles so a stale canvas does not
516
639
  // require a browser refresh to reveal completed slides.
@@ -1204,7 +1327,6 @@ export default function DeckEditor() {
1204
1327
  useRef<typeof deck extends undefined ? null : any>(null);
1205
1328
 
1206
1329
  // Session for collab user identity
1207
- const { session } = useSession();
1208
1330
  const currentUser = session?.email
1209
1331
  ? {
1210
1332
  email: session.email,
@@ -1242,7 +1364,7 @@ export default function DeckEditor() {
1242
1364
  recentEdits: deckRecentEdits,
1243
1365
  awareness: deckPresenceAwareness,
1244
1366
  } = useDeckPresence({
1245
- deckId: id ?? null,
1367
+ deckId: deck ? (id ?? null) : null,
1246
1368
  activeSlideId: activeSlideId,
1247
1369
  user: currentUser,
1248
1370
  });
@@ -1254,7 +1376,10 @@ export default function DeckEditor() {
1254
1376
  const agentActive = generating || deckAgentActive || slideAgentActive;
1255
1377
 
1256
1378
  // Comments for the current slide (for badge count)
1257
- const currentSlideCommentsQuery = useSlideComments(id ?? null, activeSlideId);
1379
+ const currentSlideCommentsQuery = useSlideComments(
1380
+ deck ? (id ?? null) : null,
1381
+ activeSlideId,
1382
+ );
1258
1383
  const currentSlideThreads: CommentThread[] =
1259
1384
  currentSlideCommentsQuery.data ?? [];
1260
1385
  const unresolvedCommentCount = currentSlideThreads.filter(
@@ -1269,6 +1394,11 @@ export default function DeckEditor() {
1269
1394
  accessCheckKey: currentDeckAccessKey,
1270
1395
  checkedAccessKey: checkedDeckAccessKey,
1271
1396
  retrying: retryingMissingDeck,
1397
+ privateDeckAccessConfirmed: Boolean(
1398
+ deckAccessStatus?.exists &&
1399
+ !deckAccessStatus.hasAccess &&
1400
+ deckAccessStatus.visibility === "private",
1401
+ ),
1272
1402
  })
1273
1403
  ) {
1274
1404
  return <DeckEditorSkeleton label={t("deckEditor.lookingForDeck")} />;
@@ -1276,10 +1406,20 @@ export default function DeckEditor() {
1276
1406
  if (!deck || !id) {
1277
1407
  return (
1278
1408
  <MissingDeckAccessPane
1409
+ accessStatus={deckAccessStatus}
1410
+ accessStatusError={deckAccessStatusQuery.isError}
1411
+ accessStatusLoading={loading || deckAccessStatusQuery.isLoading}
1279
1412
  hasTeamJoinOption={hasTeamJoinOption}
1280
1413
  orgLoading={orgLoading}
1281
1414
  orgError={orgError || loadError}
1415
+ requestAccessPending={requestDeckAccessMutation.isPending}
1416
+ accessRequestSent={accessRequestSentDeckId === id}
1417
+ accessRequestNotified={accessRequestNotified}
1418
+ signedIn={Boolean(session) && !sessionLoading}
1419
+ viewerEmail={session?.email ?? deckAccessStatus?.viewerEmail ?? null}
1282
1420
  refreshing={retryingMissingDeck}
1421
+ onRequestAccess={requestDeckAccess}
1422
+ onSignIn={openSignIn}
1283
1423
  onRetry={() => void retryOpenDeck()}
1284
1424
  onBack={() => navigate("/")}
1285
1425
  />
@@ -1629,7 +1769,7 @@ export default function DeckEditor() {
1629
1769
  slideAgentActive ||
1630
1770
  (deckAgentActive && agentSlideId === currentSlide.id) ||
1631
1771
  (isNewDeckGenerating &&
1632
- currentSlide.id === deck.slides.at(-1)?.id)
1772
+ currentSlide.id === deck.slides[deck.slides.length - 1]?.id)
1633
1773
  }
1634
1774
  recentEdits={deckRecentEdits}
1635
1775
  onComment={(quotedText) => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Private deck links now explain access and let viewers notify the owner
@@ -83,3 +83,13 @@ export const slideComments = table("slide_comments", {
83
83
  createdAt: text("created_at").notNull().default(now()),
84
84
  updatedAt: text("updated_at").notNull().default(now()),
85
85
  });
86
+
87
+ export const deckEvents = table("deck_events", {
88
+ id: text("id").primaryKey(),
89
+ deckId: text("deck_id").notNull(),
90
+ type: text("type").notNull(),
91
+ message: text("message").notNull(),
92
+ payload: text("payload"),
93
+ createdBy: text("created_by").notNull().default("human"),
94
+ createdAt: text("created_at").notNull().default(now()),
95
+ });
@@ -21,6 +21,7 @@ export interface SlidesRequestAuthContext {
21
21
  */
22
22
  export class SlidesSessionLookupError extends Error {
23
23
  readonly statusCode = 503;
24
+ readonly cause: unknown;
24
25
  constructor(cause: unknown) {
25
26
  super("Could not verify your session. Please try again.");
26
27
  this.name = "SlidesSessionLookupError";
@@ -235,6 +235,23 @@ export const runSlidesMigrations = runMigrations(
235
235
  name: "slides-share-design-system-snapshot",
236
236
  sql: `ALTER TABLE deck_share_links ADD COLUMN IF NOT EXISTS design_system_data TEXT`,
237
237
  },
238
+ // v22: durable access-request events for private deck links. The request
239
+ // action also sends the owner an email when outbound email is configured,
240
+ // but the event remains the source of truth when it is not.
241
+ {
242
+ version: 22,
243
+ name: "slides-deck-access-requests",
244
+ sql: `CREATE TABLE IF NOT EXISTS deck_events (
245
+ id TEXT PRIMARY KEY,
246
+ deck_id TEXT NOT NULL,
247
+ type TEXT NOT NULL,
248
+ message TEXT NOT NULL,
249
+ payload TEXT,
250
+ created_by TEXT NOT NULL DEFAULT 'human',
251
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
252
+ );
253
+ CREATE INDEX IF NOT EXISTS deck_events_deck_created_idx ON deck_events (deck_id, created_at)`,
254
+ },
238
255
  ],
239
256
  { table: "slides_migrations" },
240
257
  );
@@ -18,6 +18,11 @@ Any resource a user **creates** (dashboards, documents, forms, decks, compositio
18
18
 
19
19
  This is the framework-level primitive. Every ownable resource gets it for free — same API, same UI, same skill.
20
20
 
21
+ Workspace apps are the intentional organization-scoped exception: their default
22
+ visibility is `org`, configurable by an organization owner/admin. The creator is
23
+ stored as the app owner, and the creator or an organization owner/admin can
24
+ change the app's visibility or manage its share grants.
25
+
21
26
  ## Concepts
22
27
 
23
28
  ### Three visibility levels
@@ -26,7 +31,8 @@ This is the framework-level primitive. Every ownable resource gets it for free
26
31
  - **`org`** — owner + explicit grants + anyone in the same org (read-only).
27
32
  - **`public`** — owner + explicit grants + **anyone with the link** (read-only). Public docs do NOT appear in other users' list/sidebar/search results — `accessFilter` omits them by default. They're reachable by id (`resolveAccess` admits them) so direct links and SSR routes like `/p/:id` keep working. If a list endpoint legitimately needs cross-user public discovery (a template gallery, etc.), pass `accessFilter(table, shares, ctx, minRole, { includePublic: true })`.
28
33
 
29
- Visibility is coarse. Explicit share grants are fine-grained (per user or per org).
34
+ Visibility is coarse. Explicit share grants are fine-grained (per user, reusable
35
+ organization group, or per org).
30
36
 
31
37
  ### Roles on a share grant
32
38
 
@@ -101,6 +107,7 @@ registerShareableResource({
101
107
  // ...
102
108
  allowPublic: false, // hides "Public" in the share dialog and rejects it server-side
103
109
  requireOrgMemberForUserShares: true, // user shares must target an org member or pending invitee
110
+ supportsGroupShares: true, // enable reusable organization groups for this resource
104
111
  });
105
112
  ```
106
113
 
@@ -111,6 +118,21 @@ Use both for resources that execute code or expose privileged data with the *vie
111
118
 
112
119
  Defaults match historical behaviour: `allowPublic: true`, `requireOrgMemberForUserShares: false`. Resources that don't set the flags work as before.
113
120
 
121
+ ### Reusable organization groups
122
+
123
+ The workspace organization model stores reusable groups in
124
+ `workspace_user_groups`. Organization owners/admins manage the groups and their
125
+ members through the existing group actions. Set `supportsGroupShares: true` on a
126
+ resource registration to enable `principalType: "group"` in the generic share
127
+ actions and the shared popover's autocomplete. Group ids are always checked
128
+ against the resource's organization, and membership is evaluated at access time
129
+ so group changes apply to existing shares.
130
+
131
+ Workspace apps opt into this flag and use the organization default visibility
132
+ setting. The app creator is the durable `owner_email`, while organization
133
+ owners/admins receive admin access through the registration's
134
+ `canManageAccess` callback.
135
+
114
136
  ## Filter list/read queries
115
137
 
116
138
  ```ts
@@ -197,6 +219,9 @@ All app share popovers should use the same compact surface contract:
197
219
  - Add `ShareAgentsSection` only when the resource has a real agent-readable
198
220
  link or prompt. Keep it collapsed by default and supply domain-specific
199
221
  content through the shared section shell.
222
+ - When `supportsGroupShares` is enabled, keep the existing people input as the
223
+ only entry point and include organization groups in its autocomplete. Do not
224
+ add a separate group-management surface to each resource.
200
225
  - App-specific tabs or controls may remain when they represent a real domain
201
226
  action (for example, an embed-code editor), but they should retain the same
202
227
  trigger, copy-row, access, and spacing language.
@@ -211,7 +236,7 @@ The framework auto-mounts these actions in every template — no per-template bo
211
236
 
212
237
  | Action | Args | Purpose |
213
238
  | -------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------- |
214
- | `share-resource` | `resourceType, resourceId, principalType, principalId, role, notify?, resourceUrl?, message?` | Grant a user or org access. `notify` defaults to true for individual user shares; `resourceUrl` can provide the direct app link and `message` an optional short note for the notification email. |
239
+ | `share-resource` | `resourceType, resourceId, principalType, principalId, role, notify?, resourceUrl?, message?` | Grant a user, group, or org access. `notify` defaults to true for individual user shares; `resourceUrl` can provide the direct app link and `message` an optional short note for the notification email. |
215
240
  | `unshare-resource` | `resourceType, resourceId, principalType, principalId` | Revoke access. |
216
241
  | `list-resource-shares` | `resourceType, resourceId` | Current visibility + all share grants. |
217
242
  | `set-resource-visibility` | `resourceType, resourceId, visibility` | Change to `private` / `org` / `public`. |
@@ -43,6 +43,7 @@ export declare const appConfigSchema: z.ZodObject<{
43
43
  isWorkspace: z.ZodOptional<z.ZodBoolean>;
44
44
  appsJson: z.ZodOptional<z.ZodString>;
45
45
  gatewayUrl: z.ZodOptional<z.ZodString>;
46
+ orgDirectoryUrl: z.ZodOptional<z.ZodString>;
46
47
  oauthOrigin: z.ZodOptional<z.ZodString>;
47
48
  }, z.core.$strip>>;
48
49
  }, z.core.$strip>;
@@ -19,5 +19,6 @@ export declare const workspaceConfig: z.ZodObject<{
19
19
  isWorkspace: z.ZodOptional<z.ZodBoolean>;
20
20
  appsJson: z.ZodOptional<z.ZodString>;
21
21
  gatewayUrl: z.ZodOptional<z.ZodString>;
22
+ orgDirectoryUrl: z.ZodOptional<z.ZodString>;
22
23
  oauthOrigin: z.ZodOptional<z.ZodString>;
23
24
  }, z.core.$strip>;
@@ -42,6 +42,14 @@ export const workspaceConfig = z.object({
42
42
  env: ["WORKSPACE_GATEWAY_URL", "VITE_WORKSPACE_GATEWAY_URL"],
43
43
  doc: "URL of the workspace gateway fronting this app.",
44
44
  }),
45
+ orgDirectoryUrl: z
46
+ .string()
47
+ .min(1)
48
+ .optional()
49
+ .meta({
50
+ env: ["AGENT_NATIVE_ORG_DIRECTORY_URL"],
51
+ doc: "URL of the authoritative organization Dispatch directory.",
52
+ }),
45
53
  oauthOrigin: z
46
54
  .string()
47
55
  .min(1)
@@ -403,14 +403,14 @@ export declare const chatThreadShares: import("drizzle-orm/sqlite-core").SQLiteT
403
403
  tableName: string;
404
404
  dataType: "string";
405
405
  columnType: "SQLiteText";
406
- data: "org" | "user";
406
+ data: "group" | "org" | "user";
407
407
  driverParam: string;
408
408
  notNull: true;
409
409
  hasDefault: false;
410
410
  isPrimaryKey: false;
411
411
  isAutoincrement: false;
412
412
  hasRuntimeDefault: false;
413
- enumValues: ["user", "org"];
413
+ enumValues: ["user", "group", "org"];
414
414
  baseColumn: never;
415
415
  identity: undefined;
416
416
  generated: undefined;
@@ -21,6 +21,7 @@ declare global {
21
21
  workspaceRuntime?: boolean;
22
22
  sentryDsn?: string;
23
23
  sentryEnvironment?: string;
24
+ deploymentEnvironment?: string;
24
25
  /**
25
26
  * Public PostHog project key + host. Publishable and identical for every
26
27
  * visitor, so it ships inside the CDN-cached SSR shell alongside the