feat(workspace): expose workspace switcher on desktop/local for credit selection - #15051
feat(workspace): expose workspace switcher on desktop/local for credit selection#15051comfydesigner wants to merge 7 commits into
Conversation
🎭 Playwright: ✅ 1802 passed, 0 failed · 2 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 8.7 MB gzip 🔴 +1.87 kBDetailsSummary
Category Glance App Entry Points — 3.67 kB (baseline 3.67 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.36 MB (baseline 1.36 MB) • 🔴 +2.58 kBGraph editor runtime, canvas, workflow orchestration
Status: 2 added / 2 removed / 1 unchanged Views & Navigation — 124 kB (baseline 124 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 13 added / 13 removed / 4 unchanged Panels & Settings — 565 kB (baseline 565 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 16 unchanged User & Accounts — 27 kB (baseline 27 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 4 unchanged Editors & Dialogs — 125 kB (baseline 125 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 7 added / 7 removed / 1 unchanged UI Components — 67.1 kB (baseline 67.1 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.5 MB (baseline 3.5 MB) • 🔴 +1.17 kBStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 550 kB (baseline 550 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 20 unchanged Vendor & Third-Party — 16.3 MB (baseline 16.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 17 unchanged Other — 14 MB (baseline 14 MB) • 🔴 +1.89 kBBundles that do not match a named category
Status: 69 added / 68 removed / 217 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-11T22:10:56.982Z",
"gitSha": "83d8f6a6d15bad477129aab23fb5666c097fda88",
"branch": "comfydesigner/fe-1584-local-workspace-switcher",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2045.0559999999882,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.201,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 561.4639999999999,
"heapDeltaBytes": 5989944,
"heapUsedBytes": 70315452,
"domNodes": 20,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 22.306,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-idle",
"durationMs": 2016.9049999999515,
"styleRecalcs": 9,
"styleRecalcDurationMs": 10.136,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 568.1160000000001,
"heapDeltaBytes": 6006764,
"heapUsedBytes": 70232740,
"domNodes": 18,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 22.617,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2094.067999999993,
"styleRecalcs": 78,
"styleRecalcDurationMs": 50.63099999999999,
"layouts": 12,
"layoutDurationMs": 4.091,
"taskDurationMs": 1088.768,
"heapDeltaBytes": -10486720,
"heapUsedBytes": 53878976,
"domNodes": -282,
"jsHeapTotalBytes": 24354816,
"scriptDurationMs": 149.18699999999998,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1900.7250000000795,
"styleRecalcs": 75,
"styleRecalcDurationMs": 44.63100000000001,
"layouts": 12,
"layoutDurationMs": 3.4250000000000003,
"taskDurationMs": 970.3620000000001,
"heapDeltaBytes": -12550200,
"heapUsedBytes": 51691940,
"domNodes": -280,
"jsHeapTotalBytes": 23044096,
"scriptDurationMs": 131.90099999999998,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1773.3159999999657,
"styleRecalcs": 32,
"styleRecalcDurationMs": 20.518,
"layouts": 6,
"layoutDurationMs": 0.6769999999999998,
"taskDurationMs": 428.353,
"heapDeltaBytes": 8598476,
"heapUsedBytes": 73001224,
"domNodes": 79,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 25.511,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1737.6779999999599,
"styleRecalcs": 32,
"styleRecalcDurationMs": 20.474,
"layouts": 6,
"layoutDurationMs": 0.747,
"taskDurationMs": 470.49800000000005,
"heapDeltaBytes": 8827064,
"heapUsedBytes": 73226020,
"domNodes": 77,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 25.701,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 610.5919999999969,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.067000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 421.234,
"heapDeltaBytes": -11184920,
"heapUsedBytes": 53097440,
"domNodes": 22,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 67.11500000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 639.6180000000413,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.708999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 422.395,
"heapDeltaBytes": -11198528,
"heapUsedBytes": 53139032,
"domNodes": 18,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 68.996,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2053.2670000000053,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.514000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 759.6659999999999,
"heapDeltaBytes": 7931824,
"heapUsedBytes": 67388352,
"domNodes": -281,
"jsHeapTotalBytes": 3772416,
"scriptDurationMs": 128.962,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2028.2959999999548,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.570999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 772.8560000000001,
"heapDeltaBytes": 7315504,
"heapUsedBytes": 67426108,
"domNodes": -283,
"jsHeapTotalBytes": 3772416,
"scriptDurationMs": 127.856,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2211.041000000023,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.542,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1370.4809999999998,
"heapDeltaBytes": 4112868,
"heapUsedBytes": 64795976,
"domNodes": -285,
"jsHeapTotalBytes": 3440640,
"scriptDurationMs": 449.815,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2324.4309999998904,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.087999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1414.5259999999998,
"heapDeltaBytes": 4568184,
"heapUsedBytes": 65043300,
"domNodes": -285,
"jsHeapTotalBytes": 3178496,
"scriptDurationMs": 474.96,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3329.906000000051,
"styleRecalcs": 64,
"styleRecalcDurationMs": 15.160999999999998,
"layouts": 60,
"layoutDurationMs": 7.607000000000001,
"taskDurationMs": 1633.671,
"heapDeltaBytes": 5074084,
"heapUsedBytes": 67603264,
"domNodes": -290,
"jsHeapTotalBytes": 7180288,
"scriptDurationMs": 569.212,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3463.3580000000848,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.011,
"layouts": 60,
"layoutDurationMs": 8.043,
"taskDurationMs": 1699.995,
"heapDeltaBytes": 11456128,
"heapUsedBytes": 73869692,
"domNodes": -290,
"jsHeapTotalBytes": 5869568,
"scriptDurationMs": 608.7819999999999,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2030.8469999999943,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.486999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 727.1990000000001,
"heapDeltaBytes": 6644808,
"heapUsedBytes": 67220668,
"domNodes": -284,
"jsHeapTotalBytes": 4558848,
"scriptDurationMs": 118.28500000000001,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2037.341999999967,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.295,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 756.6910000000001,
"heapDeltaBytes": 6492348,
"heapUsedBytes": 67214568,
"domNodes": -282,
"jsHeapTotalBytes": 4558848,
"scriptDurationMs": 124.811,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 638.8699999999972,
"styleRecalcs": 46,
"styleRecalcDurationMs": 11.195,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 428.20000000000005,
"heapDeltaBytes": -10076284,
"heapUsedBytes": 54164772,
"domNodes": 18,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 126.689,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 635.7199999999921,
"styleRecalcs": 46,
"styleRecalcDurationMs": 13.677000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 444.75499999999994,
"heapDeltaBytes": -10419016,
"heapUsedBytes": 54099264,
"domNodes": 18,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 127.806,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1997.1950000000334,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.95,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 492.417,
"heapDeltaBytes": 5930488,
"heapUsedBytes": 70138780,
"domNodes": 20,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 16.229,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2005.3449999999202,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.02,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 508.34299999999996,
"heapDeltaBytes": 5726892,
"heapUsedBytes": 70136368,
"domNodes": 16,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 18.636,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.2880000000055,
"styleRecalcs": 76,
"styleRecalcDurationMs": 39.862,
"layouts": 16,
"layoutDurationMs": 4.6979999999999995,
"taskDurationMs": 835.56,
"heapDeltaBytes": -3360580,
"heapUsedBytes": 61146212,
"domNodes": 62,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 102.228,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1733.3889999999883,
"styleRecalcs": 78,
"styleRecalcDurationMs": 40.457,
"layouts": 16,
"layoutDurationMs": 4.819,
"taskDurationMs": 876.405,
"heapDeltaBytes": -3372308,
"heapUsedBytes": 61142220,
"domNodes": 63,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 103.14,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1435.325999999975,
"styleRecalcs": 18,
"styleRecalcDurationMs": 32.657000000000004,
"layouts": 14,
"layoutDurationMs": 18.842000000000002,
"taskDurationMs": 1033.5059999999999,
"heapDeltaBytes": 30953496,
"heapUsedBytes": 98607352,
"domNodes": 13673,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 44.21299999999999,
"eventListeners": 2375,
"totalBlockingTimeMs": 149,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 9057.923000000017,
"styleRecalcs": 249,
"styleRecalcDurationMs": 38.907,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 5084.524,
"heapDeltaBytes": 18785864,
"heapUsedBytes": 78354532,
"domNodes": -283,
"jsHeapTotalBytes": 5275648,
"scriptDurationMs": 1577.7179999999998,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8653.067999999963,
"styleRecalcs": 249,
"styleRecalcDurationMs": 38.681,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4915.958,
"heapDeltaBytes": 16409940,
"heapUsedBytes": 76931272,
"domNodes": -279,
"jsHeapTotalBytes": 6062080,
"scriptDurationMs": 1492.086,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17701.514000000032,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17663.918,
"heapDeltaBytes": -59489640,
"heapUsedBytes": 167211992,
"domNodes": -8312,
"jsHeapTotalBytes": -5709824,
"scriptDurationMs": 601.652,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17170.84900000009,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17153.961,
"heapDeltaBytes": -58251968,
"heapUsedBytes": 167420036,
"domNodes": -8312,
"jsHeapTotalBytes": -6758400,
"scriptDurationMs": 548.406,
"eventListeners": -16385,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.330000000000048,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 22112.638000000003,
"styleRecalcs": 161,
"styleRecalcDurationMs": 22.020999999999958,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 22061.575999999997,
"heapDeltaBytes": -49699440,
"heapUsedBytes": 156416044,
"domNodes": -8312,
"jsHeapTotalBytes": -9641984,
"scriptDurationMs": 996.4300000000002,
"eventListeners": -16383,
"totalBlockingTimeMs": 502,
"frameDurationMs": 17.776666666666763,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 21262.534999999956,
"styleRecalcs": 149,
"styleRecalcDurationMs": 19.782999999999994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21236.424,
"heapDeltaBytes": -51856120,
"heapUsedBytes": 163416248,
"domNodes": -8312,
"jsHeapTotalBytes": -11214848,
"scriptDurationMs": 910.9870000000001,
"eventListeners": -16379,
"totalBlockingTimeMs": 443,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 460.11699999996836,
"styleRecalcs": 12,
"styleRecalcDurationMs": 20.921999999999997,
"layouts": 3,
"layoutDurationMs": 0.538,
"taskDurationMs": 112.65299999999999,
"heapDeltaBytes": 5064960,
"heapUsedBytes": 68617628,
"domNodes": 124,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 9.951000000000002,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 473.97399999999834,
"styleRecalcs": 14,
"styleRecalcDurationMs": 23.769,
"layouts": 4,
"layoutDurationMs": 1.4129999999999998,
"taskDurationMs": 119.41099999999999,
"heapDeltaBytes": 5048976,
"heapUsedBytes": 68558504,
"domNodes": 123,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 10.195999999999998,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughNon-cloud distributions now hydrate workspace context in the background. The legacy user popover displays a workspace selector when workspace data is ready. Workspace authentication and billing use workspace context after hydration. Workspace creation controls remain limited to cloud distributions. Vite supports an optional development gateway for workspace APIs. ChangesWorkspace flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant WorkspaceAuthGate
participant Firebase
participant TeamWorkspaceStore
participant CurrentUserPopoverLegacy
participant WorkspaceSwitcherPopover
participant BillingRouter
WorkspaceAuthGate->>Firebase: wait for authentication initialization
WorkspaceAuthGate->>TeamWorkspaceStore: initialize workspace mode
TeamWorkspaceStore-->>CurrentUserPopoverLegacy: expose ready workspaces
CurrentUserPopoverLegacy->>WorkspaceSwitcherPopover: open on workspace selection
BillingRouter->>TeamWorkspaceStore: read workspace type
TeamWorkspaceStore-->>BillingRouter: return workspace billing context
Possibly related PRs
Suggested labels: Suggested reviewers: Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/topbar/CurrentUserPopoverLegacy.test.ts`:
- Line 179: Add behavioral Vitest coverage in CurrentUserPopoverLegacy.test.ts
for the workspace selector: configure createTestingPinia with
teamWorkspace.initState set to ready and supplied workspaces, verify the
selector is visible only when available, use Testing Library user actions to
open it, and verify it closes after selecting a workspace. Retain coverage for
the unavailable state and assert rendered behavior rather than mocks.
In `@src/components/topbar/CurrentUserPopoverLegacy.vue`:
- Around line 35-51: Replace the clickable workspace-switcher trigger div with
the shared Button component, preserving its click behavior, styling, tooltip,
and workspace-name content. Ensure the button exposes the visible workspace name
as its accessible name and binds aria-expanded to isWorkspaceSwitcherOpen so
keyboard users can activate it with Enter or Space.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1acf40a2-51b0-4f8d-be3d-ed268edbceda
📒 Files selected for processing (6)
src/components/topbar/CurrentUserButton.vuesrc/components/topbar/CurrentUserPopoverLegacy.test.tssrc/components/topbar/CurrentUserPopoverLegacy.vuesrc/platform/workspace/auth/WorkspaceAuthGate.vuesrc/platform/workspace/components/WorkspaceSwitcherPopover.test.tssrc/platform/workspace/components/WorkspaceSwitcherPopover.vue
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #15051 +/- ##
==========================================
- Coverage 81.39% 81.39% -0.01%
==========================================
Files 1873 1873
Lines 107225 107273 +48
Branches 33329 33347 +18
==========================================
+ Hits 87273 87310 +37
+ Misses 19593 19592 -1
- Partials 359 371 +12
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 10 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@vite.config.mts`:
- Around line 168-169: Update the WORKSPACE_DEV_PROXY configuration flow so the
value is loaded from .env.local as well as .env, or explicitly document that
only the current environment source is supported. Apply the change around
dotenvConfig() and the WORKSPACE_DEV_PROXY/workspaceDevProxyConfig symbols.
- Around line 169-181: Update the workspaceDevProxyConfig condition so it only
creates the workspace development proxy when WORKSPACE_DEV_PROXY is set and
DISTRIBUTION is not 'cloud'. Preserve the existing route mappings and
empty-object fallback for cloud distributions or when the proxy is unset.
- Line 178: Update the proxy configuration for WORKSPACE_DEV_PROXY to set secure
to true, preserving upstream TLS certificate verification; do not use this
setting to support self-signed local targets, which should require a separate
explicit opt-in.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c12a13a0-09ec-46cd-b104-a2a318549340
📒 Files selected for processing (2)
src/platform/workspace/auth/WorkspaceAuthGate.vuevite.config.mts
| const WORKSPACE_DEV_PROXY = process.env.WORKSPACE_DEV_PROXY | ||
| const workspaceDevProxyConfig = WORKSPACE_DEV_PROXY |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Expectation: the repository either loads this value with loadEnv
# or documents a shell-exported configuration.
rg -n --hidden --glob '!.git' 'WORKSPACE_DEV_PROXY|loadEnv|dotenv' .Repository: Comfy-Org/ComfyUI_frontend
Length of output: 1888
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- vite.config.mts ---'
sed -n '1,35p;155,190p;280,300p' vite.config.mts
printf '%s\n' '--- environment files and documentation references ---'
find . -maxdepth 3 -type f \( -name '.env*' -o -name 'README*' -o -name '*example*' \) -print
rg -n --hidden --glob '!.git' 'WORKSPACE_DEV_PROXY|\.env\.local|dotenvConfig|loadEnv' \
README* docs .github package.json vite.config.mts apps 2>/dev/null || trueRepository: Comfy-Org/ComfyUI_frontend
Length of output: 4622
Load WORKSPACE_DEV_PROXY from .env.local or document the supported source. dotenvConfig() loads .env, but not .env.local.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@vite.config.mts` around lines 168 - 169, Update the WORKSPACE_DEV_PROXY
configuration flow so the value is loaded from .env.local as well as .env, or
explicitly document that only the current environment source is supported. Apply
the change around dotenvConfig() and the
WORKSPACE_DEV_PROXY/workspaceDevProxyConfig symbols.
Source: MCP tools
…ed workspaces on non-cloud
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/composables/billing/useBillingRouting.test.ts`:
- Around line 45-63: Update the billing routing tests around useBillingRouting
to initialize createTestingPinia according to docs/testing/vitest-patterns.md,
using stubActions: false when real actions are exercised. Replace the
hand-written activeWorkspace getter setup with the useTeamWorkspaceStore public
interface, including the state required for activeWorkspaceBillingRail, while
preserving both existing behavioral assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e6590d72-2bd6-42a7-95b2-50fa8325b2dd
📒 Files selected for processing (4)
src/composables/billing/useBillingRouting.test.tssrc/composables/billing/useBillingRouting.tssrc/stores/authStore.tsvite.config.mts
| it('uses legacy billing off Cloud until a workspace context loads', () => { | ||
| mockIsCloud.value = false | ||
| mockActiveWorkspace.value = team | ||
| mockActiveWorkspace.value = null | ||
|
|
||
| const { type, shouldUseWorkspaceBilling } = useBillingRouting() | ||
|
|
||
| expect(type.value).toBe('legacy') | ||
| expect(shouldUseWorkspaceBilling.value).toBe(false) | ||
| }) | ||
|
|
||
| it('uses workspace billing off Cloud once a workspace context loads', () => { | ||
| mockIsCloud.value = false | ||
| mockActiveWorkspace.value = team | ||
|
|
||
| const { type, shouldUseWorkspaceBilling } = useBillingRouting() | ||
|
|
||
| expect(type.value).toBe('workspace') | ||
| expect(shouldUseWorkspaceBilling.value).toBe(true) | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the required Testing Pinia setup for these cases.
The new tests drive activeWorkspace through the hand-written getter shown at Lines 26-28. This bypasses the real useTeamWorkspaceStore contract, including activeWorkspaceBillingRail, so the assertions can pass while store integration is broken.
As per path instructions: docs/testing/vitest-patterns.md requires store-dependent tests to initialize with createTestingPinia; use stubActions: false when exercising real actions. Initialize a testing Pinia and set state through the store's public interface. Keep the behavioral assertions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/composables/billing/useBillingRouting.test.ts` around lines 45 - 63,
Update the billing routing tests around useBillingRouting to initialize
createTestingPinia according to docs/testing/vitest-patterns.md, using
stubActions: false when real actions are exercised. Replace the hand-written
activeWorkspace getter setup with the useTeamWorkspaceStore public interface,
including the state required for activeWorkspaceBillingRail, while preserving
both existing behavioral assertions.
Source: Path instructions
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/topbar/CurrentUserPopoverLegacy.vue`:
- Around line 91-92: Update the permission gates in
CurrentUserPopoverLegacy.vue: at lines 91-92, render add-credits-button only
when the user has the top-up capability; at lines 145-146, render
manage-plan-menu-item only when the user can manage the active workspace plan.
Do not use workspace or subscription availability as authorization, and ensure
read-only team members see neither control.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 24319a7d-0639-44ef-b4e4-9c1bc04bb53e
📒 Files selected for processing (1)
src/components/topbar/CurrentUserPopoverLegacy.vue
| <Button | ||
| v-else | ||
| v-else-if="canAccessSubscriptionFeatures" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use action-specific permissions for billing controls.
The new conditions use workspace or subscription availability as a proxy for authorization. Keep each action gated by its own capability.
src/components/topbar/CurrentUserPopoverLegacy.vue#L91-L92: Renderadd-credits-buttononly when the user can top up.src/components/topbar/CurrentUserPopoverLegacy.vue#L145-L146: Rendermanage-plan-menu-itemonly when the user can manage the active workspace plan.
The related workspace tests require read-only team members to have neither control.
📍 Affects 1 file
src/components/topbar/CurrentUserPopoverLegacy.vue#L91-L92(this comment)src/components/topbar/CurrentUserPopoverLegacy.vue#L145-L146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/topbar/CurrentUserPopoverLegacy.vue` around lines 91 - 92,
Update the permission gates in CurrentUserPopoverLegacy.vue: at lines 91-92,
render add-credits-button only when the user has the top-up capability; at lines
145-146, render manage-plan-menu-item only when the user can manage the active
workspace plan. Do not use workspace or subscription availability as
authorization, and ensure read-only team members see neither control.
…ission gate, dev proxy hardening
|
Review dispositions for the CodeRabbit pass: Addressed in d397f7a:
Declined, with reasoning:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/topbar/CurrentUserPopoverLegacy.test.ts`:
- Around line 579-597: Add a test alongside the existing workspace switcher
visibility cases using a ready workspace state with workspaces set to an empty
array. Render the component with that state and assert
workspace-switcher-trigger remains absent, covering the showWorkspaceSwitcher
condition for ready-but-empty stores.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1a5738d1-9db4-4d90-856d-f05da4bd50ea
📒 Files selected for processing (3)
src/components/topbar/CurrentUserPopoverLegacy.test.tssrc/components/topbar/CurrentUserPopoverLegacy.vuevite.config.mts
| it('stays hidden while the workspace store is not hydrated', () => { | ||
| renderComponent() | ||
|
|
||
| expect(screen.queryByTestId('workspace-switcher-trigger')).toBeNull() | ||
| }) | ||
|
|
||
| it('shows the trigger and opens the switcher once the store is ready', async () => { | ||
| const { user } = renderComponent(readyWorkspaceState) | ||
|
|
||
| const trigger = screen.getByTestId('workspace-switcher-trigger') | ||
| expect(trigger).toHaveAttribute('aria-expanded', 'false') | ||
| expect(screen.queryByTestId('workspace-switcher-panel')).toBeNull() | ||
|
|
||
| await user.click(trigger) | ||
|
|
||
| expect(screen.getByTestId('workspace-switcher-panel')).toBeInTheDocument() | ||
| expect(trigger).toHaveAttribute('aria-expanded', 'true') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add coverage for the ready-but-empty workspace state.
The suite tests an unhydrated store and a populated ready store. It does not test initState: 'ready' with workspaces: [].
Add a case that verifies the selector remains hidden for that state. This covers the workspaces.value.length > 0 condition in showWorkspaceSwitcher.
As per path instructions, “Cover unloaded, ready, empty, error, and permission edge cases.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/topbar/CurrentUserPopoverLegacy.test.ts` around lines 579 -
597, Add a test alongside the existing workspace switcher visibility cases using
a ready workspace state with workspaces set to an empty array. Render the
component with that state and assert workspace-switcher-trigger remains absent,
covering the showWorkspaceSwitcher condition for ready-but-empty stores.
Source: Path instructions
…omfy-Org#15151) ## Summary Settings → Credits still assumed the legacy `/customers` rail, so its activity table never loaded when the balance came from anywhere else, and Invoice History opened the wrong Stripe customer. ## Changes - **What**: - `UsageLogsTable` owns its first load (`immediate` watch on the billing route). It previously started at `loading = true` and waited for a parent `refresh()`, which `CreditsPanel` only fired from `authStore.lastBalanceUpdateTime` — written solely by the legacy `/customers/balance` success path (`authStore.ts:374`). Any path that does not write it leaves the spinner up forever. - `CreditsPanel` refreshes activity off the balance it renders instead of that legacy-only timestamp, so a top-up refreshes the table on both rails. First hydration is skipped, since the table now loads itself. - Invoice History routes through `useBillingContext().manageSubscription()`, matching `SubscriptionFooterLinks`. The legacy `/customers` portal bills a different Stripe customer than the workspace on screen. ## Review Focus Two independent ways to reach the stuck spinner on `main`: 1. **Local/Desktop, new account** — `authStore.fetchBalance()` returns early on a `404` from `/customers/balance` (`authStore.ts:355-358`), so `lastBalanceUpdateTime` is never written. A user with no customer record yet gets a Credits tab that spins forever. 2. **Cloud, workspace rail** — with `billing_control_enabled` off the standalone Credits panel stays in the sidebar (`useSettingUI.ts:325`) while `useWorkspaceBilling.fetchBalance()` supplies the balance from `/billing/balance` and never touches the timestamp. This also becomes the default failure once the workspace switcher is exposed on Local/Desktop (Comfy-Org#15051). Other notes: - `AutoRefreshWrapper` is gone from the `UsageLogsTable` tests; the component no longer needs a parent to drive its first fetch. Reverting the `immediate` flag alone fails 19 of the 21 tests. - Legacy `manageSubscription()` also starts the cancellation watcher, which the raw `accessBillingPortal()` call did not. `SubscriptionFooterLinks` already accepts that for the same action, and `browser_tests/tests/localCreditsNoSubscribeUi.spec.ts` (which asserts Invoice History issues `POST /customers/billing`) still passes. ## Screenshots Captured from the same local dev-server setup and viewport with a fully mocked Personal workspace on the workspace billing rail. `billing_control_enabled` is off so the standalone Settings → Credits panel is visible. AS-IS uses base `085c6ec51`; TO-BE uses this PR's head `e9fcd6adc`. | AS-IS | TO-BE | | --- | --- | | Activity stays on the loading indicator because no initial request is issued. | Activity loads from `/api/billing/events` on mount. | |  |  | Linear: [FE-1584](https://linear.app/comfyorg/issue/FE-1584/add-workspace-switcher-to-localdesktop-credits-only-no-settings) Prototype this supports: Comfy-Org#15051
Summary
Local/desktop users can't switch workspaces, so credits purchased on any non-Personal workspace are unusable locally (Pylon 15986). This exposes the existing cloud workspace switcher on non-cloud distributions, scoped to credit-wallet selection only — no asset/file semantics, no settings-surface changes.
WorkspaceAuthGate: on non-cloud, hydrate the team workspace store in the background after Firebase auth resolves — never blocks rendering, and the app is untouched if it fails (offline keeps the cached last selection)CurrentUserPopoverLegacy: workspace row + switcher panel (same components as cloud), shown once the store is readyWorkspaceSwitcherPopover: create-workspace footer is now cloud-only — a workspace created from local would be an empty wallet with no local way to subscribe or manage itCurrentUserButton: topbar shows the workspace avatar whenever a non-personal workspace is active (was cloud-only)No new strings. Switch behavior is the existing store path: context clear + reload, selection persisted per device (personal default).
Design: stripped MVP board · popover + menu
Linear: FE-1584
Review notes
CurrentUserPopoverLegacytests gained a testing Pinia (component now reads the team workspace store); switcher tests pinisCloudand cover the hidden footer┆Linear: FE-1584
Status & handoff (2026-08-11)
Verified working end-to-end on localhost dev (switcher renders, switch reloads into the selected workspace, balance follows) via the opt-in dev proxy:
DISTRIBUTION=localhost WORKSPACE_DEV_PROXY=https://testcloud.comfy.org pnpm devwith a local ComfyUI on 8188. Dev builds pair with staging Firebase, so testcloud is the only valid gateway for local testing.Merged as-is this ships dormant: production non-cloud builds cannot reach the workspace routes (gateway-only + CORS), so init fails silently and the switcher stays hidden. Remaining to activate and finish:
/workspaces,/auth/token,/auth/session, workspace/billing/*to non-cloud clients — onapi.comfy.org(pattern local billing already uses) or via gateway CORS for desktop origins; prod-Firebase tokens must validate. Then swap these clients to a distribution-aware absolute base instead of relativeapi.apiURL()paths.getBillingEvents; needs a network capture).customerEventsServiceusesgetAuthHeader()(now workspace-preferring) for a user-scoped/customers/*endpoint — should usegetFirebaseAuthHeader()per authStore's contract.Details and acceptance criteria on FE-1584.