From 0b70b291f824fca9989bd7d452cda42fe978129c Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:11:26 +0800 Subject: [PATCH 1/2] feat(home): surface pending drafts + make Studio reachable in a fresh env (P0 part 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the visibility half of the AI magic-moment loop and fixes a chicken-and-egg that stranded new users: - HomePage: a 'You have N unpublished changes → Review & publish' banner (driven by MetadataClient.listDrafts()), shown wherever the user lands. After AI drafts a whole system, they're no longer stranded on an empty-looking home with no trace of it. - AppContent: exempt /metadata routes from the 'no apps configured' guard. A brand-new env (AI just drafted everything → ZERO published apps) could not reach the Studio/metadata designer AT ALL to review+publish those first drafts — the guard blocked every /apps/* route. Now the designer renders shell-less (like /create-app and /system already do). Known remaining layer (P0 part 2, needs a product call): Studio is project-package-scoped and the AI binds drafts to a default package it doesn't list, so one-click 'publish my AI drafts' isn't wired yet. Co-Authored-By: Claude Opus 4.8 --- packages/app-shell/src/console/AppContent.tsx | 20 +++++++-- .../app-shell/src/console/home/HomePage.tsx | 43 ++++++++++++++++++- packages/i18n/src/locales/en.ts | 4 ++ packages/i18n/src/locales/zh.ts | 4 ++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/app-shell/src/console/AppContent.tsx b/packages/app-shell/src/console/AppContent.tsx index 56a90be17..cb0e9ae5d 100644 --- a/packages/app-shell/src/console/AppContent.tsx +++ b/packages/app-shell/src/console/AppContent.tsx @@ -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 (
{t('empty.noAppsConfigured')} @@ -366,7 +372,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
); - if (!activeApp && (isCreateAppRoute || isSystemRoute)) { + if (!activeApp && (isCreateAppRoute || isSystemRoute || isMetadataRoute)) { return ( }> @@ -374,6 +380,14 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = } /> } /> } /> + {/* Studio / metadata designer — reachable with no active app so a + fresh env can review + publish its first (AI-authored) drafts. */} + } /> + } /> + } /> + } /> + } /> + } /> {extraRoutesNoApp} diff --git a/packages/app-shell/src/console/home/HomePage.tsx b/packages/app-shell/src/console/home/HomePage.tsx index f403b9aa1..b88a8fff1 100644 --- a/packages/app-shell/src/console/home/HomePage.tsx +++ b/packages/app-shell/src/console/home/HomePage.tsx @@ -27,7 +27,8 @@ 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'; function pickGreetingKey(hour: number): string { if (hour < 5) return 'home.greetingNight'; @@ -102,6 +103,44 @@ 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 navigate = useNavigate(); + const client = useMetadataClient(); + const [count, setCount] = useState(0); + useEffect(() => { + let cancelled = false; + Promise.resolve(client.listDrafts?.({})) + .then((drafts) => { if (!cancelled && Array.isArray(drafts)) setCount(drafts.length); }) + .catch(() => { /* drafts unsupported / error → don't show */ }); + return () => { cancelled = true; }; + }, [client]); + if (count <= 0) return null; + return ( +
+
+
+ +

+ {t('home.pendingDrafts.message', { count, defaultValue: 'You have {{count}} unpublished change(s) — review and publish to make them live.' })} +

+ +
+
+
+ ); +} + /** * 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 @@ -183,6 +222,7 @@ export function HomePage() { if (activeApps.length === 0) { return (
+
@@ -228,6 +268,7 @@ export function HomePage() { the gradient display name in the hero. */} + {/* Hero */} diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index aa6a01749..ceb873e59 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1460,6 +1460,10 @@ const en = { cta: 'Set password', dismiss: 'Dismiss', }, + pendingDrafts: { + message: 'You have {{count}} unpublished change(s) — review and publish to make them live.', + cta: 'Review & publish', + }, createFirstApp: 'Create app manually', systemSettings: 'System Settings', browseMarketplace: 'Browse App Marketplace', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 7e60f6520..16bddada0 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -1473,6 +1473,10 @@ const zh = { cta: '设置密码', dismiss: '关闭', }, + pendingDrafts: { + message: '你有 {{count}} 项未发布的更改 —— 审核并发布即可生效。', + cta: '查看并发布', + }, createFirstApp: '手动创建应用', systemSettings: '系统设置', browseMarketplace: '浏览应用市场', From 67eb1a8c64afaba0ca95bf9b2a51a0289577c578 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:21:34 +0800 Subject: [PATCH 2/2] feat(home): one-click publish AI drafts from the banner (closes the loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 part 2 (per product call: pre-PMF activation > the draft-review gate). The pending-drafts banner's button now publishes the draft package(s) directly via POST /api/v1/packages/:id/publish-drafts (same-origin, credentials:'include', mirroring PackagesPage), then reloads so the home surfaces the now-live app. Verified end-to-end: 'describe a CRM' → AI drafts 12 items → banner '12 unpublished changes → Publish' → one click → publish-drafts 200 → home shows the live CRM app → opens to its dashboard with 客户/联系人/商机. No stranding, no hunting for a designer. Co-Authored-By: Claude Opus 4.8 --- .../app-shell/src/console/home/HomePage.tsx | 49 +++++++++++++++++-- packages/i18n/src/locales/en.ts | 7 ++- packages/i18n/src/locales/zh.ts | 7 ++- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/app-shell/src/console/home/HomePage.tsx b/packages/app-shell/src/console/home/HomePage.tsx index b88a8fff1..c70127670 100644 --- a/packages/app-shell/src/console/home/HomePage.tsx +++ b/packages/app-shell/src/console/home/HomePage.tsx @@ -29,6 +29,7 @@ import { StarredApps } from './StarredApps'; import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components'; 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'; @@ -113,16 +114,52 @@ function StatPill({ * (listDrafts → 0). */ function PendingDraftsBanner({ t }: { t: (key: string, opts?: any) => string }) { - const navigate = useNavigate(); const client = useMetadataClient(); const [count, setCount] = useState(0); + const [pkgIds, setPkgIds] = useState([]); + const [publishing, setPublishing] = useState(false); + useEffect(() => { let cancelled = false; Promise.resolve(client.listDrafts?.({})) - .then((drafts) => { if (!cancelled && Array.isArray(drafts)) setCount(drafts.length); }) + .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 (
@@ -130,10 +167,12 @@ function PendingDraftsBanner({ t }: { t: (key: string, opts?: any) => string })

- {t('home.pendingDrafts.message', { count, defaultValue: 'You have {{count}} unpublished change(s) — review and publish to make them live.' })} + {t('home.pendingDrafts.message', { count, defaultValue: 'You have {{count}} unpublished change(s) — publish to make them live.' })}

-
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index ceb873e59..ffb1b8e4e 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1461,8 +1461,11 @@ const en = { dismiss: 'Dismiss', }, pendingDrafts: { - message: 'You have {{count}} unpublished change(s) — review and publish to make them live.', - cta: 'Review & publish', + 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', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 16bddada0..f9f68c972 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -1474,8 +1474,11 @@ const zh = { dismiss: '关闭', }, pendingDrafts: { - message: '你有 {{count}} 项未发布的更改 —— 审核并发布即可生效。', - cta: '查看并发布', + message: '你有 {{count}} 项未发布的更改 —— 点击发布即可生效。', + cta: '发布', + publishing: '发布中…', + published: '已发布!更改已生效。', + publishFailed: '发布失败', }, createFirstApp: '手动创建应用', systemSettings: '系统设置',