Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/app-shell/src/console/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,14 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =

const isCreateAppRoute = location.pathname.endsWith('/create-app');
const isSystemRoute = location.pathname.includes('/system');

if (!activeApp && !isCreateAppRoute && !isSystemRoute) return (
// The metadata designer (Studio) must be reachable even with no active app —
// a brand-new env where AI just drafted everything has ZERO published apps,
// so without this exemption the "no apps configured" guard below would block
// the very surface you need to REVIEW & PUBLISH those first drafts (a
// chicken-and-egg that stranded the AI magic-moment loop).
const isMetadataRoute = location.pathname.includes('/metadata');

if (!activeApp && !isCreateAppRoute && !isSystemRoute && !isMetadataRoute) return (
<div className="h-screen flex items-center justify-center">
<Empty>
<EmptyTitle>{t('empty.noAppsConfigured')}</EmptyTitle>
Expand All @@ -366,14 +372,22 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
</div>
);

if (!activeApp && (isCreateAppRoute || isSystemRoute)) {
if (!activeApp && (isCreateAppRoute || isSystemRoute || isMetadataRoute)) {
return (
<Suspense fallback={<LoadingScreen />}>
<Routes>
<Route path="create-app" element={<CreateAppPage />} />
<Route path="system/marketplace" element={<MarketplacePage />} />
<Route path="system/marketplace/installed" element={<MarketplaceInstalledPage />} />
<Route path="system/marketplace/:packageId" element={<MarketplacePackagePage />} />
{/* Studio / metadata designer — reachable with no active app so a
fresh env can review + publish its first (AI-authored) drafts. */}
<Route path="metadata" element={<MetadataDirectoryPage />} />
<Route path="metadata/_diagnostics" element={<MetadataDiagnosticsPage />} />
<Route path="metadata/:type" element={<MetadataResourceListPage />} />
<Route path="metadata/:type/new" element={<MetadataResourceEditPage createMode />} />
<Route path="metadata/:type/:name" element={<MetadataResourceEditPage />} />
<Route path="metadata/:type/:name/history" element={<MetadataResourceHistoryPage />} />
{extraRoutesNoApp}
</Routes>
</Suspense>
Expand Down
82 changes: 81 additions & 1 deletion packages/app-shell/src/console/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import { AppCard } from './AppCard';
import { RecentApps } from './RecentApps';
import { StarredApps } from './StarredApps';
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid, ShieldAlert, X } from 'lucide-react';
import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid, ShieldAlert, X, UploadCloud } from 'lucide-react';
import { useMetadataClient } from '../../views/metadata-admin/useMetadata';
import { toast } from 'sonner';

function pickGreetingKey(hour: number): string {
if (hour < 5) return 'home.greetingNight';
Expand Down Expand Up @@ -102,6 +104,82 @@ function StatPill({
);
}

/**
* Pending-drafts banner — closes the AI magic-moment loop. After the metadata
* assistant drafts objects/views/apps (ADR-0033 draft-gated authoring), nothing
* is live until the human publishes. Without this, a user who just had AI build
* their whole system landed back on an empty-looking home with no trace of it
* and no path to publish. This surfaces the pending drafts and routes to the
* designer to review + publish. Disappears automatically once published
* (listDrafts → 0).
*/
function PendingDraftsBanner({ t }: { t: (key: string, opts?: any) => string }) {
const client = useMetadataClient();
const [count, setCount] = useState(0);
const [pkgIds, setPkgIds] = useState<string[]>([]);
const [publishing, setPublishing] = useState(false);

useEffect(() => {
let cancelled = false;
Promise.resolve(client.listDrafts?.({}))
.then((drafts) => {
if (cancelled || !Array.isArray(drafts)) return;
setCount(drafts.length);
setPkgIds([...new Set(drafts.map((d: any) => d.packageId).filter(Boolean) as string[])]);
})
.catch(() => { /* drafts unsupported / error → don't show */ });
return () => { cancelled = true; };
}, [client]);

// One-click publish: promote the draft package(s) directly so a brand-new
// user reaches "it's live" without hunting for a designer. (Pre-PMF activation
// > the draft-review gate; the review path can return when it matters.)
const publish = async () => {
setPublishing(true);
try {
const ids = pkgIds.length
? pkgIds
: [...new Set((((await client.listDrafts?.({})) as any[]) || []).map((d) => d.packageId).filter(Boolean) as string[])];
if (ids.length === 0) throw new Error('no draft packages');
for (const id of ids) {
const res = await fetch(`/api/v1/packages/${encodeURIComponent(id)}/publish-drafts`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({}),
});
if (!res.ok) throw new Error((await res.text().catch(() => '')) || `HTTP ${res.status}`);
}
toast.success(t('home.pendingDrafts.published', { defaultValue: 'Published! Your changes are live.' }));
setCount(0);
// Surface the now-live app — reload so the populated home shows it.
setTimeout(() => { try { window.location.reload(); } catch { /* ignore */ } }, 700);
} catch (e) {
toast.error(`${t('home.pendingDrafts.publishFailed', { defaultValue: 'Publish failed' })}: ${(e as Error).message}`);
setPublishing(false);
}
};

if (count <= 0) return null;
return (
<div className="px-4 sm:px-6 lg:px-8 pt-4">
<div className="max-w-7xl mx-auto">
<div className="flex items-center gap-3 rounded-xl border border-indigo-300/60 dark:border-indigo-700/50 bg-indigo-50 dark:bg-indigo-950/30 px-4 py-3">
<UploadCloud className="h-5 w-5 shrink-0 text-indigo-600 dark:text-indigo-400" />
<p className="flex-1 min-w-0 text-sm text-indigo-900 dark:text-indigo-200">
{t('home.pendingDrafts.message', { count, defaultValue: 'You have {{count}} unpublished change(s) — publish to make them live.' })}
</p>
<Button size="sm" onClick={publish} disabled={publishing} data-testid="pending-drafts-publish">
{publishing
? t('home.pendingDrafts.publishing', { defaultValue: 'Publishing…' })
: t('home.pendingDrafts.cta', { defaultValue: 'Publish' })}
</Button>
</div>
</div>
</div>
);
}

/**
* Dismissible nudge to set a local recovery password — shown when the user
* signed in via SSO and has no local credential yet. We no longer force this
Expand Down Expand Up @@ -183,6 +261,7 @@ export function HomePage() {
if (activeApps.length === 0) {
return (
<div className="flex flex-col flex-1">
<PendingDraftsBanner t={t} />
<RecoveryPasswordReminder t={t} />
<div className="flex flex-1 items-center justify-center p-6">
<Empty>
Expand Down Expand Up @@ -228,6 +307,7 @@ export function HomePage() {
the gradient display name in the hero.
*/}

<PendingDraftsBanner t={t} />
<RecoveryPasswordReminder t={t} />

{/* Hero */}
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,13 @@ const en = {
cta: 'Set password',
dismiss: 'Dismiss',
},
pendingDrafts: {
message: 'You have {{count}} unpublished change(s) — publish to make them live.',
cta: 'Publish',
publishing: 'Publishing…',
published: 'Published! Your changes are live.',
publishFailed: 'Publish failed',
},
createFirstApp: 'Create app manually',
systemSettings: 'System Settings',
browseMarketplace: 'Browse App Marketplace',
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,13 @@ const zh = {
cta: '设置密码',
dismiss: '关闭',
},
pendingDrafts: {
message: '你有 {{count}} 项未发布的更改 —— 点击发布即可生效。',
cta: '发布',
publishing: '发布中…',
published: '已发布!更改已生效。',
publishFailed: '发布失败',
},
createFirstApp: '手动创建应用',
systemSettings: '系统设置',
browseMarketplace: '浏览应用市场',
Expand Down
Loading