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 {