From 8bb6e2096048272c82f8b9bbef07e7a261b4d43b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:22:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(console):=20Build=20Doctor=20drawer=20?= =?UTF-8?q?=E2=80=94=20self-serve=20"what=20actually=20landed=3F"=20for=20?= =?UTF-8?q?AI=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Build Doctor button (build-agent conversations only) to the AI build page header. It opens a right-side drawer that calls the admin build-debug endpoint (objectstack-ai/cloud) and renders the reconciliation: what the agent CLAIMED vs what is LIVE in sys_metadata. The chat renders the agent's self-report ("已搭好"), never what actually landed — so a build that silently dropped a change (a confirm card no turn applied) reads as success. The drawer surfaces exactly that: - PROPOSED-BUT-ORPHANED: a changes_proposed card that evaporated. - CLAIMED-BUT-MISSING: said applied, isn't live. plus de-noised verify_build (platform sys_*/cloud_*/ai_* hidden), pending actions, and a collapsed timeline. Read-only; same-origin cookie auth, no DB credentials — the diagnosis a founder used to do by hand-querying two databases now happens in the browser. Distinct from useReconcileOnError (ADR-0013 D2 stream-failure recovery): this reconciles the BUILD against live metadata, not a transport drop. - buildDebugApi.ts: typed client for GET /conversations/:id/debug. - BuildDebugDrawer.tsx: the Sheet drawer + rendering. - AiChatPage: Build Doctor button (showDebug=isBuildAgent) + drawer wiring. type-check clean; app-shell suite green (929) incl. 2 new drawer tests (orphaned render + endpoint call). Needs the cloud service-ai debug endpoint deployed; absent it the drawer shows an empty state. Co-Authored-By: Claude Opus 4.8 --- .../app-shell/src/console/ai/AiChatPage.tsx | 34 ++- .../src/console/ai/BuildDebugDrawer.tsx | 264 ++++++++++++++++++ .../ai/__tests__/BuildDebugDrawer.test.tsx | 57 ++++ .../app-shell/src/console/ai/buildDebugApi.ts | 86 ++++++ 4 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 packages/app-shell/src/console/ai/BuildDebugDrawer.tsx create mode 100644 packages/app-shell/src/console/ai/__tests__/BuildDebugDrawer.test.tsx create mode 100644 packages/app-shell/src/console/ai/buildDebugApi.ts diff --git a/packages/app-shell/src/console/ai/AiChatPage.tsx b/packages/app-shell/src/console/ai/AiChatPage.tsx index 2f6a02288..ef7d0ecca 100644 --- a/packages/app-shell/src/console/ai/AiChatPage.tsx +++ b/packages/app-shell/src/console/ai/AiChatPage.tsx @@ -36,7 +36,7 @@ import { EmptyDescription, cn, } from '@object-ui/components'; -import { PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react'; +import { Bug, PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react'; import { ChatbotEnhanced, useAgents, @@ -76,6 +76,7 @@ import { import { useReconcileOnError } from '../../hooks/useReconcileOnError'; import { ConversationsSidebar } from './ConversationsSidebar'; import { LiveCanvas } from './LiveCanvas'; +import { BuildDebugDrawer } from './BuildDebugDrawer'; const DEFAULT_AI_PATH = '/api/v1/ai'; @@ -633,6 +634,7 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro const [refreshKey, setRefreshKey] = useState(0); const [titleHints, setTitleHints] = useState>({}); const [shareOpen, setShareOpen] = useState(false); + const [debugOpen, setDebugOpen] = useState(false); const [mobileChatsOpen, setMobileChatsOpen] = useState(false); const { collapsed: chatsCollapsed, @@ -833,6 +835,14 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro publicBaseUrl={publicShareBase} /> )} + {conversationId && isBuildAgent(activeAgent) && ( + + )}
{!chatsCollapsed && ( setShareOpen(true)} + onDebug={() => setDebugOpen(true)} + showDebug={isBuildAgent(activeAgent)} onCanvasOpenChange={handleCanvasOpenChange} /> @@ -932,6 +944,10 @@ interface ChatPaneProps { initialMessages: HydratedUIMessage[]; onSent: (firstUserMessage?: string) => void; onShare: () => void; + /** Opens the Build Doctor drawer (build agent only). */ + onDebug?: () => void; + /** Show the Build Doctor button — true only for build-agent conversations. */ + showDebug?: boolean; /** Reports the Live Canvas preview opening/closing so the page can auto-tuck the chats list. */ onCanvasOpenChange?: (open: boolean) => void; } @@ -948,6 +964,8 @@ function ChatPane({ initialMessages, onSent, onShare, + onDebug, + showDebug, onCanvasOpenChange, }: ChatPaneProps) { const { t } = useObjectTranslation(); @@ -1141,6 +1159,20 @@ function ChatPane({ )}
+ {showDebug && onDebug ? ( + + ) : null}