Uh oh!
There was an error while loading. Please reload this page.
feat: URL-driven debug/developer panel (à la amis devtools) - #844
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ok, and DebugPanel - @object-ui/core: export DebugCategory, DebugFlags, parseDebugFlags(), isDebugEnabled() with URL/env/globalThis support - @object-ui/react: add useDebugMode hook with URL detection, Ctrl+Shift+D shortcut, manual toggle - @object-ui/react: enhance SchemaRendererContext with debugFlags prop - @object-ui/components: add DebugPanel floating panel with Schema/Data/Registry/Flags tabs - Add comprehensive tests (32 new test cases across 3 test files) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
hotlong
commented
Feb 24, 2026
@copilot 仔细评估是否完成任务要求的所有功能 |
…, SchemaRenderer debug attrs, MetadataInspector auto-open - @object-ui/core: add DebugCollector (perf/expr/event data collection, tree-shakeable singleton) - @object-ui/components: add Perf/Expr/Events tabs to DebugPanel (7 built-in tabs total) - @object-ui/react: SchemaRenderer injects data-debug-type/id attrs + reports render perf to DebugCollector when debug enabled - apps/console: MetadataInspector auto-opens when ?__debug URL param is present - Add 16 new tests (DebugCollector: 10, DebugPanel tabs: 3, SchemaRenderer debug: 3) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Completed all missing features in commit 1. URL-Driven Debug Mode ✅
2. Debug Panel Tabs (7/7) ✅
3. SchemaRenderer Enhancement ✅
4. Console MetadataInspector ✅
5. Code Decoupling ✅
Tests: 48 new tests across 5 files. All 512 react tests + 744 core tests pass. |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Pull request overview
Introduces a URL-driven debug/developer mode across ObjectUI, enabling a floating DebugPanel UI and adding core utilities to parse debug flags and collect debug telemetry (perf/expr/events), with React runtime wiring to propagate flags and attach debug attributes.
Changes:
- Added
DebugFlagsparsing (parseDebugFlags) andDebugCollectorsingleton to@object-ui/core, plus enhancedisDebugEnabled()resolution order. - Added
useDebugMode()hook anddebugFlagspropagation viaSchemaRendererContext;SchemaRenderernow injectsdata-debug-*attributes and reports perf entries. - Added
DebugPanel(plugin-extensible tabs) to@object-ui/components, and updated ConsoleMetadataInspectorto auto-open when?__debugis present.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/hooks/useDebugMode.ts | New hook to derive debug state from URL, manual toggle, and keyboard shortcut. |
| packages/react/src/hooks/index.ts | Re-export useDebugMode. |
| packages/react/src/hooks/tests/useDebugMode.test.ts | Unit tests for useDebugMode. |
| packages/react/src/context/SchemaRendererContext.tsx | Adds optional debugFlags to renderer context/provider. |
| packages/react/src/tests/SchemaRenderer.debug.test.tsx | Tests for data-debug-* attribute injection behavior. |
| packages/react/src/SchemaRenderer.tsx | Injects debug attributes and reports render perf to DebugCollector. |
| packages/core/src/utils/debug.ts | Exports DebugCategory, adds DebugFlags, parseDebugFlags(), and enhanced isDebugEnabled(). |
| packages/core/src/utils/debug-collector.ts | New DebugCollector singleton collecting perf/expr/event entries with subscriptions and caps. |
| packages/core/src/utils/tests/debug.test.ts | Adds tests for parseDebugFlags() and isDebugEnabled(). |
| packages/core/src/utils/tests/debug-collector.test.ts | Adds tests for DebugCollector. |
| packages/core/src/index.ts | Re-exports debug-collector. |
| packages/components/src/index.ts | Re-exports debug components (./debug). |
| packages/components/src/debug/index.ts | New debug barrel export for DebugPanel. |
| packages/components/src/debug/tests/DebugPanel.test.tsx | Adds tests for DebugPanel rendering, tabs, accessibility, and empty states. |
| packages/components/src/debug/DebugPanel.tsx | New floating debug panel with built-in tabs and plugin extension points. |
| apps/console/src/components/MetadataInspector.tsx | Auto-opens MetadataInspector when ?__debug is detected. |
| ROADMAP.md | Marks URL-driven debug/developer panel as completed. |
| debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`); | ||
| const renderStart = isDebug ? performance.now() : 0; | ||
| const rendered = ( |
There was a problem hiding this comment.
performance.now() is used when isDebug is true, but performance is not guaranteed to exist in SSR/non-browser runtimes (even when debug is enabled via env/globalThis). This can throw and break rendering. Guard with typeof performance !== 'undefined' (or globalThis.performance?.now) and skip perf tracking / fall back when unavailable.
| debugLog, | ||
| debugTime, | ||
| debugTimeEnd, | ||
| DebugCollector, |
There was a problem hiding this comment.
DebugCollector is imported unconditionally in SchemaRenderer, which means the collector code is pulled into the main runtime bundle even when the DebugPanel is never imported. This contradicts the “tree-shakeable / only included when imported” goal in the PR description; consider restructuring so perf collection wiring is in a debug-only entrypoint or otherwise optional.
| DebugCollector, |
| function SchemaTab({ schema }: { schema?: unknown }) { | ||
| if (!schema) { | ||
| return <p className="text-xs text-muted-foreground italic">No schema available</p>; | ||
| } |
There was a problem hiding this comment.
SchemaTab treats any falsy schema as missing. If schema can ever legitimately be 0, false, or an empty string (e.g., debugging a primitive node), this will incorrectly show the empty state. Prefer checking schema == null (null/undefined) instead of !schema.
| } | ||
| function DataTab({ dataContext }: { dataContext?: unknown }) { | ||
| if (!dataContext) { |
There was a problem hiding this comment.
DataTab treats any falsy dataContext as missing. If the active data context can be 0, false, or '', the panel will incorrectly show the empty state. Prefer checking dataContext == null (null/undefined) instead of !dataContext.
| if(!dataContext){ | |
| if(dataContext==null){ |
| function RegistryTab() { | ||
| const entries = useMemo(() => { | ||
| try { | ||
| return ComponentRegistry.getAllTypes(); | ||
| } catch { | ||
| return []; | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
ComponentRegistry.getAllTypes() returns all internal registry keys, which include backward-compatibility fallback keys (e.g., both ui:button and button). This will show duplicates and inflate the “N components registered” count in the Registry tab. Consider de-duping (e.g., new Set(...)) or filtering out fallback keys / preferring namespaced keys for display.
| const urlFlags = useMemo(() => { | ||
| try { | ||
| return typeof window !== 'undefined' | ||
| ? parseDebugFlags(window.location.search) | ||
| : { enabled: false } as DebugFlags; | ||
| } catch { | ||
| return { enabled: false } as DebugFlags; | ||
| } |
There was a problem hiding this comment.
urlFlags is memoized with an empty dependency array, so changes to window.location.search after mount (client-side navigation, manual URL edits, history.pushState) will not update enabled/flags. If the intent is truly URL-driven debug mode, consider listening to popstate (and/or patching pushState/replaceState via an event) and re-parsing flags when the URL changes.
| consturlFlags=useMemo(()=>{ | |
| try{ | |
| returntypeofwindow!=='undefined' | |
| ? parseDebugFlags(window.location.search) | |
| : {enabled: false}asDebugFlags; | |
| }catch{ | |
| return{enabled: false}asDebugFlags; | |
| } | |
| const[urlFlags,setUrlFlags]=useState<DebugFlags>(()=>{ | |
| try{ | |
| returntypeofwindow!=='undefined' | |
| ? parseDebugFlags(window.location.search) | |
| : ({enabled: false}asDebugFlags); | |
| }catch{ | |
| return{enabled: false}asDebugFlags; | |
| } | |
| }); | |
| useEffect(()=>{ | |
| if(typeofwindow==='undefined')return; | |
| consthandleLocationChange=()=>{ | |
| try{ | |
| setUrlFlags(parseDebugFlags(window.location.search)); | |
| }catch{ | |
| setUrlFlags({enabled: false}asDebugFlags); | |
| } | |
| }; | |
| window.addEventListener('popstate',handleLocationChange); | |
| return()=>{ | |
| window.removeEventListener('popstate',handleLocationChange); | |
| }; |
| // Report render perf to DebugCollector when debug mode is active | ||
| if (isDebug && renderStart) { | ||
| const durationMs = performance.now() - renderStart; | ||
| DebugCollector.getInstance().addPerf({ | ||
| type: evaluatedSchema.type, | ||
| id: evaluatedSchema.id, | ||
| durationMs, | ||
| timestamp: Date.now(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
DebugCollector.getInstance().addPerf(...) is executed during the SchemaRenderer render path and synchronously notifies subscribers. If DebugPanel is subscribed, this can trigger setState while another component is rendering (React warning: “Cannot update a component while rendering a different component”) and can also record aborted/strict-mode double renders. Consider deferring collection to an effect/commit phase (or making subscriber notifications async) to keep renders side-effect free.
…t create dialog (#2631) Staging E2E (2026-07-17): the welcome hero's "Create your environment" navigated to the environments list, where the user had to find and click a SECOND create button — an extra hop on the very first thing a new user does (#844). - action:button: client-side `autoTrigger` flag — runs the action once on mount through the exact same execute path as a click (param dialog, confirm, entitlement gate all apply). Not persisted metadata; only client- composed schemas set it. - EnvironmentListToolbar: consume `?runAction=create_environment` once entitlements resolve — setup_production / add_development mark the create action autoTrigger; upgrade-locked orgs open the upgrade prompt (the honest answer to "create" there). Param is stripped on consumption so refresh / back don't re-open the dialog. Router-free (location + replaceState) so non-Router hosts and tests keep working. - CloudOnboardingNext: the create CTA navigates with the runAction param. - i18n: the toolbar's state-aware label overrides were hard-coded English in a zh console — now {en,zh} via the same pick() pattern as the widget. Closes#844. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): login-page config race + sign-in watchdog — never strand SSO-only users on a password wall Staging E2E (2026-07-17) on a freshly provisioned environment: the login page's FIRST load rendered the plain password form — no "Continue with ObjectStack", no ssoEnforced collapse — because the /auth/config fetch hung or failed while the kernel cold-started, and both LoginForm and SocialSignInButtons silently fell back to defaults. Platform-SSO JIT users have no password, so this dead-ends the "open your environment" moment (#2625). Clicking the SSO button inside the same cold-start window hung the POST forever with the button stuck spinning (#2626). - getConfig: single-flight + success cache (3 requests → 1) with retrying backoff (500ms/1.5s/3.5s, 8s per-attempt AbortController timeout) so a hung request converts into a retry; final failure clears the cache. - LoginForm: hold a spinner until config resolves; on resolve, honour ssoEnforced on first paint. On final failure keep the old safe default (password form) — break-glass beats lock-out. - signInWithProvider: 20s watchdog rejects a hung sign-in so the #2458 button contract (pending + inline error) can recover it; legacy oauth2 fallback failures no longer mask the social-route error. - Drop LoginForm's duplicate "or" divider (SocialSignInButtons already renders one) — the stacked dividers read as a glitch. Closes#2625. Closes#2626. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app-shell,components): welcome CTA deep-links into the environment create dialog Staging E2E (2026-07-17): the welcome hero's "Create your environment" navigated to the environments list, where the user had to find and click a SECOND create button — an extra hop on the very first thing a new user does (#844). - action:button: client-side `autoTrigger` flag — runs the action once on mount through the exact same execute path as a click (param dialog, confirm, entitlement gate all apply). Not persisted metadata; only client- composed schemas set it. - EnvironmentListToolbar: consume `?runAction=create_environment` once entitlements resolve — setup_production / add_development mark the create action autoTrigger; upgrade-locked orgs open the upgrade prompt (the honest answer to "create" there). Param is stripped on consumption so refresh / back don't re-open the dialog. Router-free (location + replaceState) so non-Router hosts and tests keep working. - CloudOnboardingNext: the create CTA navigates with the runAction param. - i18n: the toolbar's state-aware label overrides were hard-coded English in a zh console — now {en,zh} via the same pick() pattern as the widget. Closes#844. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chatbot): plan approval flips the card to a Building… badge immediately Staging E2E (2026-07-17): clicking 开始搭建 (plan approve) showed no change at the card for ~10s — the approval sends a chat message whose visible effects (user bubble + streaming turn) land at the BOTTOM of the thread, outside the viewport when the card is in view — so users assumed the click was lost and clicked again (#2627). - Track approved plan ids locally; the clicked card's buttons flip to a spinning "Building…" badge on click (both the structured plan card and the unstructured fallback gate). Built state still derives from the message stream (#432 semantics unchanged). - An approval that never left the client (unsent error) rolls the badge back so the button returns; a newer typed/suggestion send supersedes the approve as "last send" so ITS failure can't roll back a delivered approval. - New planBuildingLabel prop; AiChatPage passes 正在搭建… for zh conversations. Partially addresses #2627 — the conversation-history-clears-after-build race needs a live repro and stays open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): drop RegisterForm's duplicate "or" divider (register page had the same glitch as login) Live staging verification (2026-07-17, cloud.objectos.app/_console/register) caught that #2629 fixed the double divider on the LOGIN page but the SIGN-UP page still stacked "OR CONTINUE WITH EMAIL" (from SocialSignInButtons) over a second "OR" (from RegisterForm). Remove RegisterForm's redundant AuthDivider — same fix as LoginForm — and clean up the now-unused hasSocialProviders state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
) Four surfaces picked their language with a hand-written `startsWith('zh')` instead of the locale packs, so ja/ko/de/fr/es/pt/ru/ar silently rendered English and the strings could never be translated without a code change. - RecordTitleChip's private zh-CN/zh-TW dictionary is deleted. Its comment claimed "components is i18n-free"; the package declares @object-ui/i18n and the file it cites already uses it. All four keys already existed in all ten packs — ten locales fixed, zero new translations, on a component that renders on every record detail page. - EnvironmentListToolbar's three CTA labels move to a new `environment.*` namespace (added to all ten packs). This surface regressed once before for the same reason (#844). - StudioAiCopilot's dock title moves to the Studio catalog. - StudioHomePage.relativeTime uses Intl.RelativeTimeFormat instead of five ternaries: every locale, correct plurals, "yesterday"/「昨天」 rather than "1d ago", and Arabic's dual form which a ternary cannot express. EnvironmentListToolbar's tests now render inside a real I18nProvider — without one `t()` returns the raw key, so the previous assertions on literal English were asserting nothing.
Adds a universal
?__debugURL parameter to activate a floating developer panel across all@object-ui/*packages, with fine-grained sub-flags and SSR safety.@object-ui/core— debug flag parsing & data collectionDebugFlags,DebugCategory,parseDebugFlags(search?), enhancedisDebugEnabled()globalThis.OBJECTUI_DEBUG→process.env.OBJECTUI_DEBUG?__debug_schema,?__debug_perf,?__debug_data,?__debug_expr,?__debug_events,?__debug_registryDebugCollectorsingleton for centralized perf/expression/event data collection (tree-shakeable, capped at 200 entries)@object-ui/react— hook + context + renderer enhancementsuseDebugMode()→{ enabled, flags, toggle, setEnabled }withCtrl+Shift+DshortcutSchemaRendererContextextended with optionaldebugFlagspropagationSchemaRendererinjectsdata-debug-type/data-debug-idattributes when debug mode is activeSchemaRendererreports render performance toDebugCollectorfor the Perf tab@object-ui/components— DebugPanelsrc/debug/with 7 built-in tabs:extraTabspropapps/console— MetadataInspector backward compatibilityuseMetadataInspector()auto-opens when?__debugURL parameter is detectedTests
48 new tests across 5 test files: core debug flags (16), DebugCollector (10), useDebugMode hook (6), DebugPanel (13), SchemaRenderer debug attributes (3).
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.