Skip to content

feat(app): keep narrow desktop windows in the desktop layout - #58894

Open
kitlangton wants to merge 5 commits into
devfrom
narrow-window
Open

kitlangton wants to merge 5 commits into
devfrom
narrow-window

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

A desktop window narrower than 768px keeps the desktop layout instead of switching to the phone layout.
Before, narrowing the window swapped in bottom navigation, no titlebar, no hover and phone density. Now panes make room the way native apps do, and the phone layout is only for touch screens under 768px.

❌ Before

  1. Narrow the window below 768px.
  2. ❌ The whole app switches to the phone layout, with a mouse in hand.

✅ After

  1. At 1064px and wider everything docks. The side panel shrinks from 480px toward 320px before anything hides.
  2. Below 1064px with the side panel open (734px with it closed), the left sidebar hides on its own. ⌘B or hovering the leading edge floats it over the chat; Escape or a click outside closes it.
  3. Below 794px the side panel stops docking and slides over the chat from the trailing edge as a sheet. Escape or its toggle closes it.
  4. Hiding for room never changes the saved sidebar or side-panel setting, and has 16px of hysteresis so a window at an edge doesn't flap. Panes snap during a live resize and animate only on a toggle.
  5. Electron windows get a 480×480 minimum, and a saved smaller size is clamped on restore.
- narrow = window width < 768px  -> phone layout
+ narrow = touch screen under 768px (isMobileLayout) -> phone layout
+ otherwise the shell shares width: side panel shrinks -> sidebar floats -> side panel becomes a sheet

📊 Proof: a 700px window with a mouse, headless Chromium, mocked server.

Before (phone layout) After (desktop layout, side panel open as a sheet)

Unit tests pass (session-panel-width: 10, window-state: 6). The new end-to-end case in tabs.spec.ts (700px mouse window: sidebar floats on demand, side panel becomes a sheet) and the specs that now emulate touch for phone-layout checks were run before the latest rebase, not after.

⚠️ Behavior change: a narrow browser tab with a mouse now keeps the desktop layout too, so phone-only affordances no longer appear there. Layout.narrow() in the GUI extension SDK now means "touch screen under 768px" (same type).

👀 Read: shell/room.ts (the width rules), session/screen.tsx (the side panel sheet, which stays a sheet through its own close animation) and shell/shell.tsx (when the sidebar hides). isMobileLayout in @opencode/ui/mobile-layout is now the one place that decides the phone layout. Right-to-left layouts use logical properties and a mirrored sheet transform; checked in code, not on screen. Real Electron window resizing and full screen weren't exercised.

Rebased onto dev, resolving conflicts in the shell, session screen, sidebar toggle and Home route · no user reports

Checks run
  • bun typecheck (packages/app, packages/desktop, after the rebase): clean
  • bun test … src/session/session-panel-width.test.ts (packages/app): 10 pass
  • bun test src/main/windows/window-state.test.ts (packages/desktop): 6 pass
  • pre-push bun run check (lint and typecheck): pass

Added by the opencode bot (AI-generated).

Related: #50459

Public API: changes the meaning of the GUI extension SDK's Layout.narrow() from "viewport under 768px" to "touch screen under 768px" (type-compatible)

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code looks sound. I couldn't see it working here: Chromium won't launch in this environment (missing system libraries), so the e2e and component specs didn't run, and the Electron minimum-size change needs the real desktop app. The window-state and session-panel-width unit tests pass, and typecheck:tsc is clean in app and desktop. I read through the room and hysteresis logic, how the sidebar auto-hide drives the peek and the toggle command, the sheet's Escape handling next to the peek's, and the move from width-only media queries to isMobileLayout. I found nothing blocking.

Public API: one behaviour change for GUI extensions. In @opencode/gui-extensions/sdk, Layout.narrow() changes from "viewport under 768px" to "touch screen without hover, under 768px". The type is unchanged, but an extension that used narrow() to adapt to a small desktop window will now get false there. @opencode/ui also gains a ./mobile-layout export, which is additive and internal to the workspace. Config, the server API, the CLI and the client SDK are unchanged.

A few width-only styles are left, so narrow mouse windows still get them: the md: padding in the timeline and session route, and @media (max-width: 767px) in session-ui/src/pierre/index.ts. The pierre rule looks like a deliberate width rule. Please check the padding ones are meant to stay.

The stills and clips you mentioned will help here, since none of this could be checked on screen.

Related: #50459

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit only touches patch-sticky-header.spec.ts. Under 768px it now emulates touch, like the other phone-layout specs, so the 390px case still exercises the phone layout. The wrapper is small and easy to follow, and I have nothing new to raise. I couldn't run the spec here because headless Chromium won't start in this environment. The public API impact is unchanged from my previous review.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit makes sense. The sheet decision now uses the shell's full row width, is made while the panel is open and is kept while it closes. That matches how the sidebar gives way: with the side panel open, the sidebar only stays docked if the content still has the docking width, so re-docking on close shouldn't turn a docked panel into a sheet. The peek's focus return and toggle-hover fix look right. I checked this by reading the code: typecheck:tsc in app is clean and the session-panel-width unit tests pass. I couldn't run the e2e specs here because headless Chromium won't launch. The public API impact is unchanged from earlier.

One small thing in the new test, inline.

// Records whether the side panel ever floats as a sheet, on any frame, through an open and a close.
await page.evaluate(() => {
const sample = () => {
if (document.querySelector('[data-slot="session-side-panel-presence"].z-20'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This sampler finds the sheet by its Tailwind class (.z-20). If that styling changes, the selector never matches and the "never became a sheet" check passes even when the bug is back. A dedicated attribute on the presence element (e.g. data-sheet set from sideSheet()) would keep this test checking the right thing.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit fixes the test selector point: the sampler now uses a data-sheet attribute, set under the same condition as the sheet styling, so the "never became a sheet" check can't pass by accident. typecheck:tsc in app is clean. I couldn't run the e2e spec here because headless Chromium won't launch. The public API impact is unchanged.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This push rebases the change onto dev. I compared the change's own diff before and after the rebase: the differences only adapt to dev, where the panel-pinning code (sideMotion.pinned()), the side-panel toggle replay and the peek-enabled setting don't exist, and the sidebar is wider. The sheet logic and data-sheet are unchanged. typecheck:tsc is clean in app and desktop, and the window-state and session-panel-width unit tests pass. I couldn't run the e2e specs here because headless Chromium won't launch. Nothing new to raise. The public API impact is unchanged.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commits only touch tests. The narrow-window test now expects the 276px sidebar width that is the default on dev, and settings.spec.ts gets a blank line for lint. Both are correct and I have nothing new to raise. The earlier test-selector thread was addressed by the data-sheet attribute. The public API impact is unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant