diff --git a/packages/app-shell/src/console/ai/AiChatPage.tsx b/packages/app-shell/src/console/ai/AiChatPage.tsx index 62df48b46..6e2444dac 100644 --- a/packages/app-shell/src/console/ai/AiChatPage.tsx +++ b/packages/app-shell/src/console/ai/AiChatPage.tsx @@ -495,6 +495,23 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro [apiBase], ); + // Public share-link landing base. SharedRecordPage lives UNDER the console + // SPA basename (e.g. `/_console/s/:token`), so the ShareDialog default of + // `${origin}/s/:token` 404s for recipients. Derive the base from the SPA's + // BASE_URL so the copyable link points at the actually-served route. + const publicShareBase = useMemo(() => { + if (typeof window === 'undefined' || typeof document === 'undefined') return undefined; + // Mirror the console's own basename resolution (App.tsx resolveBasename): + // the published SPA uses a relative Vite base, so the mount path is carried + // by the injected `` tag, NOT import.meta.env.BASE_URL. + let base = ''; + try { + const href = document.querySelector('base')?.getAttribute('href'); + if (href) base = new URL(href, window.location.origin).pathname.replace(/\/+$/, ''); + } catch { /* no → root-mounted SPA */ } + return `${window.location.origin}${base}/s`; + }, []); + // New-conversation race guard. On an IN-SPA `/ai?new=1` navigation the // URL-mirroring effect below fires in the SAME commit as the hook's effect, // with this render's (stale) `conversationId` still in its closure — the @@ -630,6 +647,7 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro recordId={conversationId} recordLabel="this conversation" apiBase={restApiBase} + publicBaseUrl={publicShareBase} /> )}
diff --git a/packages/components/src/share/ShareDialog.tsx b/packages/components/src/share/ShareDialog.tsx index 4144ac565..60c8f3673 100644 --- a/packages/components/src/share/ShareDialog.tsx +++ b/packages/components/src/share/ShareDialog.tsx @@ -41,7 +41,7 @@ import { Badge } from '../ui/badge'; import { cn } from '../lib/utils'; export type ShareLinkPermission = 'view' | 'comment' | 'edit'; -export type ShareLinkAudience = 'link_only' | 'signed_in' | 'workspace' | 'email_allowlist'; +export type ShareLinkAudience = 'public' | 'link_only' | 'signed_in' | 'email'; export interface ShareLink { id: string; @@ -106,11 +106,14 @@ const EXPIRY_OPTIONS: { label: string; value: number | null }[] = [ { label: 'Never', value: null }, ]; +// Audience values match the framework's ShareLinkAudience contract +// (@objectstack/spec). Only the two that need no extra input are offered; +// 'email' (allowlist) / 'public' require UI not yet built. The target +// object's `publicSharing.allowedAudiences` further constrains this — the +// server returns 422 AUDIENCE_NOT_ALLOWED if a value is not permitted. const AUDIENCE_OPTIONS: { value: ShareLinkAudience; label: string; help: string }[] = [ { value: 'link_only', label: 'Anyone with the link', help: 'No sign-in required' }, { value: 'signed_in', label: 'Signed-in users', help: 'Must be logged in to view' }, - { value: 'workspace', label: 'Workspace members', help: 'Same tenant only' }, - { value: 'email_allowlist', label: 'Specific emails', help: 'Restricted to listed addresses' }, ]; function buildPublicUrl(base: string | undefined, token: string): string {