fix: keep tray panel height stable when provider content changes#342
fix: keep tray panel height stable when provider content changes#342prayzey wants to merge 8 commits intorobinebers:mainfrom
Conversation
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/hooks/app/use-panel.ts">
<violation number="1" location="src/hooks/app/use-panel.ts:115">
P2: Panel height clamp is not recalculated on monitor/scale context changes, so fixed-height tray can remain too tall after moving to a smaller display.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
validatedev
left a comment
There was a problem hiding this comment.
One issue I have seen behaviorally that users expect a scrollbar to appear while scrolling, similar to how browsers handle it, and I don't have such thing while testing your build. Not sure whether it should stay visible at all times or only show during active scrolling. Nice thinking though, thanks!
There was a problem hiding this comment.
Pull request overview
This PR stabilizes the Tauri tray panel height across provider/view changes to prevent vertical “jitter,” using a preferred fixed panel height and relying on inner scrolling when content overflows.
Changes:
- Introduces a preferred panel height (560px) with monitor-based clamping in
usePanel, instead of sizing the window to content height. - Updates
AppShellto use a fixedheight(andmaxHeight) for the card container derived from the computed panel height. - Adds regression tests for preferred-height sizing and small-monitor clamping.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/hooks/app/use-panel.ts | Computes a stable panelHeightPx (preferred + clamped) and resizes the Tauri window accordingly. |
| src/components/app/app-shell.tsx | Applies a fixed card height based on panelHeightPx so overflow scrolls inside the panel. |
| src/App.test.tsx | Adds assertions for preferred height (560px) and clamping behavior on smaller monitors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/hooks/app/use-panel.ts
Outdated
| // Keep the tray panel visually stable; scrolling should happen inside the shell. | ||
| const nextPanelHeightLogical = Math.max(1, Math.min(PANEL_PREFERRED_HEIGHT, maxHeightLogical)) | ||
|
|
||
| if (panelHeightPxRef.current !== nextPanelHeightLogical) { | ||
| panelHeightPxRef.current = nextPanelHeightLogical | ||
| setPanelHeightPx(nextPanelHeightLogical) | ||
| } | ||
|
|
||
| const desiredHeightPhysical = Math.ceil(desiredHeightLogical * factor) | ||
| const height = Math.ceil(Math.min(desiredHeightPhysical, maxHeightPhysical!)) | ||
| const height = Math.ceil(Math.min(nextPanelHeightLogical * factor, maxHeightPhysical!)) |
There was a problem hiding this comment.
resizeWindow now sets a fixed preferred height, but the effect still reruns on activeView/displayPlugins changes. Since displayPlugins updates frequently, this can cause repeated currentMonitor() calls and setSize() calls even when the target size hasn't changed. Consider removing displayPlugins from the dependency list (or running this effect only on mount) and/or caching the last {width,height} in a ref to skip no-op setSize calls.
| // Keep the tray panel visually stable; scrolling should happen inside the shell. | ||
| const nextPanelHeightLogical = Math.max(1, Math.min(PANEL_PREFERRED_HEIGHT, maxHeightLogical)) | ||
|
|
There was a problem hiding this comment.
nextPanelHeightLogical can be clamped below the shell's non-content overhead (padding + arrow), especially on very small monitors or high devicePixelRatio. In that case the UI will be clipped because AppShell enforces a minimum card height of 1px. Consider enforcing a minimum panel height that accounts for the shell overhead (or moving the overhead constant into the sizing logic) so the total window height can always fit the chrome.
src/components/app/app-shell.tsx
Outdated
| const cardHeightPx = panelHeightPx ? Math.max(1, panelHeightPx - ARROW_OVERHEAD_PX) : null | ||
|
|
||
| return ( | ||
| <div ref={containerRef} className="flex flex-col items-center p-6 pt-1.5 bg-transparent"> | ||
| <div className="tray-arrow" /> | ||
| <div | ||
| className="relative bg-card rounded-xl overflow-hidden select-none w-full border shadow-lg flex flex-col" | ||
| style={maxPanelHeightPx ? { maxHeight: `${maxPanelHeightPx - ARROW_OVERHEAD_PX}px` } : undefined} | ||
| style={cardHeightPx ? { height: `${cardHeightPx}px`, maxHeight: `${cardHeightPx}px` } : undefined} | ||
| > |
There was a problem hiding this comment.
cardHeightPx is derived from panelHeightPx using a fixed ARROW_OVERHEAD_PX, but cardHeightPx is clamped to at least 1px. If panelHeightPx is ever < ARROW_OVERHEAD_PX (possible with small monitors/high DPI), the card + overhead will no longer fit in the window and the top/bottom chrome may be clipped. Consider clamping panelHeightPx to at least ARROW_OVERHEAD_PX + 1 (or computing these values in one place) to keep the layout internally consistent.
src/App.test.tsx
Outdated
| it("keeps a stable preferred panel height when content is shorter", async () => { | ||
| state.isTauriMock.mockReturnValue(true) | ||
| state.currentMonitorMock.mockResolvedValueOnce({ size: { height: 1000 } }) | ||
| render(<App />) | ||
|
|
||
| await waitFor(() => | ||
| expect(state.setSizeMock).toHaveBeenCalledWith( | ||
| expect.objectContaining({ width: 400, height: 560 }) | ||
| ) | ||
| ) | ||
| }) | ||
|
|
||
| it("clamps the stable panel height to small monitors", async () => { | ||
| state.isTauriMock.mockReturnValue(true) | ||
| state.currentMonitorMock.mockResolvedValueOnce({ size: { height: 500 } }) | ||
| render(<App />) | ||
|
|
||
| await waitFor(() => | ||
| expect(state.setSizeMock).toHaveBeenCalledWith( | ||
| expect.objectContaining({ width: 400, height: 400 }) | ||
| ) | ||
| ) | ||
| }) |
There was a problem hiding this comment.
These tests use currentMonitorMock.mockResolvedValueOnce(...), but usePanel can call currentMonitor() multiple times (initial call + subsequent ResizeObserver callbacks). Using mockResolvedValueOnce makes the tests fragile and can mask regressions in later resize calls. Prefer mockResolvedValue(...) (or assert on the last setSize call / no subsequent call exceeding the clamp).
src/App.test.tsx
Outdated
| it("keeps a stable preferred panel height when content is shorter", async () => { | ||
| state.isTauriMock.mockReturnValue(true) | ||
| state.currentMonitorMock.mockResolvedValueOnce({ size: { height: 1000 } }) | ||
| render(<App />) | ||
|
|
||
| await waitFor(() => | ||
| expect(state.setSizeMock).toHaveBeenCalledWith( | ||
| expect.objectContaining({ width: 400, height: 560 }) | ||
| ) | ||
| ) | ||
| }) |
There was a problem hiding this comment.
Test name/intent mismatch: "keeps a stable preferred panel height when content is shorter" doesn't currently set up a short-content scenario or verify stability across a provider/view change; it only asserts the initial resize target. Either adjust the name to match what’s asserted or extend the test to simulate a content/view change and confirm the height remains unchanged.
@validatedev How is this ? moved it further away from the content. Unless you want it in a different area. Thanks |
|
Hmm, now we have a gap on the right even without any scrollbar, which doesn’t feel ideal. But thanks, I appreciate your work. Let me work on it as well. |
|
@prayzey @validatedev guys make sure to support different macos scrollbar modes. maybe that's a root of your issues?
However not sure if it works the same way in tauri app as it does in browser. So basically in first two options scrollbar is overlayed, in last option a space for scrollbar will be added. CleanShot.2026-04-05.at.23.54.29.mp4 |










Description
Fixes the tray panel resizing vertically when switching between providers or views with different amounts of content.
Before this change, the window height followed the current content height, so short states and longer states made the whole panel jump up and down.
This change keeps a stable preferred panel height and lets the content scroll inside the panel when needed.
Related Issue
N/A
Related to the general sizing/compactness discussion in #106, but this specifically fixes panel height jitter rather than adding a compact mode.
Type of Change
Testing
bun run buildand it succeededbun run testand all tests passScreenshots
Checklist
CONTRIBUTING.mdmainbranchSummary by cubic
Keeps the tray panel height stable across provider/view changes so the window stops jumping. Uses a preferred 560px height with in-panel scrolling, clamps for small monitors, and refines the scrollbar and layout for better spacing.
panelHeightPxwith a fixed card height inAppShell; dedup window resizes; recalc on DPI/scale changes and window moves..panel-scrollon the outer edge; padding moved to an inner content wrapper; footer now haspx-3to match the gutter; updated styles and tests.Written for commit 4410fb4. Summary will update on new commits.