feat(floating-pane): resize + maximize support#1132
Conversation
Hand-rolled resize and maximize for the WS_POPUP + WS_EX_TOOLWINDOW floating pane — CEF Views is unavailable for floaters (no taskbar / owned-cascade requirement), so the native wndproc carries the load. Backend (Windows-only paths): - floating_pane_wndproc: WM_GETMINMAXINFO floor (320x180 CSS px) + work-area clamp for maximize; WM_SIZE drives the embedded CEF WS_CHILD to track the outer (children of WS_POPUP do NOT auto-track parent size); WM_NCHITTEST band 6 -> 8 CSS px for discoverability. - maximize_window: route via resolve_window_hwnd (label-aware, PR #1094 pattern) instead of find_own_top_level_window; stash pre-maximize rect for floaters into state.floating_restored_rects; return {state} so the FE can update its glyph without a follow-up query. - New IPCs: set_window_size, get_window_state, consume_restored_rect, restore_window_and_move. All label-routed. Frontend: - New shared signal in store/floating-pane-context.ts holds {windowLabel, state} when the renderer is the floating shell. Docked panes see null and skip all of the below. - BlockFrame_Header EndIcons: maximize/restore button gated to floating context, immediately left of close-X. - FloatingPaneWorkspace.onMount: seed context from get_window_state; install dblclick listener (toggles maximize, suppresses the bubble- phase magnify-toggle on the header); install drag-from-maximized branch in mousedown (consume_restored_rect + restore_window_and_move before arming the drag loop, anchored under the cursor). Spec: docs/specs/SPEC_FLOATING_PANE_RESIZE_AND_MAXIMIZE_2026-05-28.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | PR opened |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | Clean |
| Review Time | 119.1s |
| Timestamp | 2026-05-28T17:24:18Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
Issues:
- [P2] frontend/app/workspace/floating-pane-workspace.tsx:228 -
restoredY = cursorPhysY - 16uses a hardcoded 16-physical-px offset; at 200% DPI that's 8 CSS px, which lands the cursor right at the 8-CSS-px top resize band boundary. Scale bydpr(e.g.Math.round(16 * dpr)) so the cursor consistently lands ~16 CSS px below the top edge across DPIs. - [P2] agentmux-cef/src/commands/window.rs -
set_window_sizeis defined and wired into the IPC router but has no frontend caller in this PR; speculative addition per spec §5.2 (deferred uses listed but not implemented here). - [P2] agentmux-cef/src/commands/window.rs - On Windows when
resolve_window_hwndreturns null (label not found),maximize_windowsilently returns{state:"normal"}with no error; the FE will then show a stale "restored" glyph for a window it never actually toggled. Consider returning an Err on unresolved label.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a09a4dc0e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
| const currentState = useFloatingPaneInfo()?.state; | ||
| if (currentState === "maximized") { |
There was a problem hiding this comment.
Restore maximized panes only after drag begins
When a floating pane is maximized, this branch runs on every non-interactive header mousedown before the code knows the user is actually dragging. That means a plain click on the maximized header restores and moves the window, and a double-click intended to restore can pre-restore on mousedown before the dblclick handler toggles it back to maximized. Gate this restore path behind a movement threshold (or otherwise suppress it for click/dblclick gestures) so maximize/restore clicks do not change state prematurely.
Useful? React with 👍 / 👎.
| (*mmi).ptMaxPosition = POINT { | ||
| x: work.left - wr.left, | ||
| y: work.top - wr.top, |
There was a problem hiding this comment.
Compute maximized position from monitor offsets
For floaters that are not already located at the monitor work-area origin, this makes the maximized origin depend on the window's previous position: e.g. on a primary monitor with work.left == 0, a pane at wr.left == 500 reports ptMaxPosition.x == -500, so maximizing shifts the window off-screen instead of clamping to the work area. MINMAXINFO.ptMaxPosition is the maximized window position, not a delta from the current window; compute it from the monitor/work-area offset (with the usual primary/target monitor adjustment) rather than subtracting wr.left/top.
Useful? React with 👍 / 👎.
Three concrete fixes for the resize+maximize PR:
1. WM_GETMINMAXINFO ptMaxPosition (Codex P2, floating_pane.rs:398)
Was `work.left - wr.left` / `work.top - wr.top` — a delta from the
current window rect. ptMaxPosition is the absolute screen-space
position of the maximized window, not a delta; for a floater at
wr.left=500 on a primary monitor (work.left=0), the old code
reported ptMaxPosition.x=-500 and shifted the window off-screen
on maximize. Use (work.left, work.top) directly.
2. maximize_window null-label error (ReAgent P2, commands/window.rs)
When resolve_window_hwnd returned null on the Windows path, the
function silently fell through to the cross-platform default
`Ok({state:"normal"})`. The frontend then updated its glyph for
a window it never actually toggled. Return Err on unresolved
label so the FE keeps the old glyph and surfaces the failure.
3. Drag-from-maximized gating + DPR-scaled restore offset
(Codex P2 floating-pane-workspace.tsx:205 + ReAgent P2 :228)
The maximize→restore-under-cursor branch fired on every header
mousedown — a plain click on a maximized header restored and
moved the window, and a dblclick's mousedown half pre-restored
before the dblclick handler could toggle back. Gate behind a
4-CSS-px drag threshold (matching Windows SM_CXDRAG default):
mousedown only arms the restore; mousemove past threshold fires
it; mouseup clears the arm (= it was a click).
Also scaled the 16-physical-px cursor-below-top-edge offset by
devicePixelRatio. At 200% DPI the hardcoded 16 collapsed to 8
CSS px, landing the cursor right at the top resize-band boundary.
Set_window_size speculative-API finding (ReAgent P2) intentionally
left in-place per the PR body's spec §5.2 deferred-use rationale;
will reply to ReAgent in the re-review comment.
|
@codex review Addressed:
|
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | Clean |
| Review Time | 89.7s |
| Timestamp | 2026-05-28T17:49:31Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
All four prior review issues (Codex P2 on ptMaxPosition, ReAgent P2 on null HWND, Codex P2 on premature restore, ReAgent P2 on DPR-scaled offset) are addressed in commit 487b493. Verified: (1) floating_pane.rs:399 now uses work.left/top directly; (2) window.rs returns Err on unresolved label on Windows; (3) floating-pane-workspace.tsx gates restore behind a 4-CSS-px movement threshold via pendingMaximizedRestore; (4) restoredY offset is now Math.round(16 * dpr). The mousedown→mouseup→mousedown→mouseup→dblclick sequence correctly clears pendingMaximizedRestore on each mouseup. Race guards via currentMouseDownId are in place around async IPCs.
The "version validation failed" warning is a false positive — per CLAUDE.md RFC #857 Phase 2, feature PRs use changesets, not package.json bumps, and this PR correctly adds .changesets/1779988830-feat-floating-pane-resize-maximize-tlm6.md.
LGTM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 487b493464
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
| (*mmi).ptMaxPosition = POINT { | ||
| x: work.left, | ||
| y: work.top, | ||
| }; |
There was a problem hiding this comment.
Keep maximized geometry monitor-relative
When the floater is on any non-primary monitor, Windows treats ptMaxPosition as the position it would use on the primary monitor and then adjusts it to the actual monitor (Microsoft), so storing absolute virtual-screen coordinates here gets offset a second time. For example, on a monitor at left = 1920, setting x = work.left can maximize the pane around 3840 instead of filling that monitor; use the work-area offset relative to the monitor (work.left - mi.rcMonitor.left, similarly for top) while keeping ptMaxSize as the work-area size.
Useful? React with 👍 / 👎.
| let hwnd = find_own_top_level_window(); | ||
| use windows_sys::Win32::Foundation::RECT; | ||
|
|
||
| let hwnd = resolve_window_hwnd(state, label); |
There was a problem hiding this comment.
Reject unresolved maximize labels before fallback
When a floating-pane label is stale or missing from window_hwnds, resolve_window_hwnd falls through to find_own_top_level_window() for non-main labels, so this call can still return some other top-level HWND and maximize_window will toggle it and report success instead of hitting the null-HWND error path below. This is the stale-label case the frontend relies on to keep its glyph unchanged; check the label-specific cache/registry resolution strictly here, or make the resolver skip the process-wide fallback for labeled maximize requests.
Useful? React with 👍 / 👎.
User reported during smoke test of dev build:
- Floating-pane resize does not engage on edge bands. The CEF
child WS_CHILD covered the entire outer client area, so the
OS dispatched WM_NCHITTEST to the topmost window under the
cursor (the CEF child) — which returned HTCLIENT and never
bubbled to the outer wndproc's HT{LEFT,RIGHT,...} branch.
Inset the CEF child by `resize_border_px` on each edge in
WM_SIZE so the outer's edge bands stay exposed and the resize
hit-test fires. The 8-CSS-px inset matches the band size and
is visually indistinguishable from a system resize border.
Codex P2 round 2 (commit 487b493 review):
- floating_pane.rs:407 — `ptMaxPosition` written as absolute
virtual-screen coords (`work.left/top` directly). Per
Raymond Chen's devblog (2015-05-01), Windows treats
`ptMaxPosition` as primary-monitor-relative and re-adjusts
by the target monitor's offset, so absolute coords get
offset twice on non-primary monitors. Switched to the work-
area offset WITHIN its monitor (`work - mon`) so the OS
adjustment lands correctly across monitor configurations.
- commands/window.rs `maximize_window` — null-HWND error path
was reachable for `main` but not for stale `floating-…`
labels, because `resolve_window_hwnd` falls back to
`find_own_top_level_window()` for non-main labels and that
returns *some* top-level HWND in Z-order (often the wrong
one). Replaced the `resolve_window_hwnd` call with a direct
per-label cache check for `floating-…` so stale labels
surface as the null-HWND Err path. Non-floater labels keep
the old resolution path.
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | Clean |
| Review Time | 94.1s |
| Timestamp | 2026-05-28T18:07:31Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
LGTM
|
@codex review Round 2 of fixes pushed in c7bd575:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c7bd575947
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
| WM_SIZE => { | ||
| if wparam as u32 != SIZE_MINIMIZED { | ||
| let w = (lparam & 0xFFFF) as i16 as i32; | ||
| let h = ((lparam >> 16) & 0xFFFF) as i16 as i32; | ||
| if w > 0 && h > 0 { | ||
| let child = GetWindow(hwnd, GW_CHILD); |
There was a problem hiding this comment.
Inset the child before relying on WM_SIZE
When a floater is first opened, create_owned_popup shows the outer HWND before the CEF child is created, so the initial WM_SIZE has already run with no child to move. The child is then created at (0,0,width,height), covering the resize bands until some later resize/maximize sends another WM_SIZE; in the common freshly-opened case the OS still hit-tests the CEF child as HTCLIENT, so edge resizing remains unavailable. Please also create or immediately move the child with the same inset after browser_host_create_browser succeeds.
Useful? React with 👍 / 👎.
| let hwnd = if label.starts_with("floating-") { | ||
| let cached = state.window_hwnds.lock().get(label).copied(); | ||
| match cached { | ||
| Some(raw_isize) => raw_isize as *mut std::ffi::c_void, | ||
| None => std::ptr::null_mut(), |
There was a problem hiding this comment.
Validate cached floater HWNDs before toggling
This strict floating-label path treats any window_hwnds entry as a live window, but those cache entries are never removed on BrowserUnregistered/floater close. After a pane is redocked or closed, a stale floating-… label can still return a non-null destroyed/reused HWND here, causing GetWindowPlacement/ShowWindow to no-op or hit another window while returning {state:"maximized"} instead of taking the intended error path for stale labels. Please check IsWindow (and remove invalid entries) or clean the cache on unregister before accepting the cached handle.
Useful? React with 👍 / 👎.
- commands/window.rs:173 (Codex P2 round 3): cache entries are not removed on `BrowserUnregistered` or floater close in this PR's baseline, so a stale `floating-…` entry can return a non-null but destroyed HWND. After fetching the cache entry, validate with `IsWindow(hwnd)` and treat as null when the window's gone — so the null-HWND Err path fires and the FE keeps its old glyph instead of toggling a zombie. PR #1133 adds eviction on close on main; this check is defense in depth against any race window before eviction. - floating_pane.rs (Codex P2 round 3 — initial-creation gap): the CEF child was previously created at (0, 0, width, height), and the first `WM_SIZE` on the outer HWND fires from inside `create_owned_popup`'s `ShowWindow(SW_SHOWNOACTIVATE)` BEFORE the child exists — `GetWindow(GW_CHILD)` returns null and the WM_SIZE inset path no-ops. Result: the child covered the resize bands on initial open until the user resized once. Apply the same `resize_border_px` inset at creation time so the outer's edge bands stay exposed from frame zero. Same DPI math as WM_NCHITTEST.
|
@codex review Round 3 pushed in 87e16a6 + merged main (6a454e8):
Plus merged in the 5 commits that landed on main since the branch opened (including PR #1133 which is the complementary cache-eviction-on-close fix). |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | 1 regression(s) detected |
| Review Time | 33.4s |
| Timestamp | 2026-05-28T20:30:02Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
LGTM
The round-3 CEF child inset exposes 8 CSS px of the outer window's client area around the embedded browser, so the wndproc's WM_NCHITTEST gets edge events for resize. With `hbrBackground=NULL` the OS doesn't auto-paint, but DWM still fills the un-painted strip with its default light frame color — producing a thick white border visible around the floating window after the round-3 fix. Handle `WM_ERASEBKGND` to fill the client rect with `RGB(34, 34, 34)` matching `frontend/app/app.scss`'s default `background: rgba(34, 34, 34, …)` — the CEF child paints on top of this, so only the 8 CSS-px ring around the child surfaces the color, and it now reads as a thin dark resize gutter instead of a fat white outline. Light-theme support is deferred; an `set_floater_chrome_color` IPC from FE can pass the current theme bg later. Hard-coded dark for now since AgentMux defaults to dark. User-reported regression on PR #1132 dev smoke test (round 3 fix).
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | 1 regression(s) detected |
| Review Time | 60.3s |
| Timestamp | 2026-05-28T22:00:16Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
LGTM
White-border investigation: my earlier WM_ERASEBKGND fix (paint
strip RGB(34,34,34) dark) produced a thick WHITE border per user
report. Three candidate root causes:
(a) the handler isn't being called at all
(b) the handler IS called but DWM repaints in white
(c) the white is from a different source (CEF child initial paint,
WS_THICKFRAME's resize-border decoration, etc.)
Diagnostics added to discriminate:
1. WM_ERASEBKGND now paints MAGENTA so we can visually verify
whether my handler reaches the pixels. If user sees magenta
strip → my handler works → root cause is the dark color was
wrong / mis-encoded. If user still sees white → handler is
skipped or being overpainted.
2. tracing::info! on WM_ERASEBKGND / WM_PAINT / WM_NCPAINT entry
so the host log records the message sequence. Filter via
`floating-pane-diag` target.
3. WM_NCPAINT now returns 0 to suppress the OS's default non-
client paint. WS_THICKFRAME normally draws a thin frame border
here even with WM_NCCALCSIZE=0; if the visible white was from
this path it should now be gone.
Once we identify which of (a)/(b)/(c) it is, revert to the proper
dark color and remove the diagnostics.
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | 1 regression(s) detected |
| Review Time | 33.0s |
| Timestamp | 2026-05-28T22:20:20Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
Magenta debug paint shipping. That's the major blocker.
Issues:
- [P0] agentmux-cef/src/floating_pane.rs:414 - WM_ERASEBKGND paints the floater background MAGENTA (0x00FF00FF) — diagnostic code from commit c15392d ("diag: trace paint messages + visually distinguish strip"). The comment itself says "Reverts to the dark color once we know which case we're in." This MUST be reverted to the app background color before merge; otherwise every floating pane ships with a bright magenta border / background flash.
- [P1] agentmux-cef/src/floating_pane.rs:419 -
tracing::info!on every WM_ERASEBKGND, WM_PAINT, and WM_NCPAINT (lines 419, 433, 443) spams the host log on every paint cycle. Diagnostic-only; drop the logging (or downgrade totrace!) before shipping — paint messages fire continuously and will swampmuxlog host. - [P1] agentmux-cef/src/floating_pane.rs:442 - WM_NCPAINT unconditionally returns 0 to suppress the OS frame paint. The comment frames this as a diagnostic ("If the white we're seeing is from here, this should kill it"). If kept intentionally, remove the diagnostic framing; if not, restore default handling. The combination of "suppress WM_NCPAINT + paint magenta in WM_ERASEBKGND + log every paint" reads as an unfinished investigation that was force-pushed into the PR.
Diagnostics confirmed: my WM_ERASEBKGND handler IS being called and
DOES paint the inset strip (magenta in the diagnostic build), yet
user still saw a thick WHITE border. DWM is overpainting the strip
with the system default after my handler runs, and even WM_NCPAINT
suppression didn't help. The "paint the strip dark" approach can't
beat DWM.
Switching architectures: instead of insetting the CEF child by
8 CSS px (which exposes the visible strip), let the child fill the
entire client area and subclass its wndproc so its WM_NCHITTEST
returns HTTRANSPARENT for cursor positions within 8 CSS px of any
edge. Per MSDN, returning HTTRANSPARENT from a child's WM_NCHITTEST
tells the OS "this window doesn't process this hit-test; check the
window beneath" — and for a WS_CHILD of a top-level popup that
bubbles to the parent's wndproc, where the existing HT{LEFT,RIGHT,...}
mapping returns the resize code.
Net result:
- Zero visible inset strip — no white border, regardless of DWM.
- Resize works the same as before (the outer's WM_NCHITTEST still
fires on edge bands, just via HTTRANSPARENT bubble from the
child instead of via direct dispatch).
- Non-edge cursor positions fall through to the original CEF
wndproc unchanged — focus, clicks, scroll, every other behavior
is preserved.
Reverts:
- WM_SIZE inset (back to SetWindowPos child at 0,0,w,h)
- Initial-create inset in the Rect passed to set_as_child
- WM_ERASEBKGND magenta paint (no longer needed; nothing to paint)
- WM_PAINT diagnostic trace
- WM_NCPAINT suppression (didn't help and adds risk)
Keeps:
- `install_child_nchittest_forwarder` diagnostic info log so we
can confirm subclass install on every floater open.
The wndproc subclass mirrors `install_frameless_resize_hook` in
`client/wndproc.rs` — same `Mutex<HashMap>` of original wndprocs,
same CallWindowProcW fallback. Idempotent: re-installing on the
same child is a no-op.
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | Clean |
| Review Time | 113.3s |
| Timestamp | 2026-05-28T22:28:46Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
Issues:
- [P2] agentmux-cef/src/floating_pane.rs:351 -
install_child_nchittest_forwarder's process-globalORIGINAL_WNDPROCSmap is never pruned on child destruction. If a destroyed CEF child's HWND value is later reused by Win32 for a new floater's child,contains_keyshort-circuits the installer, the forwarder is never installed on the new HWND, and edge-band resize silently breaks for that floater. Mirror the round-3IsWindowdefense added towindow_hwnds(or interceptWM_NCDESTROYin the forwarder to remove its own entry).
ReAgent P2 on PR #1132: `install_child_nchittest_forwarder`'s process-global `ORIGINAL_WNDPROCS` map had no eviction path. The entry leaked on child destruction, and — worse — if Win32 later recycled a destroyed child's HWND value for a new floater's child, the installer's `contains_key` short-circuit would skip installing the forwarder on the new HWND, silently breaking edge-band resize for that floater. Handle WM_NCDESTROY in the forwarder: forward to the original wndproc so CEF completes teardown, then remove our own map entry. WM_NCDESTROY is the last message a window receives, so the entry is gone before the HWND value can be recycled. Mirrors the round-3 IsWindow defense added to window_hwnds in maximize_window.
|
@codex review ebf015a addresses the one open P2 (ReAgent, on 6d47109): the WM_NCHITTEST forwarder's ORIGINAL_WNDPROCS map is now pruned on the child's WM_NCDESTROY — forward to the original wndproc for CEF teardown, then remove our entry, so a recycled HWND value can't short-circuit the installer and silently break resize on a future floater. Note: the magenta-debug-paint P0 that was flagged earlier was on the superseded diagnostic commit c15392d; the subclass commit 6d47109 already removed it (no WM_ERASEBKGND paint, no per-paint logging, no WM_NCPAINT suppression remain). The only floating-pane-diag line left is a one-time install log per floater. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.54 |
| Trigger | Re-review (new commits) |
| Project Context | CLAUDE.md loaded |
| Model | claude-opus-4-7 |
| Effort | high |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Analysis | Clean |
| Review Time | 43.9s |
| Timestamp | 2026-05-29T05:31:06Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
Issues:
- [P0] package.json:9 - Merge regression: PR downgrades package.json from 0.39.3 (main) to 0.39.2. Feature PRs should not touch version files (RFC #857 Phase 2 — use changesets only); the merge of main into the branch dropped main's 0.39.3 bump.
- [P0] Cargo.toml:9 - Merge regression: workspace version downgraded from 0.39.3 (main) to 0.39.2. Same root cause as package.json — rebase/re-merge to take main's version files verbatim, keep only the
.changesets/entry. - [P0] VERSION_HISTORY.md:3 - Merge regression: removes the
## 0.39.3 — 2026-05-28section that exists on main. Restore by taking main's version of this file.
Handoff notes — state + open investigation (AgentY)Parking this PR open. Summary of where it stands so it can be picked up cleanly. Resolved
Open — two runtime regressions, NOT yet root-caused
Investigation finding — the diff doesn't explain itWalked
So statically, #1132 has no obvious mechanism to break normal docking/ghosts or main-window drag. Prime suspect for pickupRuntime mouse-routing during cross-window drag. Specifically: if Decisive next step (settles the merge question)A/B test: does redock + ghosts work on plain
Also re the parent-drag zero-IPC observation: rule out a dev-mode artifact first — in Branch HEAD: |
… (Phase 1) Phase 1 of the pane-state reducer (SPEC_PANE_STATE_REDUCER_2026-05-28.md). Builds on Phase 0 (#1154). The headline invariant: wherever a pane's lifecycle ends, its window-placement entry (`HostState.pane_window_states`) is evicted in the SAME reducer arm. This is cleanup by construction — the structural replacement for the leak-prone manual cleanup of `AppState.floating_restored_rects` that Codex flagged on PR #1132 round 3. Adds `state.pane_window_states.remove(&block_id)` to all three arms that remove a pane from `browser_panes`: - handle_complete_browser_pane_close - handle_drain_browser_pane_by_label - handle_abort_browser_pane_create All co-evictions are no-ops when absent (docked panes never have a placement entry), so there's no behavior change for existing flows — the placement map is still only populated in tests until the IPC wiring lands in a later phase. 4 new tests (8 total in pane_window): each close/drain/abort co-evicts; plus a docked-pane-has-no-placement no-op case. 48 reducer tests pass.
… (Phase 1) (#1157) Phase 1 of the pane-state reducer (SPEC_PANE_STATE_REDUCER_2026-05-28.md). Builds on Phase 0 (#1154). The headline invariant: wherever a pane's lifecycle ends, its window-placement entry (`HostState.pane_window_states`) is evicted in the SAME reducer arm. This is cleanup by construction — the structural replacement for the leak-prone manual cleanup of `AppState.floating_restored_rects` that Codex flagged on PR #1132 round 3. Adds `state.pane_window_states.remove(&block_id)` to all three arms that remove a pane from `browser_panes`: - handle_complete_browser_pane_close - handle_drain_browser_pane_by_label - handle_abort_browser_pane_create All co-evictions are no-ops when absent (docked panes never have a placement entry), so there's no behavior change for existing flows — the placement map is still only populated in tests until the IPC wiring lands in a later phase. 4 new tests (8 total in pane_window): each close/drain/abort co-evicts; plus a docked-pane-has-no-placement no-op case. 48 reducer tests pass. Co-authored-by: AgentY-asaf <253608533+AgentY-asaf@users.noreply.github.com>
Adds the floating half of the shared maximize button (SPEC_PANE_STATE_REDUCER
§3.3a). Docked panes keep layout magnify; a torn-off floating pane gets an
OS-window maximize/restore routed through the host reducer.
- Host: new `toggle_floating_maximize` IPC handler (commands/window/chrome.rs)
dispatches `HostCommand::ToggleFloatingMaximize { label }`, then applies the
`ShowWindow(SW_MAXIMIZE/SW_RESTORE)` side-effect AFTER dispatch from the
emitted `PaneWindowStateChanged` (snapshot-and-drop; no I/O in the reducer),
resolving the floater's outer HWND via `resolve_window_hwnd(window_hwnds)`.
Returns `{ placement }` so the button can sync its icon (no host->frontend
push channel on main). Registered in ipc.rs route_command.
- Frontend: `FloatingMaximizeButton` in blockframe EndIcons, shown only when
the window carries a `?windowLabel=floating-…` param; reuses MagnifyIcon so
maximize and magnify read as one operation.
Deliberately independent of edge-resize: no HTTRANSPARENT WM_NCHITTEST child
subclass, so it cannot perturb redock hit-testing the way #1132's resize did.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) * fix(host-reducer): re-key pane window-placement by floater label, not block_id Correction to the merged Phases 0-1, found while wiring the real consumer: the floating-pane create path (floating_pane.rs) registers ONLY into `window_hwnds` keyed by the `floating-<uuid>` window label — a floater's block is NEVER in `browser_panes`. So Phase 0/1's choice to key `pane_window_states` by block_id and co-evict via the `browser_panes` arms was wrong for the only thing that ever populates it (floaters): the co-eviction would never fire → latent leak. Re-key to the floating-window label end-to-end: - `pane_window_states` keyed by label (HashMap key meaning; type unchanged). `ToggleFloatingMaximize { label }` and `PaneWindowStateChanged { label, ... }` renamed from block_id. - Removed the (never-firing-for-floaters) block_id co-eviction from handle_complete_browser_pane_close / handle_drain_browser_pane_by_label / handle_abort_browser_pane_create — reverted those arms to original. - Added `HostCommand::EvictFloatingPaneWindowState { label }` + arm, dispatched from `client/mod.rs::on_before_close` right where `window_hwnds[label]` is evicted (gated to `floating-` labels). THAT is the floater's real teardown hook. Idempotent no-op if absent. Still dormant at runtime: nothing populates `pane_window_states` until the maximize IPC lands (next commit). 46 reducer tests pass (6 pane_window: label-keyed toggle + evict-on-close). Label is the right key because floaters are identified by window label everywhere — `window_hwnds`, the frontend `?windowLabel=` URL, and the `on_before_close` close path. * feat(floating-pane): reducer-backed maximize/restore button for floaters Adds the floating half of the shared maximize button (SPEC_PANE_STATE_REDUCER §3.3a). Docked panes keep layout magnify; a torn-off floating pane gets an OS-window maximize/restore routed through the host reducer. - Host: new `toggle_floating_maximize` IPC handler (commands/window/chrome.rs) dispatches `HostCommand::ToggleFloatingMaximize { label }`, then applies the `ShowWindow(SW_MAXIMIZE/SW_RESTORE)` side-effect AFTER dispatch from the emitted `PaneWindowStateChanged` (snapshot-and-drop; no I/O in the reducer), resolving the floater's outer HWND via `resolve_window_hwnd(window_hwnds)`. Returns `{ placement }` so the button can sync its icon (no host->frontend push channel on main). Registered in ipc.rs route_command. - Frontend: `FloatingMaximizeButton` in blockframe EndIcons, shown only when the window carries a `?windowLabel=floating-…` param; reuses MagnifyIcon so maximize and magnify read as one operation. Deliberately independent of edge-resize: no HTTRANSPARENT WM_NCHITTEST child subclass, so it cannot perturb redock hit-testing the way #1132's resize did. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(window): cache the OUTER top-level HWND, not the CEF inner child Regression (main): dragging the main window did nothing and floating-pane redock ghosts stopped appearing once any window's HWND was cached. Root cause: `capture_hwnd_for_label`'s fast path stored `host.window_handle()` verbatim. For the main Views window (and any `set_as_child` browser) that handle is the CEF inner WS_CHILD, not the outer top-level frame. The cache-first `resolve_window_hwnd` returns cache hits WITHOUT a GA_ROOT walk (correct — walking up would land on a floater's owner), so the child leaked through to: - `set_window_position`: SetWindowPos moved the child within its parent, so the main-window title-bar drag looked dead. - `resolve_window_at_cursor`: the redock Z-order walk matches real top-level frames against `window_hwnds`; the main frame was never in the map (only its child was) → no redock target → no drop ghost. This regressed when the cache-first resolve landed: the original by-label resolve (#1089/8821c2e0) went through the reducer registry WITH a GA_ROOT walk, so it always returned the outer frame. Fix: walk GA_ROOT once at capture time so the cache always holds the outer top-level — the same invariant floater-create already satisfies (it registers the outer popup HWND it built). Floater entries are pre-registered and preserved by the existing early-return, so they're unaffected; the fallback path already enumerates top-level windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(floating-pane): work-area maximize + normal-rect restore; fixed maximize button Two bugs in the floating maximize button from this PR: 1. Maximize didn't maximize — it parked the floater at the top-left at its current size ("a copy pinned to the left"). Cause: the floater is a borderless `WS_POPUP | WS_THICKFRAME` window (no WS_CAPTION/WS_MAXIMIZEBOX), and `ShowWindow(SW_MAXIMIZE)` can't compute a maximized rect for it. The PR's assumption that native WINDOWPLACEMENT would remember the pre-maximize rect holds for normal windows but not borderless popups. Fix (the deferred normal-rect reducer phase, brought forward — it's needed now): `ToggleFloatingMaximize` carries the floater's current rect; on Normal→Maximized the reducer stashes it as `last_known_normal_rect`, on Maximized→Normal it returns it as `PaneWindowStateChanged.restore_rect`. The IPC handler sizes to the monitor work area (`MonitorFromWindow` + `GetMonitorInfoW().rcWork`) on maximize and `SetWindowPos`-es back to the captured rect on restore — Win32 physical px throughout, no SW_MAXIMIZE. 2. The icon never reflected state (looked the same maximized vs normal) in both docked and floating. Per product call: don't flip it — `FloatingMaximizeButton` is now a FIXED "Maximize" button that just toggles. Removes the mirrored frontend `maximized()` signal entirely; the reducer is the single source of truth for placement (no drift). Users expect a maximize button to toggle. Reducer stays pure (rect tracked in state, Win32 applied in the handler after dispatch — snapshot-and-drop). +1 reducer test (capture-on-maximize / return-on-restore); 47 reducer tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(floating-pane): resize embedded CEF child on outer-window resize (WM_SIZE) Completes the floating-maximize fix. With the work-area maximize in place, the outer floater correctly fills the monitor work area — but the embedded CEF browser child stayed at its creation size, leaving the pane content pinned small in the top-left of the maximized window ("a copy pinned to the left"). Confirmed live: maximized floater outer = (0,0) 900x1570 (= work area), but its CefBrowserWindow child = (0,0) 200x1039 (unchanged). Cause: CEF `set_as_child` browsers don't self-resize, and the floater's custom wndproc replaced CEF's default proc — so nothing resized the child on outer resize. Fix: a WM_SIZE arm that SetWindowPos-es the immediate child (GW_CHILD) to the client rect; CEF cascades to its own render-widget children. Also makes the floater correctly resizable in general (not just maximize/restore). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(floating-pane): correct docstrings to match the activated reducer design Addresses reagent's 3 P2 doc-drift findings on #1159 (max-effort review). The fixed-button decision + activating the previously-dormant reducer left three docstrings describing the old design: - pane_window.rs module header: said pane_window_states is keyed by block_id (now label), that ToggleFloatingMaximize is DORMANT (this PR dispatches it from the IPC + button), and that cleanup-on-close is a later phase (now done via EvictFloatingPaneWindowState from on_before_close). - chrome.rs toggle_floating_maximize: said it calls ShowWindow (now SetWindowPos to the work area / restore rect) and that the return drives the button icon (the button is intentionally fixed — it ignores the result). - mod.rs PaneWindowStateChanged: said renderers subscribe to sync the button icon (none do; the button is stateless, reducer is the source of truth). Doc-only; no behavior change. The Kimi P1 about stale block_id eviction in panes.rs is a false positive — panes.rs has no pane_window_states references (grep-verified); AgentY's re-key removed them cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(floating-pane): guard against stashing a degenerate restore rect Addresses Kimi's defensive flag: only stash the floater's current rect as the restore target when GetWindowRect succeeds AND the rect has positive area (right>left, bottom>top). A failed/degenerate rect now yields None, so the later restore is a safe no-op instead of sizing the floater to a 0x0 or inverted rect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: AgentY-asaf <253608533+AgentY-asaf@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: AgentA-asaf <agenta@asafebgi.com>
…te (adopt #1132 approach) My first cut installed the WM_NCHITTEST forwarder on ALL descendants in on_after_created — wrong target + wrong timing. Live diagnostics proved it: gate fired, hooked=0/2, forwarder HTTRANSPARENT=0 (the window receiving the edge hit-test was never hooked), yet the PARENT's WM_NCHITTEST fired 6x — the mechanism is sound, the install was wrong. Adopt the proven approach from the parked #1132 work (AgentY): the OS sends WM_NCHITTEST to the topmost window under the cursor = the floater's DIRECT child (CefBrowserWindow), so subclass THAT, installed synchronously right after browser_host_create_browser (the GW_CHILD exists then; on_after_created is too late). Forwarder returns HTTRANSPARENT in the resize band → falls through to the outer wndproc → native WS_THICKFRAME resize. Keeps WM_NCCALCSIZE=0 (frameless, no inset) — #1132 tried the inset and abandoned it because DWM overpaints a thick white border that can't be repainted. - `install_child_nchittest_forwarder(child)` — self-contained subclass with its own original-wndproc map; prunes on WM_NCDESTROY (recycled-HWND safety). Unified on the shared `floater_resize_hittest` band (parent + forwarder match). - Installed at create on the frontend's GW_CHILD; re-installed on a browser pane's web-content child (the floater's 2nd direct child) from browser_pane::callbacks. - Removed the broken EnumChildWindows/on_after_created install + diagnostics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1177) * feat(floating-pane): edge-resize via HTTRANSPARENT hit-test forwarder (PR A) Edge-hover/drag on a floating pane was a no-op. The floater's parent wndproc already maps a 6px resize border in WM_NCHITTEST and the window is WS_THICKFRAME, but the embedded CEF child windows fill the client area (incl. the border) and receive the edge mouse events first, so the parent's HT{LEFT,…} zones were never reached and native resize never started. Fix (the deferred "HTTRANSPARENT WM_NCHITTEST forwarder" from #1159): subclass the floater's descendant CEF windows so that, over the resize border, they return HTTRANSPARENT — Windows then falls through to the floater's own WM_NCHITTEST (HT{LEFT,…}) and runs the native resize loop. Off the border, and for all other messages, they delegate to the original wndproc unchanged (content clicks, scroll, typing, JS header-drag all untouched). The #1173 WM_SIZE handler already reflows the frontend browser on resize. - Shared `floater_resize_hittest` border math, used by BOTH the parent wndproc and the forwarder so the strips are pixel-identical. - `install_floating_resize_forwarder` enumerates ALL descendants (EnumChildWindows recurses), reaching the deepest Chromium render-widget window that actually receives the edge hit-test. Idempotent — re-run from the browser-pane on_after_created so a pane's web-content child (created after the frontend browser) is also covered. - `uninstall_floating_resize_forwarder` on floater close restores the subclassed wndprocs + drops map entries (HWND values can be recycled). Win32-only (the floater is a Win32 WS_POPUP; Linux/macOS resize is separate platform parity). Docs: docs/analysis/REPORT_… + docs/specs/SPEC_FLOATING_PANE_EDGE_RESIZE_2026_05_29.md. Known caveat: Chromium can recreate its render-widget HWND (cross-process nav); the forwarder re-installs on pane on_after_created but a mid-life recreate could need a re-trigger — to be hardened if it manifests. Phase A of the 3-phase plan (B: ReportNormalRect reducer; C: dimension overlay). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(floating-pane): edge-resize forwarder install on GW_CHILD at create (adopt #1132 approach) My first cut installed the WM_NCHITTEST forwarder on ALL descendants in on_after_created — wrong target + wrong timing. Live diagnostics proved it: gate fired, hooked=0/2, forwarder HTTRANSPARENT=0 (the window receiving the edge hit-test was never hooked), yet the PARENT's WM_NCHITTEST fired 6x — the mechanism is sound, the install was wrong. Adopt the proven approach from the parked #1132 work (AgentY): the OS sends WM_NCHITTEST to the topmost window under the cursor = the floater's DIRECT child (CefBrowserWindow), so subclass THAT, installed synchronously right after browser_host_create_browser (the GW_CHILD exists then; on_after_created is too late). Forwarder returns HTTRANSPARENT in the resize band → falls through to the outer wndproc → native WS_THICKFRAME resize. Keeps WM_NCCALCSIZE=0 (frameless, no inset) — #1132 tried the inset and abandoned it because DWM overpaints a thick white border that can't be repainted. - `install_child_nchittest_forwarder(child)` — self-contained subclass with its own original-wndproc map; prunes on WM_NCDESTROY (recycled-HWND safety). Unified on the shared `floater_resize_hittest` band (parent + forwarder match). - Installed at create on the frontend's GW_CHILD; re-installed on a browser pane's web-content child (the floater's 2nd direct child) from browser_pane::callbacks. - Removed the broken EnumChildWindows/on_after_created install + diagnostics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(floating-pane): JS-driven edge-resize (replace abandoned forwarder) Floating panes can now be resized by dragging any edge or corner. The HTTRANSPARENT hit-test forwarder (PR A) was abandoned: on cef-rs 146 Chromium consumes WM_NCHITTEST before any child-wndproc subclass sees it, so the forwarder got zero hits (confirmed with live diagnostics). A WM_SYSCOMMAND(SC_SIZE) fallback also failed — the post succeeds, but Chromium holds the OS mouse capture from the DOM pointerdown and the host's ReleaseCapture is cross-thread (no-op), so the native resize loop can't track the mouse. Shipped instead — JS-driven resize, mirroring the working JS header move: - floating-pane-workspace.tsx detects an edge pointerdown within a 12px band, takes pointer capture (keeps receiving moves after the cursor leaves the window), reads the start rect via get_window_rect, and on each move computes the new rect (delta + which of 8 edges) and calls set_window_rect -> SetWindowPos. Moves coalesce one-IPC-in-flight. - New host IPC get_window_rect / set_window_rect (commands/window/motion.rs). - Browser floaters: the web-content child is a separate OS window over the content region; browser-view.tsx insets it by the band depth on left/right/bottom so the frontend owns the grab band (placeholder paints the strip — no native white border). - Shared band constant (floater-resize.ts) keeps detector + inset in sync. Reverts all forwarder code (floating_pane.rs, client/mod.rs, browser_pane/callbacks.rs to base) and removes the SC_SIZE start_window_resize. Resolves codex P2 (forwarder reinstall-on-navigation) by deletion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Closing — stale base, P0 version downgrade from stale rebase. Reopen rebased on current main. |
There was a problem hiding this comment.
ReAgent Diagnostics
| Field | Value |
|---|---|
| ReAgent Version | 5.12.63 |
| Trigger | PR reopened |
| Project Context | CLAUDE.md loaded |
| Model | claude-sonnet-4-6 |
| Effort | xhigh |
| Ref Repos | Enabled (dev-tools, shared-infrastructure) |
| Merge Safety | No recent-code deletions |
| Review Time | 6m 48s |
| Timestamp | 2026-06-01T09:51:16Z |
| Repository | agentmuxai/agentmux |
| PR | #1132 |
Now I have all the information I need. Let me compile the final review.
Issues:
- [P1] package.json:9 - Version downgrade: diff shows 0.40.1 → 0.39.2 (same in Cargo.toml); stale merge artifact from commit 6a454e8 resolving version conflict to the pre-main-bump value — rebase on current main before merging
- [P1] frontend/app/workspace/floating-pane-workspace.tsx:253 - Race: if mouseup fires during the
restore_window_and_moveawait (after guard at line 216 passes but before guard at line 253 executes), the second guard returns early without callingsetFloatingPaneInfo({state:"normal"}); the window is physically restored butfloatingPaneInfostays"maximized", leaving the header button permanently stuck showing the wrong glyph - [P2] frontend/app/block/blockframe.tsx:222 -
toggleFloatingMaximizeonly catches thrown exceptions; ifinvokeCommandresolves tonull/undefinedwithout throwing,res?.stateisundefined, the ternary defaults to"normal", andsetFloatingPaneInfosilently records the wrong state without surfacing the failure
Summary
Hand-rolled resize and maximize for the floating-pane WS_POPUP + WS_EX_TOOLWINDOW window. CEF Views isn't available for floaters (the no-taskbar / owned-cascade requirement forces native Win32 — see
SPEC_FLOATING_PANE_TEAROFF_2026_05_11 §10), so the wndproc carries the load.Spec:
docs/specs/SPEC_FLOATING_PANE_RESIZE_AND_MAXIMIZE_2026-05-28.mdWhat changes
Resize (Win32 wndproc) —
agentmux-cef/src/floating_pane.rsWM_GETMINMAXINFO: 320×180 CSS-px min size + maximize work-area clamp (so maximize respects the taskbar instead of going full-monitor like raw WS_POPUP defaults to).WM_SIZE: track the embedded CEFWS_CHILDonto the outer's client size. Children of WS_POPUP do not auto-track parent size, so without this the rendered content stayed at the initial (W, H).WM_NCHITTESTband 6 → 8 CSS px for discoverability while staying clear of header buttons.Maximize (label-routed) —
agentmux-cef/src/commands/window.rsmaximize_windownow routes viaresolve_window_hwnd(PR fix(floating-pane): suppress focus-ring + route window-drag by label #1094'swindow_hwndscache pattern) instead offind_own_top_level_window— fixes the long-standing bug where a label-less call from main accidentally hit the floater (owned windows draw above their owner, so Z-order EnumWindows returned the floater first).floating-…labels, stashes the pre-maximize outer rect into a newstate.floating_restored_rectsmap so the post-restore origin can be replayed atomically byrestore_window_and_move(drag-from-maximized) or implicitly by the next toggle.{state}so the FE can update its icon glyph without a follow-upget_window_statequery.New IPCs
set_window_size({label, w, h})— companion toset_window_position; CSS px, label-routed.get_window_state({label})— returns"normal" | "maximized" | "minimized"fromGetWindowPlacement.consume_restored_rect({label})— drains the floater's pre-maximize rect; used by drag-from-maximized.restore_window_and_move({label, x, y, w, h})— atomicSW_RESTORE+SetWindowPosto avoid a flash of the old maximized origin mid-drag.Frontend —
frontend/app/store/floating-pane-context.ts: a process-wide signal holding{windowLabel, state}when the renderer is the floating shell. Null elsewhere, so docked panes are unaffected.block/blockframe.tsxEndIcons: maximize/restore button immediately left of close-X, only when the floating context is non-null. Toggle reusesmaximize_window's return value to swap the glyph.workspace/floating-pane-workspace.tsx:get_window_state; clear on cleanup.consume_restored_rect+restore_window_and_moveso there's no IPC flash between the SW_RESTORE and the move.Why this approach
History (full table in spec §0):
browser_host_create_browserhad no resize. But that lesson doesn't transfer — floaters needWS_EX_TOOLWINDOW + ownersemantics CEF Views doesn't expose, so the parent spec chose the native popup.WM_NCHITTEST+WM_NCCALCSIZE=0+DwmExtendFrameIntoClientAreapattern for resize on the native popup. We extend that pattern; we do not replace it.BlockFrame_Header.SPEC_MAXIMIZE_ZOOM_ARCHITECTURE §3.P5flags missing window-state events. This PR deliberately does not add awindow_state_changedevent channel —maximize_window's return value covers user-initiated toggles (button + dblclick). Win+Up / programmatic ShowWindow paths from outside the IPC are a follow-up; the spec calls them out.Caveats / things to validate
install_frameless_resize_hooktemplate inwndproc.rs:64-136and theset_window_positionlabel-routing template incommands/window.rs:388-422— but atask devsmoke is the first real verification.maximize_windowstill routes throughcrate::ui_tasks::post_maximize_window(unchanged) but no longer returnsValue::Null— it now returns{state: "normal"}as a placeholder. Floating-pane is Windows-only, so cross-platform maximize behavior shouldn't regress, but worth a quick check ontask devon each OS.window-maximize/window-restoreare already in use elsewhere (command-registry.ts:207,tabbar.tsx:658), so no new icon assets needed.Test plan
floating_restored_rectsentries don't collide.floating_restored_rectsentry (labels are UUID-tagged so collision would never matter, but worth confirming).useWindowDrag.win32.tspath is untouched).Follow-ups deferred from spec
window_state_changedevent channel (for keyboard Win+Up / programmatic maximize paths that bypass the IPC).SPEC_AGENT_PANE_BOTTOM_BUTTONS_2026_04_22.mdprecedent).🤖 Generated with Claude Code