Skip to content

fix(desktop): fix renderer freeze while streaming workspace logs#609

Merged
skevetter merged 7 commits into
mainfrom
ideal-moose
Jul 7, 2026
Merged

fix(desktop): fix renderer freeze while streaming workspace logs#609
skevetter merged 7 commits into
mainfrom
ideal-moose

Conversation

@skevetter

@skevetter skevetter commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a renderer freeze where the desktop window pinned the main thread at 100% CPU and became unresponsive. Reproduced during workspace creation (wizard launch tab) while the container streamed logs: the log view froze even though the underlying CLI completed the launch successfully. A representative launch produced a 3.4 MB / 12,456-line log with individual lines up to ~13 KB.

Diagnosed via sample on a frozen renderer: the main thread was spinning permanently inside v8::MicrotasksScope::PerformCheckpoint (JIT frames, unsymbolized).

Root cause

Three compounding costs on the log-rendering path:

  1. Per-line reactive flush — each CommandProgress event did outputLines = [...outputLines, msg] plus a scroll requestAnimationFrame, so a fast stream produced one full Svelte state flush per line.
  2. Quadratic re-parseLogTable re-derived parsed = lines.map(parseLogLine) over the entire buffer on every append; each line ran several regexes + JSON.parse. Line N re-parsed all N−1 prior lines.
  3. Unbounded DOMLogTable rendered one table row per line, so a large log built tens of thousands of DOM nodes in one synchronous layout.

Changes

  • log-parser.ts — memoize parseLogLine (bounded Map, cap 10k). Each unique line is parsed once.
  • WorkspaceWizard.svelte / WorkspaceDetailPage.svelte — coalesce incoming lines into pendingLines and flush once per animation frame instead of per line. Cleared on reset, new operation start, and onDestroy.
  • LogTable.sveltevirtualized (windowed) rendering. The component owns its scroll viewport and renders only the rows visible in view (plus overscan) as fixed-height absolutely-positioned rows over a full-height spacer. A 12k-line log mounts a handful of DOM nodes; the full log is scrollable with no pagination and no truncation. An opt-in follow mode pins to the tail as lines stream, but only while the user is already at the bottom. Consumers dropped their outer scroll wrappers and manual scrollIntoView hooks.
  • Terminal.svelte ResizeObserver guard was split into fix(desktop): guard terminal ResizeObserver against feedback loop #610 (unrelated defensive hardening).

Verification

  • npx svelte-check on the renderer: 0 errors (1 pre-existing unrelated warning).
  • npx vitest run: 243/243 tests pass. (One main-process suite, cli.test.ts, fails only because deps were installed with --ignore-scripts so Electron's binary wasn't fetched — unrelated to these changes; CI installs normally.)

Notes

A live symbolized profile was not captured; the O(n²) / unbounded-DOM path is strongly supported by the source audit and the exact repro (freeze while streaming a 3.4 MB log, CLI succeeds).

Summary by CodeRabbit

  • New Features
    • Live logs now use a virtualized, scrollable viewer with “follow to latest” behavior.
  • Bug Fixes
    • Live output is now batch-rendered to reduce flicker and prevent missed updates during rapid progress.
    • Log streaming cleanup now cancels any pending UI updates to avoid stale output.
    • Terminal resizing avoids redundant refresh/IPC notifications.
  • Performance Improvements
    • Log parsing is accelerated via cached results with size limits.
  • Tests
    • Test environments now include a safe ResizeObserver fallback when missing.

The ResizeObserver callback unconditionally called fit(), refresh(), and
the terminalResize IPC on every fire. Since fit() mutates the terminal's
dimensions, it could re-fire the observer, sustaining a frame-rate loop
that pins the renderer main thread at 100% CPU and freezes the window.

Track the last propagated cols/rows and early-return when the grid size
is unchanged, so the loop can no longer sustain itself.
@netlify

netlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 77be37e
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a4ce1a17dcc4a0008a8ffad

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19ef8862-4d39-42ac-8a8c-14e9bce0e24c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

LogTable.svelte is rewritten as a virtualized, scroll-tracking log viewer backed by a caching log-parser. WorkspaceWizard and WorkspaceDetailPage adopt buffered, animation-frame-flushed output streaming and render via LogTable in follow mode. Terminal.svelte deduplicates redundant resize handling. A ResizeObserver test polyfill is added.

Changes

Virtualized Log Viewer and Streaming Integration

Layer / File(s) Summary
Log line parsing cache
desktop/src/renderer/src/lib/utils/log-parser.ts
parseLogLine caches results in a module-level Map with size-bound eviction, delegating cache misses to a new internal parseLogLineUncached.
LogTable virtualization core
desktop/src/renderer/src/lib/components/log/LogTable.svelte
New props (maxHeightClass, rowHeight, overscan, follow) drive virtualized rendering: visible rows computed from scroll position, parsed via parseLogLine, positioned absolutely, with a sticky header, bottom-pinning tracking, and ResizeObserver-driven viewport sizing.
WorkspaceWizard buffered output flushing
desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
Launch progress messages are buffered (pendingLines) and flushed on animation frames instead of appended immediately; flushes are cancelled on reset/destroy/relaunch; output now renders via LogTable with follow.
WorkspaceDetailPage buffered live output and log viewer
desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte
Live output streaming buffers messages and flushes via requestAnimationFrame, cancelling on destroy/reset; Live Output and Log Files tabs render LogTable with follow or a loading placeholder.
Terminal resize dedupe and test polyfill
desktop/src/renderer/src/lib/components/terminal/Terminal.svelte, desktop/src/renderer/src/lib/__mocks__/setup.ts
Terminal's ResizeObserver callback skips redundant fit/refresh/IPC calls when cols/rows are unchanged; a no-op ResizeObserver polyfill is added for jsdom test environments.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Backend
  participant WorkspaceDetailPage
  participant pendingLinesBuffer
  participant LogTable

  Backend->>WorkspaceDetailPage: progress.message
  WorkspaceDetailPage->>pendingLinesBuffer: push message
  WorkspaceDetailPage->>WorkspaceDetailPage: schedule requestAnimationFrame(flushLines)
  Backend->>WorkspaceDetailPage: progress.done
  WorkspaceDetailPage->>WorkspaceDetailPage: cancel scheduled frame
  WorkspaceDetailPage->>pendingLinesBuffer: flushLines()
  pendingLinesBuffer-->>WorkspaceDetailPage: appended to outputLines
  WorkspaceDetailPage->>LogTable: render outputLines with follow
Loading
sequenceDiagram
  participant User
  participant LogTable
  participant ResizeObserver
  participant parseLogLine

  User->>LogTable: scroll event
  LogTable->>LogTable: onScroll updates scrollTop, isPinned
  ResizeObserver->>LogTable: viewport size change
  LogTable->>LogTable: recompute totalHeight, visible range
  LogTable->>parseLogLine: parse visible raw lines
  parseLogLine-->>LogTable: ParsedLogLine[]
  LogTable->>LogTable: render visible rows
  alt follow enabled and isPinned
    LogTable->>LogTable: auto-scroll to newest entries
  end
Loading

Possibly related PRs

  • devsy-org/devsy#551: Both PRs modify the renderer test setup file desktop/src/renderer/src/lib/__mocks__/setup.ts, one adding a ResizeObserver polyfill and the other an afterAll teardown delay.

Suggested labels: size/xl

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing a renderer freeze during workspace log streaming in the desktop app.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@netlify

netlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploy Preview for images-devsy-sh canceled.

Name Link
🔨 Latest commit 77be37e
🔍 Latest deploy log https://app.netlify.com/projects/images-devsy-sh/deploys/6a4ce1a1e3430e0008991deb

@github-actions github-actions Bot added the size/s label Jul 6, 2026
The workspace creation wizard (launch tab) and workspace detail page
froze the renderer at 100% CPU while streaming container logs, even
though the underlying CLI completed successfully.

Two compounding causes on the log-streaming path:

- Each CommandProgress event triggered a full state flush
  (outputLines = [...outputLines, msg]) plus a scroll rAF, so a fast
  stream produced one reactive flush per line.
- LogTable re-derived parsed = lines.map(parseLogLine) over the entire
  buffer on every append, making parse work quadratic in line count.

Coalesce incoming lines into a single per-frame flush via
requestAnimationFrame, and memoize parseLogLine so each unique line is
parsed once. Pending state and the frame handle are cleared on reset,
new operation start, and component destroy.
@github-actions github-actions Bot added size/m and removed size/s labels Jul 6, 2026
@skevetter skevetter changed the title fix(desktop): guard terminal ResizeObserver against feedback loop fix(desktop): fix renderer freeze while streaming workspace logs Jul 6, 2026
Opening a large persisted log (observed: 3.4 MB / 12,456 lines for a
single workspace launch) rendered every line as a table row at once,
producing tens of thousands of DOM nodes in one synchronous pass and
freezing the renderer regardless of parse cost.

Cap LogTable at the most recent maxLines rows (default 5000) and show a
notice for the hidden count. Covers both the live-stream and the
persisted-log-file render sites. The Copy actions still operate on the
full buffer, so no data is lost.
Replace the fixed 5000-line render cap with a windowed virtual list:
LogTable now renders only the rows visible in its own scroll viewport
(plus overscan), so a 12k-line log mounts a handful of DOM nodes instead
of thousands. Rows are fixed-height and absolutely positioned within a
spacer sized to the full line count, giving a native scrollbar over the
entire log with no pagination controls and no truncation.

The component owns its viewport and an optional follow mode that pins to
the tail as new lines stream in, but only while the user is already at
the bottom. Consumers drop their outer scroll wrappers and manual
scrollIntoView hooks (outputEl / tableEndEl / scrollToBottom).
@github-actions github-actions Bot added size/l and removed size/m labels Jul 6, 2026
The virtualized LogTable uses a ResizeObserver, which jsdom does not
implement. Rendering LogTable consumers in tests threw an uncaught
ReferenceError, failing the suite even though all assertions passed.
Add a no-op ResizeObserver polyfill alongside the existing matchMedia one.
@skevetter
skevetter marked this pull request as ready for review July 7, 2026 06:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte (1)

153-158: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Same per-frame full-array copy as the Wizard.

flushLines reallocates all of outputLines on every flush (~O(n²) over a long stream). outputLines is a $state array, so an in-place push avoids the copy while still updating LogTable.

♻️ Proposed change
 function flushLines() {
   flushHandle = null
   if (pendingLines.length === 0) return
-  outputLines = [...outputLines, ...pendingLines]
-  pendingLines = []
+  outputLines.push(...pendingLines)
+  pendingLines.length = 0
 }

Same Svelte 5 reactivity confirmation as noted in WorkspaceWizard.svelte applies here.
[source_library_context]

🤖 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 `@desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte` around lines 153 -
158, The flushLines function in WorkspaceDetailPage.svelte is repeatedly
recreating outputLines on every frame, causing unnecessary full-array copies
during long streams. Update flushLines to append pendingLines into the existing
$state array in place instead of reassignment, keeping the reactive LogTable
updates while avoiding the O(n²) behavior; use the existing flushLines,
outputLines, and pendingLines symbols to make the change consistent with the
Wizard fix.
desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte (1)

415-420: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid rebuilding outputLines on each flush. outputLines is a $state array, so push(...pendingLines) keeps LogTable reactive through lines.length and avoids copying the full buffer every frame.

♻️ Proposed change
 function flushLines() {
   flushHandle = null
   if (pendingLines.length === 0) return
-  outputLines = [...outputLines, ...pendingLines]
-  pendingLines = []
+  outputLines.push(...pendingLines)
+  pendingLines.length = 0
 }
🤖 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 `@desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte`
around lines 415 - 420, In flushLines in WorkspaceWizard.svelte, avoid replacing
the entire outputLines $state array on every flush, since that rebuilds the full
buffer and is unnecessary. Update the existing array in place by appending
pendingLines to outputLines, then clear pendingLines and keep the flushHandle
reset logic unchanged. This preserves reactivity for LogTable via
outputLines.length and reduces repeated copying during frequent flushes.
desktop/src/renderer/src/lib/components/log/LogTable.svelte (1)

87-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider preserving table semantics for assistive tech.

Replacing the <table> with absolutely-positioned grid <div>s drops the row/column semantics that screen readers relied on, so the Time/Level/Message relationship is no longer announced. Adding ARIA roles keeps the semantics without affecting the virtualization/layout.

♻️ Suggested ARIA roles
 <div
   bind:this={viewport}
   onscroll={onScroll}
+  role="table"
   class="relative overflow-auto rounded-md border {maxHeightClass} {className}"
 >
   <div
+    role="row"
     class="sticky top-0 z-10 grid grid-cols-[5rem_6rem_1fr] border-b bg-background text-start font-medium"
     style="height: {rowHeight}px"
   >
-    <div class="flex items-center px-2">Time</div>
-    <div class="flex items-center px-2">Level</div>
-    <div class="flex items-center px-2">Message</div>
+    <div role="columnheader" class="flex items-center px-2">Time</div>
+    <div role="columnheader" class="flex items-center px-2">Level</div>
+    <div role="columnheader" class="flex items-center px-2">Message</div>
   </div>
🤖 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 `@desktop/src/renderer/src/lib/components/log/LogTable.svelte` around lines 87
- 118, The virtualized log rendering in LogTable currently replaces the table
structure with positioned divs, which removes the row/column semantics used by
assistive tech. Update the LogTable markup around the visible row loop to
preserve table semantics by adding appropriate ARIA roles to the container, row
wrapper, and each Time/Level/Message cell so screen readers still announce the
relationship correctly. Keep the existing virtualization and styling logic
intact, and ensure the semantics map cleanly to the row.index and row.line
fields without changing the display behavior.
🤖 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 `@desktop/src/renderer/src/lib/components/log/LogTable.svelte`:
- Around line 73-86: Remove the extra scroll wrapper around LogTable in
ProviderWizard.svelte so the table’s own viewport remains the only scrolling
container. LogTable already handles scrolling and virtualization through its
bind:this={viewport} element with overflow-auto, so the outer max-h-48
overflow-y-auto wrapper is causing a duplicate scrollbar and incorrect height.
Update ProviderWizard to either pass the appropriate maxHeightClass into
LogTable or render LogTable directly without the additional wrapper.

---

Nitpick comments:
In `@desktop/src/renderer/src/lib/components/log/LogTable.svelte`:
- Around line 87-118: The virtualized log rendering in LogTable currently
replaces the table structure with positioned divs, which removes the row/column
semantics used by assistive tech. Update the LogTable markup around the visible
row loop to preserve table semantics by adding appropriate ARIA roles to the
container, row wrapper, and each Time/Level/Message cell so screen readers still
announce the relationship correctly. Keep the existing virtualization and
styling logic intact, and ensure the semantics map cleanly to the row.index and
row.line fields without changing the display behavior.

In `@desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte`:
- Around line 415-420: In flushLines in WorkspaceWizard.svelte, avoid replacing
the entire outputLines $state array on every flush, since that rebuilds the full
buffer and is unnecessary. Update the existing array in place by appending
pendingLines to outputLines, then clear pendingLines and keep the flushHandle
reset logic unchanged. This preserves reactivity for LogTable via
outputLines.length and reduces repeated copying during frequent flushes.

In `@desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte`:
- Around line 153-158: The flushLines function in WorkspaceDetailPage.svelte is
repeatedly recreating outputLines on every frame, causing unnecessary full-array
copies during long streams. Update flushLines to append pendingLines into the
existing $state array in place instead of reassignment, keeping the reactive
LogTable updates while avoiding the O(n²) behavior; use the existing flushLines,
outputLines, and pendingLines symbols to make the change consistent with the
Wizard fix.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2aaf326-34ff-436f-8d29-bd8fda461248

📥 Commits

Reviewing files that changed from the base of the PR and between 04d1ce7 and ad8bddf.

📒 Files selected for processing (6)
  • desktop/src/renderer/src/lib/__mocks__/setup.ts
  • desktop/src/renderer/src/lib/components/log/LogTable.svelte
  • desktop/src/renderer/src/lib/components/terminal/Terminal.svelte
  • desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
  • desktop/src/renderer/src/lib/utils/log-parser.ts
  • desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte

Comment thread desktop/src/renderer/src/lib/components/log/LogTable.svelte
- ProviderWizard: drop the outer scroll wrappers around LogTable so the
  table's own viewport is the only scroll container (was a double
  scrollbar with wrong height after LogTable started owning its
  viewport); pass maxHeightClass instead.
- WorkspaceWizard / WorkspaceDetailPage: push into the outputLines
  $state array in place instead of reallocating the full buffer each
  flush; length reset stays reactive for LogTable via lines.length.
- LogTable: restore table semantics for assistive tech with
  role=table/row/rowgroup/columnheader/cell and aria-rowcount/rowindex,
  which the virtualized div layout had dropped.
@skevetter
skevetter marked this pull request as draft July 7, 2026 07:03
@skevetter
skevetter marked this pull request as ready for review July 7, 2026 10:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte (1)

532-532: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

rounded-none likely won't override LogTable's built-in rounded-md.

LogTable's root renders class="... rounded-md border {maxHeightClass} {className}", so the passed className="border-x-0 border-b-0 rounded-none" is appended after rounded-md, not before. Since Tailwind resolves same-property conflicts by stylesheet generation order (not class-attribute order), and rounded-md is emitted after rounded-none in the generated CSS, rounded-md wins — the corners will likely stay rounded despite the intended override. The border-x-0/border-b-0 overrides target different longhand properties than the base border shorthand and should apply fine.

💅 Suggested fix using a class-merge utility
-<LogTable lines={initLines} maxHeightClass="max-h-48" class="border-x-0 border-b-0 rounded-none" />
+<LogTable lines={initLines} maxHeightClass="max-h-48" class={cn("border-x-0 border-b-0 rounded-none")} />

(where cn uses tailwind-merge/twMerge to resolve conflicting utilities deterministically, if such a helper already exists in this codebase)

Per Tailwind's own docs, "When you add two classes that target the same CSS property, the class that appears later in the stylesheet wins." and generated utility order places rounded-md after rounded-none.

🤖 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 `@desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte` at
line 532, The `LogTable` usage in `ProviderWizard.svelte` is trying to remove
corner rounding with `rounded-none`, but `LogTable` already applies `rounded-md`
in its root class list, so the override likely won’t win. Update the `LogTable`
class handling (or the passed class composition) so conflicting Tailwind
utilities are merged deterministically, using the existing `cn`/`twMerge` helper
if available, and ensure the `rounded-none` intent is applied after `rounded-md`
in the merged result.
🤖 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.

Nitpick comments:
In `@desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte`:
- Line 532: The `LogTable` usage in `ProviderWizard.svelte` is trying to remove
corner rounding with `rounded-none`, but `LogTable` already applies `rounded-md`
in its root class list, so the override likely won’t win. Update the `LogTable`
class handling (or the passed class composition) so conflicting Tailwind
utilities are merged deterministically, using the existing `cn`/`twMerge` helper
if available, and ensure the `rounded-none` intent is applied after `rounded-md`
in the merged result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d76210c8-613a-49b2-ad9d-094b176a7b8c

📥 Commits

Reviewing files that changed from the base of the PR and between ad8bddf and b81bae1.

📒 Files selected for processing (4)
  • desktop/src/renderer/src/lib/components/log/LogTable.svelte
  • desktop/src/renderer/src/lib/components/provider/ProviderWizard.svelte
  • desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
  • desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte
🚧 Files skipped from review as they are similar to previous changes (3)
  • desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
  • desktop/src/renderer/src/lib/components/log/LogTable.svelte
  • desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte

LogTable built its root class by string interpolation, so a consumer's
className was appended after the base rounded-md/max-h-96 rather than
overriding them — Tailwind resolves same-property conflicts by generated
stylesheet order, letting the base classes win. Route the root class
through cn() (clsx + tailwind-merge) so passed classes like rounded-none
and max-h-48 deterministically override the defaults.
@skevetter
skevetter merged commit 01fbe44 into main Jul 7, 2026
27 checks passed
@skevetter
skevetter deleted the ideal-moose branch July 7, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant