@object-ui/app-shell 7.2.0 → 7.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 7.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 17ae00c: feat(studio): remove the "Local / Custom" stopgap scope from the package selector (ADR-0070 D5)
8
+
9
+ The package-scope selector no longer offers a synthetic "Local / Custom (this
10
+ env)" entry (the `package_id = null` / `sys_metadata` orphan bucket from
11
+ objectui#1946). That was a deliberate stopgap; ADR-0070 makes every
12
+ runtime-authored item live in a writable **base**, the kernel rejects orphan
13
+ creates (`writable_package_required`), and legacy orphans are adopted into a
14
+ base via "Adopt loose items". With no authoring path producing orphans, the
15
+ bucket has no reason to exist.
16
+
17
+ - `buildPackageScopeOptions` now returns only writable bases (drops the appended
18
+ sentinel); `isLocalScope` / `LOCAL_PACKAGE_ID` / `writableBaseOptions` and the
19
+ inline `LOCAL_SCOPE_ID` in `ContextSelectors` are removed.
20
+ - The create-flow and list/home scope filters simplify accordingly (a real base
21
+ is always the active scope; never the null/local sentinel).
22
+ - Read-side `sys_metadata` provenance handling (classifying a row as
23
+ runtime-authored, artifact detection in the editor) is unchanged — the kernel
24
+ still keeps `null` as a legacy read tag.
25
+
26
+ Closes the D5 tail of #2278 (the migration tooling it depended on already
27
+ shipped).
28
+
29
+ - Updated dependencies [788dbf9]
30
+ - @object-ui/fields@7.3.0
31
+ - @object-ui/types@7.3.0
32
+ - @object-ui/core@7.3.0
33
+ - @object-ui/i18n@7.3.0
34
+ - @object-ui/react@7.3.0
35
+ - @object-ui/components@7.3.0
36
+ - @object-ui/layout@7.3.0
37
+ - @object-ui/data-objectstack@7.3.0
38
+ - @object-ui/auth@7.3.0
39
+ - @object-ui/permissions@7.3.0
40
+ - @object-ui/plugin-editor@7.3.0
41
+ - @object-ui/collaboration@7.3.0
42
+ - @object-ui/providers@7.3.0
43
+
3
44
  ## 7.2.0
4
45
 
5
46
  ### Minor Changes
@@ -27,7 +27,7 @@ const VARIANTS = {
27
27
  icon: ShieldAlert,
28
28
  short: 'Identity',
29
29
  title: 'Managed by the identity provider',
30
- body: (display) => `This object's schema is owned by ${display}. Direct edits bypass password hashing, session validation, two-factor checks, and audit hooks. Use the dedicated identity workflows instead (Invite User, Reset Password, Revoke Session, Rotate Key, …).`,
30
+ body: (display) => `This object's schema is owned by ${display}. Direct edits bypass password hashing, session validation, two-factor checks, and audit hooks. Manage these records through your authentication provider's sign-in, invitation, and security flows instead.`,
31
31
  tone: 'border-amber-300/60 bg-amber-50 text-amber-900 hover:bg-amber-100 dark:border-amber-500/40 dark:bg-amber-950/40 dark:text-amber-100',
32
32
  },
33
33
  };
@@ -18,8 +18,8 @@ import { useAuth } from '@object-ui/auth';
18
18
  import { useObjectTranslation } from '@object-ui/i18n';
19
19
  import { toast } from 'sonner';
20
20
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
21
- import { PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react';
22
- import { ChatbotEnhanced, useAgents, useObjectChat, useHitlInChat, resolveDefaultAgentName, PLATFORM_DEFAULT_AGENT, agentRouteName, resolveAgentParam, isBuiltinAgentName, isBuildAgent, isAskAgent, publishHealthFromResponse, detectDraftResult, detectProposedPlan, buildProgressFromDraftReview, } from '@object-ui/plugin-chatbot';
21
+ import { Bug, PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react';
22
+ import { ChatbotEnhanced, useAgents, useObjectChat, useAiModels, useHitlInChat, resolveDefaultAgentName, PLATFORM_DEFAULT_AGENT, agentRouteName, resolveAgentParam, isBuiltinAgentName, isBuildAgent, isAskAgent, publishHealthFromResponse, detectDraftResult, detectProposedPlan, buildProgressFromDraftReview, } from '@object-ui/plugin-chatbot';
23
23
  import { AppHeader } from '../../layout/AppHeader';
24
24
  import { fetchPendingDraftCount } from '../../preview/draftStatus';
25
25
  import { emitMetadataRefresh } from '../../assistant/assistantBus';
@@ -30,6 +30,7 @@ import { fetchConversation, sanitizeChatMessagesForCache, useChatConversation, w
30
30
  import { useReconcileOnError } from '../../hooks/useReconcileOnError';
31
31
  import { ConversationsSidebar } from './ConversationsSidebar';
32
32
  import { LiveCanvas } from './LiveCanvas';
33
+ import { BuildDebugDrawer } from './BuildDebugDrawer';
33
34
  const DEFAULT_AI_PATH = '/api/v1/ai';
34
35
  function partString(part, key) {
35
36
  const value = part[key];
@@ -363,6 +364,11 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
363
364
  // existing conversation, and the flag is stripped once the fresh id is
364
365
  // mirrored into the URL.
365
366
  const forceNewConversation = searchParams.get('new') !== null;
367
+ // ADR-0070 "Edit with AI": the package the user opened to edit (from the app
368
+ // list's per-app action). Forwarded to the build agent as `context.packageId`
369
+ // so its metadata reads scope to that app and edits bind to it from the first
370
+ // message (the agent seeds it as the conversation's active package).
371
+ const editPackageId = searchParams.get('package')?.trim() || undefined;
366
372
  const navigate = useNavigate();
367
373
  const { setContext } = useNavigationContext();
368
374
  useEffect(() => {
@@ -491,6 +497,7 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
491
497
  const [refreshKey, setRefreshKey] = useState(0);
492
498
  const [titleHints, setTitleHints] = useState({});
493
499
  const [shareOpen, setShareOpen] = useState(false);
500
+ const [debugOpen, setDebugOpen] = useState(false);
494
501
  const [mobileChatsOpen, setMobileChatsOpen] = useState(false);
495
502
  const { collapsed: chatsCollapsed, toggle: toggleChatsCollapsed, handleCanvasOpenChange, } = useCollapsibleChatsList();
496
503
  // Keyboard shortcuts (ChatGPT/Claude parity): ⌘⇧O new chat, ⌘⇧S toggle list.
@@ -609,7 +616,7 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
609
616
  ? t('console.ai.showChats', { defaultValue: 'Show chats' })
610
617
  : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), title: chatsCollapsed
611
618
  ? t('console.ai.showChats', { defaultValue: 'Show chats' })
612
- : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), "data-testid": "ai-chat-collapse-sidebar-trigger", "aria-pressed": chatsCollapsed, children: chatsCollapsed ? _jsx(PanelLeftOpen, { className: "h-4 w-4" }) : _jsx(PanelLeftClose, { className: "h-4 w-4" }) })] })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(AppHeader, { variant: "home" }) })] }), noAgents ? (_jsx(AiUnavailable, { hasError: Boolean(agentsError), onRetry: refetchAgents, onHome: () => navigate('/home'), t: t })) : (_jsxs(_Fragment, { children: [_jsx(Sheet, { open: mobileChatsOpen, onOpenChange: setMobileChatsOpen, children: _jsxs(SheetContent, { side: "left", className: "w-[320px] p-0 sm:max-w-[360px]", "data-testid": "ai-chat-mobile-sidebar", children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: t('console.ai.chats') }), _jsx(SheetDescription, { children: t('console.ai.chatsDescription') })] }), _jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "h-full border-r-0", onNavigate: () => setMobileChatsOpen(false) })] }) }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase, publicBaseUrl: publicShareBase })), _jsxs("div", { className: "flex min-h-0 flex-1 w-full bg-muted/20", children: [!chatsCollapsed && (_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "hidden w-72 shrink-0 border-r md:flex" })), _jsx("main", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, initialMessages: initialMessages, onSent: handleSent, onShare: () => setShareOpen(true), onCanvasOpenChange: handleCanvasOpenChange }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }))] }));
619
+ : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), "data-testid": "ai-chat-collapse-sidebar-trigger", "aria-pressed": chatsCollapsed, children: chatsCollapsed ? _jsx(PanelLeftOpen, { className: "h-4 w-4" }) : _jsx(PanelLeftClose, { className: "h-4 w-4" }) })] })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(AppHeader, { variant: "home" }) })] }), noAgents ? (_jsx(AiUnavailable, { hasError: Boolean(agentsError), onRetry: refetchAgents, onHome: () => navigate('/home'), t: t })) : (_jsxs(_Fragment, { children: [_jsx(Sheet, { open: mobileChatsOpen, onOpenChange: setMobileChatsOpen, children: _jsxs(SheetContent, { side: "left", className: "w-[320px] p-0 sm:max-w-[360px]", "data-testid": "ai-chat-mobile-sidebar", children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: t('console.ai.chats') }), _jsx(SheetDescription, { children: t('console.ai.chatsDescription') })] }), _jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "h-full border-r-0", onNavigate: () => setMobileChatsOpen(false) })] }) }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase, publicBaseUrl: publicShareBase })), conversationId && isBuildAgent(activeAgent) && (_jsx(BuildDebugDrawer, { apiBase: apiBase, conversationId: conversationId, open: debugOpen, onOpenChange: setDebugOpen })), _jsxs("div", { className: "flex min-h-0 flex-1 w-full bg-muted/20", children: [!chatsCollapsed && (_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "hidden w-72 shrink-0 border-r md:flex" })), _jsx("main", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, editPackageId: editPackageId, initialMessages: initialMessages, onSent: handleSent, onShare: () => setShareOpen(true), onDebug: () => setDebugOpen(true), showDebug: isBuildAgent(activeAgent), onCanvasOpenChange: handleCanvasOpenChange }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }))] }));
613
620
  }
614
621
  /**
615
622
  * Graceful state for `/ai` when the agent catalog resolved empty — shown
@@ -627,7 +634,7 @@ function AiUnavailable({ hasError, onRetry, onHome, t, }) {
627
634
  defaultValue: "This deployment doesn't have an AI assistant enabled. Everything else works as usual.",
628
635
  }) }), _jsxs("div", { className: "mt-6 flex flex-col items-center gap-3 sm:flex-row", children: [hasError && (_jsx(Button, { variant: "outline", onClick: onRetry, "data-testid": "ai-unavailable-retry", children: t('console.ai.unavailableRetry', { defaultValue: 'Try again' }) })), _jsx(Button, { onClick: onHome, "data-testid": "ai-unavailable-home", children: t('console.ai.unavailableHome', { defaultValue: 'Back to home' }) })] })] }) }));
629
636
  }
630
- function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, initialMessages, onSent, onShare, onCanvasOpenChange, }) {
637
+ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, editPackageId, initialMessages, onSent, onShare, onDebug, showDebug, onCanvasOpenChange, }) {
631
638
  const { t } = useObjectTranslation();
632
639
  const navigate = useNavigate();
633
640
  // The agent dropdown is a LAUNCHER now (not an in-surface mode toggle): it
@@ -692,9 +699,18 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
692
699
  // ADR-0013 D2: reconcile a stream-transport failure instead of blindly
693
700
  // retrying. Shared across chat surfaces — see useReconcileOnError.
694
701
  const { errorSuppressed, handleChatError, setMessagesRef, resetSuppression } = useReconcileOnError({ chatApi, conversationId });
702
+ // ADR-0028: plan-filtered selectable AI model on the full-page Build/Ask
703
+ // surface. The footer <select> in ChatbotEnhanced renders only for 2+ models,
704
+ // so free / single-model envs see nothing. Mirrors ConsoleFloatingChatbot;
705
+ // the chosen model rides each request via useObjectChat's `model` below.
706
+ const { models: aiModels, defaultModelId } = useAiModels({ apiBase });
707
+ const [selectedModelId, setSelectedModelId] = useState(undefined);
708
+ const effectiveModelId = selectedModelId ?? defaultModelId;
695
709
  const { messages, isLoading, error, sendMessage, stop, reload, clear, setMessages, } = useObjectChat({
696
710
  api: chatApi,
697
711
  conversationId,
712
+ // ADR-0028: the user's picked model (or the env default) rides each request.
713
+ model: effectiveModelId,
698
714
  onError: handleChatError,
699
715
  body: {
700
716
  context: {
@@ -703,6 +719,9 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
703
719
  // Tell the agent the environment's publish posture so its narration
704
720
  // matches reality (an auto-published build is live, not "to publish").
705
721
  autoPublishAiBuilds: getRuntimeConfig().features.autoPublishAiBuilds,
722
+ // ADR-0070 "Edit with AI": scope the build agent to the app the user
723
+ // opened to edit. Cloud seeds it as the conversation's active package.
724
+ ...(editPackageId ? { packageId: editPackageId } : {}),
706
725
  },
707
726
  },
708
727
  initialMessages: hydrated,
@@ -741,7 +760,7 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
741
760
  sendMessage(content, files);
742
761
  onSent(content);
743
762
  }, [sendMessage, onSent]);
744
- const headerSlot = (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border/50 px-4 pb-2 pt-3 sm:px-6", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: showAgentLauncher ? (_jsxs(Select, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-auto min-w-0 border-0 bg-transparent px-1.5 text-xs shadow-none hover:bg-accent focus:ring-0 focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-border/80 focus-visible:ring-offset-0 sm:min-w-[160px]", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: _jsx(SelectValue, { placeholder: t('console.ai.chooseAgent', { defaultValue: 'Choose assistant…' }) }) }), _jsx(SelectContent, { align: "start", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: localizeAgentLabel(t, agent.name, agent.label) }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[260px]", children: agent.description })) : null] }, agent.name))) })] })) : (_jsx("span", { className: "truncate text-xs font-medium text-foreground/85", children: activeAgentLabel })) }), _jsx("div", { className: "flex shrink-0 items-center gap-1", children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onShare, disabled: !conversationId, "aria-label": t('console.ai.share'), "data-testid": "ai-chat-share-button", title: conversationId ? t('console.ai.shareTitle') : t('console.ai.shareDisabledTitle'), children: _jsx(Share2, { className: "h-3.5 w-3.5" }) }) }), agentsError ? (_jsx("span", { className: "basis-full text-[10px] text-amber-700 dark:text-amber-400", title: agentsError.message, children: t('console.ai.offlineDemoMode') })) : null] }));
763
+ const headerSlot = (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border/50 px-4 pb-2 pt-3 sm:px-6", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: showAgentLauncher ? (_jsxs(Select, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-auto min-w-0 border-0 bg-transparent px-1.5 text-xs shadow-none hover:bg-accent focus:ring-0 focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-border/80 focus-visible:ring-offset-0 sm:min-w-[160px]", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: _jsx(SelectValue, { placeholder: t('console.ai.chooseAgent', { defaultValue: 'Choose assistant…' }) }) }), _jsx(SelectContent, { align: "start", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: localizeAgentLabel(t, agent.name, agent.label) }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[260px]", children: agent.description })) : null] }, agent.name))) })] })) : (_jsx("span", { className: "truncate text-xs font-medium text-foreground/85", children: activeAgentLabel })) }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [showDebug && onDebug ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onDebug, disabled: !conversationId, "aria-label": "Build Doctor", "data-testid": "ai-chat-debug-button", title: conversationId ? 'Build Doctor — what actually landed?' : 'Send a message first', children: _jsx(Bug, { className: "h-3.5 w-3.5" }) })) : null, _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onShare, disabled: !conversationId, "aria-label": t('console.ai.share'), "data-testid": "ai-chat-share-button", title: conversationId ? t('console.ai.shareTitle') : t('console.ai.shareDisabledTitle'), children: _jsx(Share2, { className: "h-3.5 w-3.5" }) })] }), agentsError ? (_jsx("span", { className: "basis-full text-[10px] text-amber-700 dark:text-amber-400", title: agentsError.message, children: t('console.ai.offlineDemoMode') })) : null] }));
745
764
  return (_jsxs("div", { ref: split.containerRef, className: "relative flex min-h-0 flex-1 px-0", children: [_jsx("div", { "data-chat-column": true, className: canvasApp
746
765
  ? 'flex min-h-0 shrink-0 justify-center'
747
766
  : 'flex min-h-0 flex-1 justify-center', style: canvasApp ? { width: split.width } : undefined, children: _jsx(ChatbotEnhanced, { className: "min-h-0 flex-1 bg-background md:max-w-5xl", onUpgrade: () => window.open(cloudPricingDeepLink(), '_blank', 'noopener,noreferrer'), surface: "plain", maxHeight: "100%", headerSlot: headerSlot, messages: messages, placeholder: activeAgent
@@ -775,7 +794,11 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
775
794
  stopResponse: t('console.ai.stopResponse'),
776
795
  trace: t('console.ai.trace'),
777
796
  viewTrace: t('console.ai.viewTrace'),
778
- }, suggestions: suggestions, onSendMessage: handleSend, onClear: clear, hideClearBar: true, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: errorSuppressed ? undefined : error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat",
797
+ },
798
+ // ADR-0028: selectable AI model — ChatbotEnhanced renders the footer
799
+ // <select> only when 2+ models are offered (free / single-model envs
800
+ // see none). The picked model flows to useObjectChat above.
801
+ models: aiModels, selectedModelId: effectiveModelId, onModelChange: setSelectedModelId, suggestions: suggestions, onSendMessage: handleSend, onClear: clear, hideClearBar: true, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: errorSuppressed ? undefined : error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat",
779
802
  // Build-tree "Open app": jump straight into the app the agent just built.
780
803
  onOpenBuiltApp: (appName, appSegment) => navigate(`/apps/${encodeURIComponent(appSegment ?? appName)}`), openBuiltAppLabel: t('console.ai.openBuiltApp', { defaultValue: 'Open app' }),
781
804
  // Live lifecycle truth for draft cards: the server's pending count per
@@ -0,0 +1,20 @@
1
+ /**
2
+ * BuildDebugDrawer — self-serve "what actually landed?" panel for a build
3
+ * conversation. Opens a right-side sheet, calls the admin build-debug endpoint
4
+ * (see buildDebugApi.ts), and renders the reconciliation: agent-CLAIMED vs LIVE
5
+ * `sys_metadata`. The headline is the verdict + the two failure modes the chat
6
+ * can't show — PROPOSED-BUT-ORPHANED (a confirm card no turn applied) and
7
+ * CLAIMED-BUT-MISSING (said applied, isn't live). Read-only; no DB credentials.
8
+ *
9
+ * Distinct from `useReconcileOnError` (ADR-0013 D2 stream-failure recovery) —
10
+ * this reconciles the BUILD against live metadata, not a transport drop.
11
+ */
12
+ import React from 'react';
13
+ interface BuildDebugDrawerProps {
14
+ apiBase: string;
15
+ conversationId?: string;
16
+ open: boolean;
17
+ onOpenChange: (open: boolean) => void;
18
+ }
19
+ export declare function BuildDebugDrawer({ apiBase, conversationId, open, onOpenChange }: BuildDebugDrawerProps): React.JSX.Element;
20
+ export {};
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * BuildDebugDrawer — self-serve "what actually landed?" panel for a build
5
+ * conversation. Opens a right-side sheet, calls the admin build-debug endpoint
6
+ * (see buildDebugApi.ts), and renders the reconciliation: agent-CLAIMED vs LIVE
7
+ * `sys_metadata`. The headline is the verdict + the two failure modes the chat
8
+ * can't show — PROPOSED-BUT-ORPHANED (a confirm card no turn applied) and
9
+ * CLAIMED-BUT-MISSING (said applied, isn't live). Read-only; no DB credentials.
10
+ *
11
+ * Distinct from `useReconcileOnError` (ADR-0013 D2 stream-failure recovery) —
12
+ * this reconciles the BUILD against live metadata, not a transport drop.
13
+ */
14
+ import { useEffect, useState } from 'react';
15
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@object-ui/components';
16
+ import { Bug, CheckCircle2, AlertTriangle, XCircle, Loader2, CircleSlash } from 'lucide-react';
17
+ import { fetchBuildDebug } from './buildDebugApi';
18
+ export function BuildDebugDrawer({ apiBase, conversationId, open, onOpenChange }) {
19
+ const [report, setReport] = useState(null);
20
+ const [loading, setLoading] = useState(false);
21
+ const [error, setError] = useState(null);
22
+ useEffect(() => {
23
+ if (!open || !conversationId)
24
+ return;
25
+ let cancelled = false;
26
+ setLoading(true);
27
+ setError(null);
28
+ setReport(null);
29
+ fetchBuildDebug(apiBase, conversationId)
30
+ .then((r) => {
31
+ if (cancelled)
32
+ return;
33
+ if (!r)
34
+ setError('Not available — the conversation was not found or you are not authorized.');
35
+ else
36
+ setReport(r);
37
+ })
38
+ .catch((e) => {
39
+ if (!cancelled)
40
+ setError(e instanceof Error ? e.message : String(e));
41
+ })
42
+ .finally(() => {
43
+ if (!cancelled)
44
+ setLoading(false);
45
+ });
46
+ return () => {
47
+ cancelled = true;
48
+ };
49
+ }, [open, conversationId, apiBase]);
50
+ const rec = report?.reconciliation;
51
+ const problems = rec ? rec.orphaned.length + rec.missing.length + rec.errors.length : 0;
52
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", className: "w-full overflow-y-auto sm:max-w-xl", children: [_jsxs(SheetHeader, { children: [_jsxs(SheetTitle, { className: "flex items-center gap-2", children: [_jsx(Bug, { className: "h-4 w-4" }), " Build Doctor"] }), _jsx(SheetDescription, { children: "What the agent claimed vs what is actually live. Read-only diagnostic." })] }), _jsxs("div", { className: "mt-4 space-y-4 text-sm", children: [loading && (_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Reconciling\u2026"] })), error && !loading && (_jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 p-3 text-destructive", children: error })), report && !loading && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground", children: [_jsx("div", { className: "font-medium text-foreground", children: report.title ?? '(untitled)' }), _jsxs("div", { className: "mt-1", children: [report.summary.userTurns, " turn(s) \u00B7 ", report.summary.messages, " msgs \u00B7", ' ', report.summary.totalTokens.toLocaleString(), " tok \u00B7", ' ', (report.summary.llmMs / 1000).toFixed(1), "s LLM", report.summary.models.length ? ` · ${report.summary.models.join(', ')}` : ''] })] }), rec && (_jsxs("div", { className: rec.ok
53
+ ? 'flex items-center gap-2 rounded-md border border-emerald-500/30 bg-emerald-500/10 p-3 text-emerald-700 dark:text-emerald-400'
54
+ : 'flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-destructive', children: [rec.ok ? _jsx(CheckCircle2, { className: "h-4 w-4" }) : _jsx(AlertTriangle, { className: "h-4 w-4" }), _jsx("span", { className: "font-medium", children: rec.ok
55
+ ? `All ${rec.liveCount} attempted change(s) are live — nothing evaporated.`
56
+ : `${problems} discrepancy(ies) — what the chat said doesn't match what's live.` })] })), rec && rec.orphaned.length > 0 && (_jsx(FindingSection, { icon: _jsx(CircleSlash, { className: "h-4 w-4 text-destructive" }), title: "Proposed but never applied", hint: "A confirm card the agent proposed but no later turn applied \u2014 the change silently evaporated.", findings: rec.orphaned, tone: "destructive" })), rec && rec.missing.length > 0 && (_jsx(FindingSection, { icon: _jsx(AlertTriangle, { className: "h-4 w-4 text-amber-600" }), title: "Claimed but missing", hint: "A tool result said it was applied, but the artifact isn't live in sys_metadata.", findings: rec.missing, tone: "amber" })), rec && rec.errors.length > 0 && (_jsx(FindingSection, { icon: _jsx(XCircle, { className: "h-4 w-4 text-destructive" }), title: "Tool errors", hint: "Tool calls that returned an error during the build.", findings: rec.errors, tone: "destructive" })), report.verify && (_jsxs("div", { className: "rounded-md border p-3 text-xs", children: [_jsx("div", { className: "font-medium text-foreground", children: "Build check (verify_build)" }), _jsxs("div", { className: "mt-1 text-muted-foreground", children: ["Your app:", ' ', report.verify.userIssues.length === 0 ? (_jsx("span", { className: "text-emerald-600 dark:text-emerald-400", children: "0 issues" })) : (_jsxs("span", { className: "text-destructive", children: [report.verify.userIssues.length, " issue(s)"] })), report.verify.platformNoise > 0
57
+ ? ` · ${report.verify.platformNoise} platform sys_* finding(s) hidden`
58
+ : ''] }), report.verify.userIssues.map((is, i) => (_jsxs("div", { className: "mt-1 text-destructive", children: ["[", is.severity, "] ", is.code, " ", is.artifact ? `${is.artifact.type}:${is.artifact.name}` : ''] }, i)))] })), report.pendingActions.length > 0 && (_jsxs("div", { className: "rounded-md border p-3 text-xs", children: [_jsx("div", { className: "font-medium text-foreground", children: "Pending actions" }), report.pendingActions.map((p, i) => (_jsxs("div", { className: "mt-1 text-muted-foreground", children: [p.tool ?? '?', " \u00B7 ", p.object ?? '-', " \u00B7 ", _jsx("span", { className: "font-mono", children: p.status ?? '-' })] }, i)))] })), _jsxs("details", { className: "rounded-md border p-3 text-xs", children: [_jsxs("summary", { className: "cursor-pointer font-medium text-foreground", children: ["Timeline (", report.timeline.length, ")"] }), _jsx("div", { className: "mt-2 space-y-1 font-mono text-[11px] leading-relaxed", children: report.timeline.map((e, i) => (_jsx(TimelineRow, { entry: e }, i))) })] })] }))] })] }) }));
59
+ }
60
+ function FindingSection({ icon, title, hint, findings, tone, }) {
61
+ const border = tone === 'destructive' ? 'border-destructive/30' : 'border-amber-500/30';
62
+ return (_jsxs("div", { className: `rounded-md border ${border} p-3`, children: [_jsxs("div", { className: "flex items-center gap-2 font-medium text-foreground", children: [icon, " ", title, " (", findings.length, ")"] }), _jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: hint }), _jsx("div", { className: "mt-2 space-y-1", children: findings.map((f, i) => (_jsxs("div", { className: "font-mono text-xs", children: [f.t ? `${f.t} · ` : '', f.tool, " \u2192 ", f.artifact.type, ":", f.artifact.name, _jsxs("span", { className: "text-muted-foreground", children: [" (", f.status, ")"] })] }, i))) })] }));
63
+ }
64
+ function TimelineRow({ entry }) {
65
+ if (entry.kind === 'user') {
66
+ return (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: entry.t }), " \uD83D\uDC64 ", entry.text] }));
67
+ }
68
+ if (entry.kind === 'assistant-text') {
69
+ return (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: entry.t }), " \uD83E\uDD16 ", entry.text] }));
70
+ }
71
+ if (entry.kind === 'assistant-calls') {
72
+ return (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: entry.t }), " \uD83E\uDD16 \u2192", ' ', entry.calls.map((c) => c.name).join(', ')] }));
73
+ }
74
+ return (_jsxs("div", { className: entry.isError ? 'text-destructive' : '', children: [_jsx("span", { className: "text-muted-foreground", children: entry.t }), " \u21B3 ", entry.name, entry.status ? ` (${entry.status})` : ''] }));
75
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Client for the admin build-debug endpoint
3
+ * (`GET /api/v1/ai/conversations/:id/debug`, service-ai).
4
+ *
5
+ * The chat renders the build agent's self-report, never what actually landed.
6
+ * This fetches the server-side reconciliation — agent-CLAIMED vs LIVE
7
+ * `sys_metadata` — so a signed-in admin can diagnose a build that "went wrong"
8
+ * from the browser, with no DB credentials. Mirrors `fetchConversation`'s
9
+ * same-origin cookie auth.
10
+ */
11
+ export interface ArtifactRef {
12
+ type: string;
13
+ name: string;
14
+ }
15
+ export interface MutationFinding {
16
+ t?: string;
17
+ tool: string;
18
+ status: string;
19
+ artifact: ArtifactRef;
20
+ }
21
+ export interface VerifyIssue {
22
+ severity: string;
23
+ code: string;
24
+ artifact?: ArtifactRef;
25
+ }
26
+ export interface VerifySummary {
27
+ status: string;
28
+ errors: number;
29
+ warnings: number;
30
+ userIssues: VerifyIssue[];
31
+ platformNoise: number;
32
+ }
33
+ export type TimelineEntry = {
34
+ t: string;
35
+ kind: 'user';
36
+ text: string;
37
+ } | {
38
+ t: string;
39
+ kind: 'assistant-text';
40
+ text: string;
41
+ model?: string;
42
+ tokens?: number;
43
+ ms?: number;
44
+ } | {
45
+ t: string;
46
+ kind: 'assistant-calls';
47
+ calls: Array<{
48
+ name: string;
49
+ args: unknown;
50
+ }>;
51
+ model?: string;
52
+ tokens?: number;
53
+ ms?: number;
54
+ } | {
55
+ t: string;
56
+ kind: 'tool-result';
57
+ name: string;
58
+ isError: boolean;
59
+ status?: string;
60
+ preview: string;
61
+ };
62
+ export interface BuildDebugReport {
63
+ conversationId: string;
64
+ title: string | null;
65
+ summary: {
66
+ models: string[];
67
+ userTurns: number;
68
+ messages: number;
69
+ totalTokens: number;
70
+ llmMs: number;
71
+ };
72
+ reconciliation: {
73
+ orphaned: MutationFinding[];
74
+ missing: MutationFinding[];
75
+ errors: MutationFinding[];
76
+ liveCount: number;
77
+ ok: boolean;
78
+ };
79
+ verify: VerifySummary | null;
80
+ timeline: TimelineEntry[];
81
+ pendingActions: Array<{
82
+ tool: string | null;
83
+ object: string | null;
84
+ status: string | null;
85
+ error: string | null;
86
+ createdAt: string | null;
87
+ }>;
88
+ }
89
+ /**
90
+ * Fetch the reconciliation report for a build conversation. Returns null on
91
+ * 403/404 (not authorized / unknown conversation) so the caller can show an
92
+ * empty state instead of throwing.
93
+ */
94
+ export declare function fetchBuildDebug(apiBase: string, conversationId: string): Promise<BuildDebugReport | null>;
@@ -0,0 +1,16 @@
1
+ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Fetch the reconciliation report for a build conversation. Returns null on
4
+ * 403/404 (not authorized / unknown conversation) so the caller can show an
5
+ * empty state instead of throwing.
6
+ */
7
+ export async function fetchBuildDebug(apiBase, conversationId) {
8
+ const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(conversationId)}/debug`, {
9
+ credentials: 'include',
10
+ });
11
+ if (res.status === 404 || res.status === 403 || res.status === 401)
12
+ return null;
13
+ if (!res.ok)
14
+ throw new Error(`GET build debug failed: ${res.status}`);
15
+ return (await res.json());
16
+ }
@@ -99,12 +99,21 @@ export function OrganizationsPage() {
99
99
  return;
100
100
  if (isOrganizationsLoading)
101
101
  return;
102
- if (manageMode || wantsCreate)
103
- return; // came to manage/create — don't bounce
102
+ if (wantsCreate)
103
+ return; // came to create — don't bounce
104
104
  if (orgList.length !== 1)
105
105
  return;
106
106
  autoSelectedRef.current = true;
107
- void handleSelect(orgList[0]);
107
+ // Single-org users have no real choice to make. In manage mode (`?manage=1`,
108
+ // used by the Cloud app "Members" nav and the avatar "My Organizations"
109
+ // entry), skip the pointless one-item picker and deep-link straight to that
110
+ // org's member management; otherwise switch into the org and land on home.
111
+ if (manageMode) {
112
+ navigate(`/organizations/${orgList[0].slug}/members`, { replace: true });
113
+ }
114
+ else {
115
+ void handleSelect(orgList[0]);
116
+ }
108
117
  // eslint-disable-next-line react-hooks/exhaustive-deps
109
118
  }, [isOrganizationsLoading, orgList.length, manageMode, wantsCreate]);
110
119
  // Open the create dialog when arriving via the header "Create workspace"
@@ -119,7 +128,7 @@ export function OrganizationsPage() {
119
128
  // Show a spinner while we're either still loading, or about to auto-redirect
120
129
  // because there's only one org. This prevents the picker from briefly
121
130
  // flashing on screen for single-org users.
122
- const willAutoSelect = !manageMode && !wantsCreate && !isOrganizationsLoading && orgList.length === 1;
131
+ const willAutoSelect = !wantsCreate && !isOrganizationsLoading && orgList.length === 1;
123
132
  if (isOrganizationsLoading || willAutoSelect) {
124
133
  return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
125
134
  }
@@ -45,7 +45,7 @@ export function OrganizationLayout() {
45
45
  { to: 'invitations', label: t('organization.tabs.invitations', { defaultValue: 'Invitations' }) },
46
46
  { to: 'settings', label: t('organization.tabs.settings', { defaultValue: 'Settings' }) },
47
47
  ];
48
- return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "organization-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "orgs" }) }), _jsx("div", { className: "border-b", children: _jsxs("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 pt-6", children: [_jsxs("button", { onClick: () => navigate('/organizations'), className: "mb-3 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), t('organization.backToList', { defaultValue: 'Back to organizations' })] }), _jsx("div", { className: "flex items-center justify-between gap-4", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "truncate text-2xl font-bold tracking-tight", children: org.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: org.slug })] }) }), _jsx("nav", { className: "mt-4 flex gap-1 -mb-px", children: tabs.map((tab) => (_jsx(NavLink, { to: tab.to, end: true, className: ({ isActive }) => `border-b-2 px-3 py-2 text-sm font-medium transition-colors ${isActive
48
+ return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "organization-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "orgs" }) }), _jsx("div", { className: "border-b", children: _jsxs("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 pt-6", children: [(organizations ?? []).length > 1 && (_jsxs("button", { onClick: () => navigate('/organizations'), className: "mb-3 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), t('organization.backToList', { defaultValue: 'Back to organizations' })] })), _jsx("div", { className: "flex items-center justify-between gap-4", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "truncate text-2xl font-bold tracking-tight", children: org.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: org.slug })] }) }), _jsx("nav", { className: "mt-4 flex gap-1 -mb-px", children: tabs.map((tab) => (_jsx(NavLink, { to: tab.to, end: true, className: ({ isActive }) => `border-b-2 px-3 py-2 text-sm font-medium transition-colors ${isActive
49
49
  ? 'border-primary text-foreground'
50
50
  : 'border-transparent text-muted-foreground hover:text-foreground'}`, children: tab.label }, tab.to))) })] }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: _jsx("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 py-6", children: _jsx(Outlet, { context: { org } }) }) })] }));
51
51
  }
@@ -20,11 +20,12 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
20
20
  */
21
21
  import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
22
22
  import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
23
- import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Boxes, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
23
+ import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
24
24
  import { useState, useEffect, useCallback, useRef } from 'react';
25
25
  import { useOffline } from '@object-ui/react';
26
26
  import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
27
27
  import { ModeToggle } from './ModeToggle';
28
+ import { WorkspaceSwitcher } from './WorkspaceSwitcher';
28
29
  import { LocaleSwitcher } from './LocaleSwitcher';
29
30
  import { ConnectionStatus } from './ConnectionStatus';
30
31
  import { InboxPopover } from './InboxPopover';
@@ -558,7 +559,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
558
559
  }
559
560
  }
560
561
  const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
561
- return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: getProductName(), children: _jsx(Layers, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: getProductName() })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(LocalizedSidebarTrigger, { className: "lg:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' }) }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
562
+ return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: getProductName(), children: _jsx(Layers, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: getProductName() })), _jsx(WorkspaceSwitcher, {}), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(LocalizedSidebarTrigger, { className: "lg:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' }) }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
562
563
  const isLast = i === extraSegments.length - 1;
563
564
  return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
564
565
  }), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": t('topbar.switchView', { defaultValue: 'Switch view' }), children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
@@ -572,7 +573,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
572
573
  }) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), aiEnabled && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) })), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
573
574
  void loadHelpDocs(); }, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-56 rounded-lg", sideOffset: 4, children: [currentAppDocs.length > 0 && currentAppPackageId ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate(currentAppDocs.length === 1
574
575
  ? `/apps/${currentAppPackageId}/docs/${currentAppDocs[0].name}`
575
- : `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?manage=1'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), hasOrgSection && !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer", "data-testid": "header-create-workspace", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] })), (metadataApps || [])
576
+ : `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer", "data-testid": "header-create-workspace", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] })), (metadataApps || [])
576
577
  .filter((a) => a.active !== false && a.hidden === true && a.name !== 'account')
577
578
  .map((app) => {
578
579
  const AppIcon = getIcon(app.icon);
@@ -104,8 +104,12 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
104
104
  planApprove: '开始搭建',
105
105
  planAdjust: '调整方案',
106
106
  planBuilt: '已搭建',
107
- planApproveMessage: '就按这个方案搭建吧。',
108
- planApproveDefaultsMessage: '就按你的合理假设直接搭建,未决问题用默认即可。',
107
+ // These messages the button SENDS must match the cloud confirm gate's
108
+ // APPROVAL_RE (service-ai-studio confirm-gate.ts) or the agent re-proposes
109
+ // and "开始搭建" looks inert — the gate anchors Chinese approval on 确认 /
110
+ // 直接搭建, so a bare "…搭建吧" does NOT match. Keep these 确认-anchored.
111
+ planApproveMessage: '确认,开始搭建。',
112
+ planApproveDefaultsMessage: '确认搭建,未决问题按你的合理假设和默认处理。',
109
113
  planAnswer: (question, option) => `关于「${question}」,我选择「${option}」。`,
110
114
  publishOk: '已发布,对象已生效。',
111
115
  publishFailed: '发布失败',
@@ -19,16 +19,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
19
19
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
20
20
  import { getIcon } from '../utils/getIcon';
21
21
  import { resolveI18nLabel } from '../utils';
22
- // Local/Custom scope sentinel — kept inline (not imported from metadata-admin)
23
- // so this layout module never forms an import cycle with the metadata-admin
24
- // views. Mirrors `LOCAL_PACKAGE_ID` in views/metadata-admin/package-scope.ts.
25
- const LOCAL_SCOPE_ID = 'sys_metadata';
26
- function localScopeLabel() {
27
- const lang = (typeof document !== 'undefined' && document.documentElement?.lang) ||
28
- (typeof navigator !== 'undefined' && navigator.language) ||
29
- '';
30
- return /^zh/i.test(lang) ? '本地 / 自定义(本环境)' : 'Local / Custom (this env)';
31
- }
32
22
  const ALL_SENTINEL = '__all__';
33
23
  /** Read a (possibly dotted) property path off a row, e.g. `manifest.id`. */
34
24
  function getByPath(row, key) {
@@ -117,15 +107,6 @@ function useSelectorOptions(def) {
117
107
  opts.push({ value, label: typeof labelRaw === 'string' && labelRaw ? labelRaw : value });
118
108
  }
119
109
  opts.sort((a, b) => a.label.localeCompare(b.label));
120
- // The package-scope selector gets a stable "Local / Custom (this env)"
121
- // entry for this environment's runtime, DB-authored metadata — it is
122
- // never a real package row (`package_id = null` / `sys_metadata`
123
- // provenance) yet must always be selectable so org-authored items are
124
- // discoverable and editable. The metadata list/get API already treats
125
- // `?package=sys_metadata` as exactly this local scope.
126
- if (/package/i.test(endpoint) && !opts.some((o) => o.value === LOCAL_SCOPE_ID)) {
127
- opts.push({ value: LOCAL_SCOPE_ID, label: localScopeLabel() });
128
- }
129
110
  setOptions(opts);
130
111
  }
131
112
  catch {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * WorkspaceSwitcher
3
+ *
4
+ * Header-left organization (workspace) switcher — the standard place users
5
+ * expect "which org am I in / switch org" to live (Linear/Vercel/GitHub style).
6
+ *
7
+ * - Single-org users (the vast majority): just the org name, NO dropdown. There
8
+ * is nothing to switch to, so a one-item menu would be pure friction.
9
+ * - Multi-org users: the active org name + a dropdown to switch orgs inline
10
+ * (full-page reload so the active-org context refreshes app-wide, mirroring
11
+ * OrganizationsPage), plus shortcuts to manage members / create a workspace.
12
+ * - No org context at all: renders nothing.
13
+ */
14
+ export declare function WorkspaceSwitcher(): import("react").JSX.Element | null;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * WorkspaceSwitcher
4
+ *
5
+ * Header-left organization (workspace) switcher — the standard place users
6
+ * expect "which org am I in / switch org" to live (Linear/Vercel/GitHub style).
7
+ *
8
+ * - Single-org users (the vast majority): just the org name, NO dropdown. There
9
+ * is nothing to switch to, so a one-item menu would be pure friction.
10
+ * - Multi-org users: the active org name + a dropdown to switch orgs inline
11
+ * (full-page reload so the active-org context refreshes app-wide, mirroring
12
+ * OrganizationsPage), plus shortcuts to manage members / create a workspace.
13
+ * - No org context at all: renders nothing.
14
+ */
15
+ import { useEffect, useState } from 'react';
16
+ import { useNavigate } from 'react-router-dom';
17
+ import { useAuth } from '@object-ui/auth';
18
+ import { useObjectTranslation } from '@object-ui/i18n';
19
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from '@object-ui/components';
20
+ import { ChevronsUpDown, Check, Plus, Users } from 'lucide-react';
21
+ import { resolveHomeUrl } from '../console/organizations/resolveHomeUrl';
22
+ function getOrgInitials(name) {
23
+ return name
24
+ .split(/[\s_-]+/)
25
+ .map((w) => w[0])
26
+ .join('')
27
+ .toUpperCase()
28
+ .slice(0, 2);
29
+ }
30
+ function OrgBadge({ name }) {
31
+ return (_jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-semibold text-muted-foreground", children: getOrgInitials(name) }));
32
+ }
33
+ export function WorkspaceSwitcher() {
34
+ const { t } = useObjectTranslation();
35
+ const navigate = useNavigate();
36
+ const { organizations, activeOrganization, switchOrganization, getAuthConfig } = useAuth();
37
+ const [multiOrgDisabled, setMultiOrgDisabled] = useState(false);
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+ getAuthConfig?.()
41
+ .then((cfg) => {
42
+ if (!cancelled)
43
+ setMultiOrgDisabled(cfg?.features?.multiOrgEnabled === false);
44
+ })
45
+ .catch(() => {
46
+ /* leave default — create entry stays available */
47
+ });
48
+ return () => {
49
+ cancelled = true;
50
+ };
51
+ }, [getAuthConfig]);
52
+ const orgList = organizations ?? [];
53
+ const current = activeOrganization ?? orgList[0] ?? null;
54
+ // No organization context (e.g. a brand-new user before provisioning) — show
55
+ // nothing rather than an empty switcher.
56
+ if (!current)
57
+ return null;
58
+ // Single-org: static label, no dropdown.
59
+ if (orgList.length <= 1) {
60
+ return (_jsxs("span", { className: "ml-2 hidden max-w-[12rem] items-center gap-1.5 sm:inline-flex", "data-testid": "workspace-name", children: [_jsx(OrgBadge, { name: current.name }), _jsx("span", { className: "truncate text-sm font-medium text-foreground/80", children: current.name })] }));
61
+ }
62
+ const handleSwitch = async (org) => {
63
+ if (org.id === current.id)
64
+ return;
65
+ try {
66
+ await switchOrganization(org.id);
67
+ // switchOrganization only updates state; reload to home so the new active
68
+ // org propagates to every data scope app-wide (same as OrganizationsPage).
69
+ window.location.href = resolveHomeUrl();
70
+ }
71
+ catch (err) {
72
+ console.error('[WorkspaceSwitcher] switch failed', err);
73
+ }
74
+ };
75
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "ml-2 inline-flex max-w-[14rem] items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground", "data-testid": "workspace-switcher", children: [_jsx(OrgBadge, { name: current.name }), _jsx("span", { className: "hidden truncate sm:inline", children: current.name }), _jsx(ChevronsUpDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-60", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: t('organization.switcher.label', { defaultValue: 'Switch organization' }) }), orgList.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => handleSwitch(org), className: "cursor-pointer gap-2", children: [_jsx(OrgBadge, { name: org.name }), _jsx("span", { className: "flex-1 truncate", children: org.name }), org.id === current.id && _jsx(Check, { className: "h-4 w-4 shrink-0" })] }, org.id))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => navigate(`/organizations/${current.slug}/members`), className: "cursor-pointer gap-2", "data-testid": "workspace-manage-members", children: [_jsx(Users, { className: "h-4 w-4" }), t('organization.switcher.manageMembers', { defaultValue: 'Manage members' })] }), !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer gap-2", "data-testid": "workspace-create", children: [_jsx(Plus, { className: "h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] }))] })] }));
76
+ }
@@ -32,5 +32,5 @@ export interface ManagedByEmptyState {
32
32
  * `defaultValue` used as the English fallback when a locale lacks the key).
33
33
  */
34
34
  type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
35
- export declare function resolveManagedByEmptyState(managedBy: string | undefined | null, t: TranslateFn): ManagedByEmptyState | undefined;
35
+ export declare function resolveManagedByEmptyState(managedBy: string | undefined | null, t: TranslateFn, objectName?: string | null): ManagedByEmptyState | undefined;
36
36
  export {};
@@ -1,4 +1,4 @@
1
- export function resolveManagedByEmptyState(managedBy, t) {
1
+ export function resolveManagedByEmptyState(managedBy, t, objectName) {
2
2
  switch (managedBy) {
3
3
  case 'system':
4
4
  return {
@@ -17,11 +17,29 @@ export function resolveManagedByEmptyState(managedBy, t) {
17
17
  }),
18
18
  };
19
19
  case 'better-auth':
20
+ // `sys_user` is the one identity table with a concrete onboarding
21
+ // answer, so give it actionable guidance: teammates arrive via an
22
+ // org-level invite + SSO just-in-time provisioning (ADR-0024 D9), and
23
+ // app end-users self-register. We deliberately do NOT name the env-level
24
+ // "Invite User" action — it is multi-org-gated and hidden in single-org —
25
+ // nor a "Reset Password" toolbar action, which does not exist (cloud#580).
26
+ // Every other identity table (sessions, accounts, tokens, jwks,
27
+ // verifications, …) is written purely by auth flows, so keep the generic
28
+ // copy — naming an invite/signup CTA on a token list would be wrong.
29
+ if (objectName === 'sys_user') {
30
+ return {
31
+ icon: 'ShieldAlert',
32
+ title: t('list.managedBy.betterAuthUser.title', { defaultValue: 'No users yet' }),
33
+ message: t('list.managedBy.betterAuthUser.message', {
34
+ defaultValue: 'User accounts are provisioned by the authentication provider, not created here. Invite teammates to your organization and they appear automatically on first sign-in (SSO just-in-time provisioning). App end-users arrive when they sign up through your app.',
35
+ }),
36
+ };
37
+ }
20
38
  return {
21
39
  icon: 'ShieldAlert',
22
40
  title: t('list.managedBy.betterAuth.title', { defaultValue: 'No identity records' }),
23
41
  message: t('list.managedBy.betterAuth.message', {
24
- defaultValue: 'Identity rows are managed by the authentication provider. Use the dedicated identity workflows (Invite User, Reset Password, …) to create new entries.',
42
+ defaultValue: 'These records are created by the authentication provider through sign-in, provisioning, and security flows not added by hand here.',
25
43
  }),
26
44
  };
27
45
  default:
@@ -1189,7 +1189,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
1189
1189
  virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
1190
1190
  emptyState: viewEmptyState(objectDef.name, viewDef.name || viewDef.id || '', viewDef.emptyState
1191
1191
  ?? listSchema.emptyState
1192
- ?? resolveManagedByEmptyState(objectDef?.managedBy, t)),
1192
+ ?? resolveManagedByEmptyState(objectDef?.managedBy, t, objectDef.name)),
1193
1193
  aria: viewDef.aria ?? listSchema.aria,
1194
1194
  // Propagate filter/sort as default filters/sort for data flow
1195
1195
  ...((() => {
@@ -25,7 +25,7 @@ import { CreatePackageDialog } from './PackagesPage';
25
25
  import { useMetadataClient, useMetadataTypes, matchesQuery, } from './useMetadata';
26
26
  import { getMetadataResource, resolveResourceConfig, } from './registry';
27
27
  import { t, tFormat, translateMetadataType, detectLocale } from './i18n';
28
- import { buildPackageScopeOptions, LOCAL_PACKAGE_ID, isLocalScope } from './package-scope';
28
+ import { buildPackageScopeOptions } from './package-scope';
29
29
  /**
30
30
  * Derive provenance from item._packageId. The `loadMetaFromDb` path
31
31
  * tags objects with the synthetic packageId 'sys_metadata' (see
@@ -190,13 +190,13 @@ function DefaultMetadataList({ type, appName }) {
190
190
  // scope); when none exists yet, prompt to create a base first.
191
191
  const [showCreateBase, setShowCreateBase] = React.useState(false);
192
192
  const handleCreate = React.useCallback(() => {
193
- const realBases = (projectPackages ?? []).filter((p) => !isLocalScope(p.id));
194
- if (projectPackages !== null && realBases.length === 0) {
193
+ const bases = projectPackages ?? [];
194
+ if (projectPackages !== null && bases.length === 0) {
195
195
  setShowCreateBase(true);
196
196
  return;
197
197
  }
198
- if (realBases.length > 0 && (!activePackage || isLocalScope(activePackage))) {
199
- navigate(`./new?package=${encodeURIComponent(realBases[0].id)}`);
198
+ if (bases.length > 0 && !activePackage) {
199
+ navigate(`./new?package=${encodeURIComponent(bases[0].id)}`);
200
200
  return;
201
201
  }
202
202
  navigate(`./new${pkgSuffix}`);
@@ -254,12 +254,10 @@ function DefaultMetadataList({ type, appName }) {
254
254
  if (!activePackage)
255
255
  return false;
256
256
  const pkg = row.item?._packageId;
257
- const isLocal = !pkg || pkg === LOCAL_PACKAGE_ID;
258
- // Local/Custom scope surfaces this environment's runtime-authored items
259
- // (untagged / `sys_metadata` provenance); a code package shows its own.
260
- if (activePackage === LOCAL_PACKAGE_ID)
261
- return isLocal;
262
- return !isLocal && pkg === activePackage;
257
+ // Only rows tagged with the active writable base match. Untagged /
258
+ // `sys_metadata`-provenance legacy rows have no scope of their own
259
+ // (ADR-0070 D5 the package-less "Local / Custom" scope is removed).
260
+ return pkg === activePackage;
263
261
  }), [items, activePackage, config]);
264
262
  // User-driven filters (search query + source provenance) on top of scope.
265
263
  const filtered = scopedItems.filter((row) => {
@@ -28,7 +28,7 @@ import { useRecentItems } from '../../context/RecentItemsProvider';
28
28
  import { useMetadataClient, useMetadataTypes, useGlobalDiagnostics, } from './useMetadata';
29
29
  import { MetadataQuickFind } from './QuickFind';
30
30
  import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
31
- import { buildPackageScopeOptions, LOCAL_PACKAGE_ID } from './package-scope';
31
+ import { buildPackageScopeOptions } from './package-scope';
32
32
  const HIDDEN_TYPES = new Set(['field', 'package']);
33
33
  const DOMAIN_ICONS = {
34
34
  data: Database,
@@ -143,10 +143,6 @@ export function StudioHomePage() {
143
143
  return false;
144
144
  if (!activePackage)
145
145
  return false;
146
- // Local/Custom scope: show every runtime-creatable type so the user can
147
- // start authoring any kind of metadata here, even with zero items yet.
148
- if (activePackage === LOCAL_PACKAGE_ID)
149
- return e.allowOrgOverride || e.allowRuntimeCreate;
150
146
  return (packagesByType[e.type] ?? []).includes(activePackage);
151
147
  }), [activePackage, entries, packagesByType]);
152
148
  const writable = React.useMemo(() => visible.filter((e) => e.allowOrgOverride || e.allowRuntimeCreate), [visible]);
@@ -181,7 +181,6 @@ const ENGINE_STRINGS_EN = {
181
181
  'engine.list.warnCount': '{count} warning(s):',
182
182
  'engine.list.allSources': 'All sources',
183
183
  'engine.list.allPackages': 'All packages',
184
- 'engine.package.local': 'Local / Custom (this env)',
185
184
  'engine.package.writableRequired': 'Pick or create a writable base (package) first — this item cannot be authored into a read-only code package.',
186
185
  'engine.list.packageFilter': 'Package',
187
186
  'engine.list.source.artifact': 'Artifact',
@@ -878,7 +877,6 @@ const ENGINE_STRINGS_ZH = {
878
877
  'engine.list.warnCount': '{count} 个警告:',
879
878
  'engine.list.allSources': '全部来源',
880
879
  'engine.list.allPackages': '全部软件包',
881
- 'engine.package.local': '本地 / 自定义(本环境)',
882
880
  'engine.list.packageFilter': '软件包',
883
881
  'engine.list.source.artifact': '代码包',
884
882
  'engine.list.source.runtime': '运行时',
@@ -1,41 +1,16 @@
1
1
  /**
2
- * Sentinel "package" id for this environment's runtime, DB-authored metadata
3
- * items with no code-package binding (`package_id IS NULL`). The metadata
4
- * list/get API treats `?package=sys_metadata` as exactly that local scope on
5
- * READ, and a WRITE under it persists `package_id = null` (matching the
6
- * server's runtime-only provenance, see framework #2252).
2
+ * Build the Studio package-scope options from the raw `package` metadata list:
3
+ * the **writable bases** (project-scoped, DB-backed packages) only the sole
4
+ * valid authoring destinations (ADR-0070 D2). Code/installed (system|cloud)
5
+ * packages are filtered out.
7
6
  *
8
- * Why this exists: a self-hosted, metadata-customizable environment is
9
- * single-tenant there is no "org" dimension here; the real axis is
10
- * code-package vs. runtime (DB-authored). Before this scope, the package
11
- * selector only listed code packages, so metadata authored at runtime
12
- * (`package_id = null`) was filtered out of every code-package view and became
13
- * un-navigable (the route redirected to "new"). Surfacing the local scope as a
14
- * first-class, always-present selector entry makes it discoverable and editable.
15
- */
16
- export declare const LOCAL_PACKAGE_ID = "sys_metadata";
17
- /**
18
- * Build the Studio package-scope options from the raw `package` metadata list.
19
- * Filters out system/cloud-scoped packages and appends a stable
20
- * "Local / Custom (this environment)" scope so runtime metadata authored here
21
- * is always selectable/visible — even when zero items exist yet.
7
+ * There is no package-less "Local / Custom" scope: every runtime-authored item
8
+ * lives in a writable base (ADR-0070 D1/D5 the kernel rejects orphan creates
9
+ * with `writable_package_required`, and legacy orphans are adopted into a base),
10
+ * so the selector never offers an orphan bucket. The kernel keeps `null` /
11
+ * `sys_metadata` provenance only as a read-side rehydration tag for legacy rows.
22
12
  */
23
13
  export declare function buildPackageScopeOptions(rawList: unknown[] | null | undefined): {
24
14
  id: string;
25
15
  name: string;
26
16
  }[];
27
- /**
28
- * True for the runtime/null "Local / Custom" sentinel scope. Per ADR-0070 D5
29
- * this is a *migration* surface (move loose items into a base), never a valid
30
- * create destination — callers gate "create" on a real writable base.
31
- */
32
- export declare function isLocalScope(id: string | null | undefined): boolean;
33
- /**
34
- * The writable bases (project-scoped DB packages) from the raw package list —
35
- * the only valid authoring destinations (ADR-0070 D2). Excludes code/installed
36
- * (system|cloud) packages AND the Local sentinel.
37
- */
38
- export declare function writableBaseOptions(rawList: unknown[] | null | undefined): {
39
- id: string;
40
- name: string;
41
- }[];
@@ -1,27 +1,16 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
- import { detectLocale, t } from './i18n';
3
- /**
4
- * Sentinel "package" id for this environment's runtime, DB-authored metadata —
5
- * items with no code-package binding (`package_id IS NULL`). The metadata
6
- * list/get API treats `?package=sys_metadata` as exactly that local scope on
7
- * READ, and a WRITE under it persists `package_id = null` (matching the
8
- * server's runtime-only provenance, see framework #2252).
9
- *
10
- * Why this exists: a self-hosted, metadata-customizable environment is
11
- * single-tenant — there is no "org" dimension here; the real axis is
12
- * code-package vs. runtime (DB-authored). Before this scope, the package
13
- * selector only listed code packages, so metadata authored at runtime
14
- * (`package_id = null`) was filtered out of every code-package view and became
15
- * un-navigable (the route redirected to "new"). Surfacing the local scope as a
16
- * first-class, always-present selector entry makes it discoverable and editable.
17
- */
18
- export const LOCAL_PACKAGE_ID = 'sys_metadata';
19
2
  const SYSTEM_SCOPES = new Set(['system', 'cloud']);
20
3
  /**
21
- * Build the Studio package-scope options from the raw `package` metadata list.
22
- * Filters out system/cloud-scoped packages and appends a stable
23
- * "Local / Custom (this environment)" scope so runtime metadata authored here
24
- * is always selectable/visible — even when zero items exist yet.
4
+ * Build the Studio package-scope options from the raw `package` metadata list:
5
+ * the **writable bases** (project-scoped, DB-backed packages) only the sole
6
+ * valid authoring destinations (ADR-0070 D2). Code/installed (system|cloud)
7
+ * packages are filtered out.
8
+ *
9
+ * There is no package-less "Local / Custom" scope: every runtime-authored item
10
+ * lives in a writable base (ADR-0070 D1/D5 — the kernel rejects orphan creates
11
+ * with `writable_package_required`, and legacy orphans are adopted into a base),
12
+ * so the selector never offers an orphan bucket. The kernel keeps `null` /
13
+ * `sys_metadata` provenance only as a read-side rehydration tag for legacy rows.
25
14
  */
26
15
  export function buildPackageScopeOptions(rawList) {
27
16
  const rows = (rawList ?? [])
@@ -36,24 +25,5 @@ export function buildPackageScopeOptions(rawList) {
36
25
  })
37
26
  .filter((p) => p.id && !SYSTEM_SCOPES.has(p.scope));
38
27
  rows.sort((a, b) => a.name.localeCompare(b.name));
39
- const opts = rows.map((p) => ({ id: p.id, name: p.name }));
40
- // Append (never default) so the existing first-code-package default is
41
- // preserved; the user opts into the local scope explicitly.
42
- return [...opts, { id: LOCAL_PACKAGE_ID, name: t('engine.package.local', detectLocale()) }];
43
- }
44
- /**
45
- * True for the runtime/null "Local / Custom" sentinel scope. Per ADR-0070 D5
46
- * this is a *migration* surface (move loose items into a base), never a valid
47
- * create destination — callers gate "create" on a real writable base.
48
- */
49
- export function isLocalScope(id) {
50
- return !id || id === LOCAL_PACKAGE_ID;
51
- }
52
- /**
53
- * The writable bases (project-scoped DB packages) from the raw package list —
54
- * the only valid authoring destinations (ADR-0070 D2). Excludes code/installed
55
- * (system|cloud) packages AND the Local sentinel.
56
- */
57
- export function writableBaseOptions(rawList) {
58
- return buildPackageScopeOptions(rawList).filter((o) => o.id !== LOCAL_PACKAGE_ID);
28
+ return rows.map((p) => ({ id: p.id, name: p.name }));
59
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -33,36 +33,36 @@
33
33
  "qrcode": "^1.5.4",
34
34
  "sonner": "^2.0.7",
35
35
  "zod": "^4.4.3",
36
- "@object-ui/auth": "7.2.0",
37
- "@object-ui/collaboration": "7.2.0",
38
- "@object-ui/components": "7.2.0",
39
- "@object-ui/core": "7.2.0",
40
- "@object-ui/data-objectstack": "7.2.0",
41
- "@object-ui/fields": "7.2.0",
42
- "@object-ui/i18n": "7.2.0",
43
- "@object-ui/layout": "7.2.0",
44
- "@object-ui/permissions": "7.2.0",
45
- "@object-ui/plugin-editor": "7.2.0",
46
- "@object-ui/providers": "7.2.0",
47
- "@object-ui/react": "7.2.0",
48
- "@object-ui/types": "7.2.0"
36
+ "@object-ui/auth": "7.3.0",
37
+ "@object-ui/collaboration": "7.3.0",
38
+ "@object-ui/components": "7.3.0",
39
+ "@object-ui/core": "7.3.0",
40
+ "@object-ui/fields": "7.3.0",
41
+ "@object-ui/data-objectstack": "7.3.0",
42
+ "@object-ui/i18n": "7.3.0",
43
+ "@object-ui/layout": "7.3.0",
44
+ "@object-ui/permissions": "7.3.0",
45
+ "@object-ui/plugin-editor": "7.3.0",
46
+ "@object-ui/providers": "7.3.0",
47
+ "@object-ui/react": "7.3.0",
48
+ "@object-ui/types": "7.3.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^18.0.0 || ^19.0.0",
52
52
  "react-dom": "^18.0.0 || ^19.0.0",
53
53
  "react-router-dom": "^6.0.0 || ^7.0.0",
54
- "@object-ui/plugin-calendar": "^7.2.0",
55
- "@object-ui/plugin-charts": "^7.2.0",
56
- "@object-ui/plugin-chatbot": "^7.2.0",
57
- "@object-ui/plugin-dashboard": "^7.2.0",
58
- "@object-ui/plugin-designer": "^7.2.0",
59
- "@object-ui/plugin-detail": "^7.2.0",
60
- "@object-ui/plugin-form": "^7.2.0",
61
- "@object-ui/plugin-grid": "^7.2.0",
62
- "@object-ui/plugin-kanban": "^7.2.0",
63
- "@object-ui/plugin-list": "^7.2.0",
64
- "@object-ui/plugin-report": "^7.2.0",
65
- "@object-ui/plugin-view": "^7.2.0"
54
+ "@object-ui/plugin-calendar": "^7.3.0",
55
+ "@object-ui/plugin-chatbot": "^7.3.0",
56
+ "@object-ui/plugin-charts": "^7.3.0",
57
+ "@object-ui/plugin-dashboard": "^7.3.0",
58
+ "@object-ui/plugin-designer": "^7.3.0",
59
+ "@object-ui/plugin-detail": "^7.3.0",
60
+ "@object-ui/plugin-form": "^7.3.0",
61
+ "@object-ui/plugin-kanban": "^7.3.0",
62
+ "@object-ui/plugin-grid": "^7.3.0",
63
+ "@object-ui/plugin-list": "^7.3.0",
64
+ "@object-ui/plugin-report": "^7.3.0",
65
+ "@object-ui/plugin-view": "^7.3.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^26.0.0",
@@ -75,18 +75,18 @@
75
75
  "sonner": "^2.0.7",
76
76
  "typescript": "^6.0.3",
77
77
  "vite": "^8.0.16",
78
- "@object-ui/plugin-calendar": "7.2.0",
79
- "@object-ui/plugin-charts": "7.2.0",
80
- "@object-ui/plugin-chatbot": "7.2.0",
81
- "@object-ui/plugin-dashboard": "7.2.0",
82
- "@object-ui/plugin-designer": "7.2.0",
83
- "@object-ui/plugin-detail": "7.2.0",
84
- "@object-ui/plugin-form": "7.2.0",
85
- "@object-ui/plugin-grid": "7.2.0",
86
- "@object-ui/plugin-kanban": "7.2.0",
87
- "@object-ui/plugin-list": "7.2.0",
88
- "@object-ui/plugin-report": "7.2.0",
89
- "@object-ui/plugin-view": "7.2.0"
78
+ "@object-ui/plugin-calendar": "7.3.0",
79
+ "@object-ui/plugin-charts": "7.3.0",
80
+ "@object-ui/plugin-chatbot": "7.3.0",
81
+ "@object-ui/plugin-dashboard": "7.3.0",
82
+ "@object-ui/plugin-designer": "7.3.0",
83
+ "@object-ui/plugin-detail": "7.3.0",
84
+ "@object-ui/plugin-form": "7.3.0",
85
+ "@object-ui/plugin-grid": "7.3.0",
86
+ "@object-ui/plugin-kanban": "7.3.0",
87
+ "@object-ui/plugin-list": "7.3.0",
88
+ "@object-ui/plugin-report": "7.3.0",
89
+ "@object-ui/plugin-view": "7.3.0"
90
90
  },
91
91
  "keywords": [
92
92
  "objectui",