Skip to content

feat(floating-pane): resize + maximize support#1132

Closed
Agent2-asaf wants to merge 9 commits into
mainfrom
agent2/floating-pane-resize-maximize
Closed

feat(floating-pane): resize + maximize support#1132
Agent2-asaf wants to merge 9 commits into
mainfrom
agent2/floating-pane-resize-maximize

Conversation

@Agent2-asaf
Copy link
Copy Markdown
Contributor

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.md

What changes

Resize (Win32 wndproc)agentmux-cef/src/floating_pane.rs

  • WM_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 CEF WS_CHILD onto 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_NCHITTEST band 6 → 8 CSS px for discoverability while staying clear of header buttons.

Maximize (label-routed)agentmux-cef/src/commands/window.rs

  • maximize_window now routes via resolve_window_hwnd (PR fix(floating-pane): suppress focus-ring + route window-drag by label #1094's window_hwnds cache pattern) instead of find_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).
  • For floating-… labels, stashes the pre-maximize outer rect into a new state.floating_restored_rects map so the post-restore origin can be replayed atomically by restore_window_and_move (drag-from-maximized) or implicitly by the next toggle.
  • Returns {state} so the FE can update its icon glyph without a follow-up get_window_state query.

New IPCs

  • set_window_size({label, w, h}) — companion to set_window_position; CSS px, label-routed.
  • get_window_state({label}) — returns "normal" | "maximized" | "minimized" from GetWindowPlacement.
  • 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}) — atomic SW_RESTORE + SetWindowPos to avoid a flash of the old maximized origin mid-drag.

Frontendfrontend/app/

  • New 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.tsx EndIcons: maximize/restore button immediately left of close-X, only when the floating context is non-null. Toggle reuses maximize_window's return value to swap the glyph.
  • workspace/floating-pane-workspace.tsx:
    • Seed the floating context on mount from get_window_state; clear on cleanup.
    • dblclick listener on the pane header (mirrors PR feat: double-click titlebar to maximize/restore #315's main-window pattern); suppresses the bubble-phase magnify-toggle since magnify on a single-pane floater is meaningless.
    • Drag-from-maximized branch in mousedown — Windows-explorer-style: dragging a maximized window restores it under the cursor and continues the drag. Uses consume_restored_rect + restore_window_and_move so there's no IPC flash between the SW_RESTORE and the move.

Why this approach

History (full table in spec §0):

Caveats / things to validate

  • I cannot compile or run this in the container. Windows-only host code, container is Linux, no cargo. The Rust changes follow the proven install_frameless_resize_hook template in wndproc.rs:64-136 and the set_window_position label-routing template in commands/window.rs:388-422 — but a task dev smoke is the first real verification.
  • The Linux/macOS branch of maximize_window still routes through crate::ui_tasks::post_maximize_window (unchanged) but no longer returns Value::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 on task dev on each OS.
  • The icon names window-maximize / window-restore are already in use elsewhere (command-registry.ts:207, tabbar.tsx:658), so no new icon assets needed.

Test plan

  • Resize floater from each edge / corner; cursor changes; CEF child follows outer 1:1; cannot shrink below 320×180 CSS px.
  • Click maximize button in header → floater fills monitor work area (taskbar visible). Icon swaps to restore.
  • Click restore → floater returns to pre-maximize rect.
  • Double-click pane header (non-interactive zone) toggles maximize. Magnify does NOT also toggle.
  • Drag a maximized floater's header → floater restores under cursor and follows mouse. Release over main window → redocks per PR feat(floating-pane): MVP re-dock — drop floater over agentmux window #1112.
  • Resize floater on a 100% monitor, drag to a 200% monitor, maximize → fills 200% monitor's work area (not stretched 2×).
  • Open two floaters, maximize one, drag/move/resize the other — independent. floating_restored_rects entries don't collide.
  • Close a maximized floater — no leaked floating_restored_rects entry (labels are UUID-tagged so collision would never matter, but worth confirming).
  • Main-window dblclick maximize still works (the existing useWindowDrag.win32.ts path is untouched).

Follow-ups deferred from spec

  • window_state_changed event channel (for keyboard Win+Up / programmatic maximize paths that bypass the IPC).
  • Header maximize-button overflow at narrow widths (collapse into existing overflow menu — see SPEC_AGENT_PANE_BOTTOM_BUTTONS_2026_04_22.md precedent).
  • Optional: docked-pane "maximize tile within window" affordance (separate spec interacting with TileLayout).

🤖 Generated with Claude Code

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>
@a5af
Copy link
Copy Markdown
Member

a5af commented May 28, 2026

@codex review

Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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 - 16 uses 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 by dpr (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_size is 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_hwnd returns null (label not found), maximize_window silently 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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +204 to +205
const currentState = useFloatingPaneInfo()?.state;
if (currentState === "maximized") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread agentmux-cef/src/floating_pane.rs Outdated
Comment on lines +398 to +400
(*mmi).ptMaxPosition = POINT {
x: work.left - wr.left,
y: work.top - wr.top,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@a5af
Copy link
Copy Markdown
Member

a5af commented May 28, 2026

@codex review

Addressed:

  • Codex P2 floating_pane.rs:398 (ptMaxPosition off-screen): fixed. ptMaxPosition is now the work-area origin directly (work.left, work.top), not a delta from wr. The old delta math placed the maximized window off-screen for any floater not already at the work-area origin.
  • Codex P2 floating-pane-workspace.tsx:205 (restore on every mousedown): fixed via a 4-CSS-px movement threshold matching Windows SM_CXDRAG. pendingMaximizedRestore is armed on mousedown over a maximized header, cleared on mouseup, and triggers the restore-under-cursor dance only when mousemove crosses the threshold. Plain clicks and the mousedown half of a dblclick fall through harmlessly.
  • ReAgent P2 floating-pane-workspace.tsx:228 (DPI-scaled offset): fixed. The 16 cursor-below-top-edge offset is now Math.round(16 * dpr). At 200% DPI it stays at 16 CSS px instead of collapsing to 8 and hitting the top resize band.
  • ReAgent P2 commands/window.rs maximize_window null label: fixed. The Windows branch now returns Err("...label not resolved to a window") instead of silently falling through to the cross-platform Ok({state:"normal"}) default. The frontend will keep its old glyph and surface the failure.
  • ReAgent P2 commands/window.rs set_window_size speculative API: left in place. The PR body explicitly cites spec §5.2 as the deferred-use rationale, and the IPC + Rust handler are mirror-images of the set_window_position template — removing them now would force the next FE-side PR to add them back. Happy to drop them if the team prefers no dead APIs at all.

reagentx-workflow[bot]
reagentx-workflow Bot previously approved these changes May 28, 2026
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +404 to +407
(*mmi).ptMaxPosition = POINT {
x: work.left,
y: work.top,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread agentmux-cef/src/commands/window.rs Outdated
let hwnd = find_own_top_level_window();
use windows_sys::Win32::Foundation::RECT;

let hwnd = resolve_window_hwnd(state, label);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
reagentx-workflow[bot]
reagentx-workflow Bot previously approved these changes May 28, 2026
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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

@a5af
Copy link
Copy Markdown
Member

a5af commented May 28, 2026

@codex review

Round 2 of fixes pushed in c7bd575:

  • floating_pane.rs:407 ptMaxPosition is now work.{left,top} minus mon.{left,top} (work-area offset within its monitor). Per Raymond Chen, Windows treats ptMaxPosition as primary-monitor-relative and adjusts by the target offset, so absolute coords get offset twice on non-primary monitors. The new form computes (0, 0) on a primary monitor with bottom taskbar, and (0, 0) on a non-primary with the same layout — the OS then adds the target monitor offset.

  • commands/window.rs maximize_window label cache strict: for floating- labels we now consult state.window_hwnds directly and treat cache miss as null, skipping resolve_window_hwnd's fallback to find_own_top_level_window. Stale floater labels now surface as the Err path so the FE keeps the old glyph. Non-floater labels keep the prior resolution chain.

  • Floating-pane resize regression from user smoke test: CEF child was filling the entire outer client area, so the OS dispatched WM_NCHITTEST to the child (returns HTCLIENT) and never bubbled to the outer wndproc. Inset the child by resize_border_px (8 CSS px) in WM_SIZE. The inset matches the hit-band width and is visually indistinguishable from a system resize border.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +443 to +448
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +169 to +173
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(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@a5af
Copy link
Copy Markdown
Member

a5af commented May 28, 2026

@codex review

Round 3 pushed in 87e16a6 + merged main (6a454e8):

  • commands/window.rs:173 IsWindow validation for floater cache lookups. After the cache hit, IsWindow(hwnd) != 0 check rejects destroyed HWNDs so the Err path fires instead of toggling a zombie. PR fix(cef): evict stale HWND from window_hwnds cache + cleanup on close #1133 (already on main) adds eviction on close; this is defense in depth against the race window between close and cleanup.

  • floating_pane.rs initial-creation inset. The CEF child is now created with (inset, inset, w - 2*inset, h - 2*inset) rect, using the same DPI-scaled RESIZE_BORDER_CSS math as WM_NCHITTEST. The first WM_SIZE on the outer fires from inside create_owned_popup's ShowWindow before the child exists, so the WM_SIZE inset path no-ops on initial open — applying the inset at creation closes that gap.

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).

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

reagentx-workflow[bot]
reagentx-workflow Bot previously approved these changes May 28, 2026
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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).
reagentx-workflow[bot]
reagentx-workflow Bot previously approved these changes May 28, 2026
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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.
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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 to trace!) before shipping — paint messages fire continuously and will swamp muxlog 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.
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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-global ORIGINAL_WNDPROCS map 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_key short-circuits the installer, the forwarder is never installed on the new HWND, and edge-band resize silently breaks for that floater. Mirror the round-3 IsWindow defense added to window_hwnds (or intercept WM_NCDESTROY in 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.
@a5af
Copy link
Copy Markdown
Member

a5af commented May 29, 2026

@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.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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-28 section that exists on main. Restore by taking main's version of this file.

@a5af
Copy link
Copy Markdown
Member

a5af commented May 29, 2026

Handoff notes — state + open investigation (AgentY)

Parking this PR open. Summary of where it stands so it can be picked up cleanly.

Resolved

  • White border around floater — fixed via the WM_NCHITTEST forwarder approach (6d471091): the CEF child fills the window and returns HTTRANSPARENT on edge bands so the outer wndproc handles resize. No inset strip → no DWM-overpainted white border.
  • Floater resize — works (edge bands resize via the forwarder).
  • ptMaxPosition off-screen on non-primary monitor — fixed (work - mon offset).
  • Magenta debug paint P0 (ReAgent 22:20) — was on the superseded diagnostic commit c15392da; 6d471091 already removed all of it (no WM_ERASEBKGND paint, no per-paint logging, no WM_NCPAINT suppression). The only floating-pane-diag line left is a one-time install log per floater.
  • ORIGINAL_WNDPROCS map never pruned P2 (ReAgent 22:28) — fixed in ebf015a5: the forwarder now handles WM_NCDESTROY, forwards to the original wndproc for CEF teardown, then removes its own map entry, so a recycled HWND can't short-circuit the installer.

Open — two runtime regressions, NOT yet root-caused

  1. Parent (main) window drag stopped working in the dev build while a floater is open. Dev log showed zero set_window_position IPC fires on main-drag attempts.
  2. Redock ghosts (hover placeholder) not showing during a floater drag-over-main.

Investigation finding — the diff doesn't explain it

Walked origin/main...HEAD:

  • useWindowDrag.win32.ts (main-window drag hook) and app-init.ts (redock-hover listener) are untouched by this PR — byte-identical to main (both from feat(floating-pane): MVP re-dock — drop floater over agentmux window #1112).
  • resolve_window_at_cursor, find_main_window, and the redock-hover host handlers in commands/window.rs are untouched. feat(floating-pane): resize + maximize support #1132 only changed maximize_window to route via resolve_window_hwnd, and added 4 new fns (set_window_size, get_window_state, restore_window_and_move, consume_restored_rect).
  • floating-pane-workspace.tsx changes are purely additive — they only alter behavior when the floater is maximized; a normal floater drag falls straight through to the original redock path and still calls pushRedockHover.
  • The HTTRANSPARENT forwarder is installed only on the floater's own CEF child (GW_CHILD).

So statically, #1132 has no obvious mechanism to break normal docking/ghosts or main-window drag.

Prime suspect for pickup

Runtime mouse-routing during cross-window drag. Specifically: if resolve_window_at_cursor (or any redock target-finding) uses WindowFromPoint, that API skips windows that return HTTRANSPARENT to WM_NCHITTEST — so the new forwarder could perturb which window is found under the cursor during a drag near a floater edge. Worth checking whether redock resolution uses WindowFromPoint vs EnumWindows Z-order (the latter is unaffected by HTTRANSPARENT).

Decisive next step (settles the merge question)

A/B test: does redock + ghosts work on plain main (no #1132)?

Also re the parent-drag zero-IPC observation: rule out a dev-mode artifact first — in task dev the main window can be served from a pool window whose URL carries windowLabel=window-pool-…, so useWindowDrag's ownWindowLabel() may target a label the host can't resolve for set_window_position. May not reproduce in a portable build.

Branch HEAD: ebf015a5. ReAgent will re-review on it; the only finding it had on the prior commit (the map-pruning P2) is addressed.

AgentA-asaf pushed a commit that referenced this pull request May 29, 2026
… (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.
AgentA-asaf added a commit that referenced this pull request May 29, 2026
… (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>
AgentA-asaf pushed a commit that referenced this pull request May 29, 2026
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>
AgentA-asaf added a commit that referenced this pull request May 29, 2026
)

* 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>
AgentA-asaf added a commit that referenced this pull request May 29, 2026
…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>
AgentA-asaf added a commit that referenced this pull request May 30, 2026
…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>
@a5af
Copy link
Copy Markdown
Member

a5af commented Jun 1, 2026

Closing — stale base, P0 version downgrade from stale rebase. Reopen rebased on current main.

@a5af a5af closed this Jun 1, 2026
@a5af a5af reopened this Jun 1, 2026
Copy link
Copy Markdown

@reagentx-workflow reagentx-workflow Bot left a comment

Choose a reason for hiding this comment

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

Claude

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_move await (after guard at line 216 passes but before guard at line 253 executes), the second guard returns early without calling setFloatingPaneInfo({state:"normal"}); the window is physically restored but floatingPaneInfo stays "maximized", leaving the header button permanently stuck showing the wrong glyph
  • [P2] frontend/app/block/blockframe.tsx:222 - toggleFloatingMaximize only catches thrown exceptions; if invokeCommand resolves to null/undefined without throwing, res?.state is undefined, the ternary defaults to "normal", and setFloatingPaneInfo silently records the wrong state without surfacing the failure

@a5af a5af closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants