Skip to content

fix(terminal): fold an attached pane a phone watches at a wider grid - #1963

Merged
devswha merged 4 commits into
mainfrom
feat/wrap-view-attach
Oct 26, 2026
Merged

devswha merged 4 commits into
mainfrom
feat/wrap-view-attach

Conversation

@devswha

@devswha devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Problem

#1961 folds a mirrored pane (a Windows PC) that is wider than a phone. An attached pane (a Linux or
macOS PC) gets cut off in the same way whenever the phone is not the one sizing the pty:

  • A device paired to watch draws the pty's grid, and a desktop sized that grid. In QA a desktop
    held the pane at 174x53. On a 390 px phone every row ran off the right edge, with 1365 px to pan
    sideways.
  • A driving phone with its soft keyboard up keeps the grid it shows until the keyboard goes
    (Terminal (phone): opening the soft keyboard resizes the shared pty #1045). If a desktop takes the pty in the meantime, the phone draws the desktop's grid. The same
    happens when the terminal lens comes back from Chat with the keyboard still up.

When the phone drives the pty, the program redraws at the phone's width, so there is nothing to fold.
The Collie comparison (lane collie-research, section 8 item 1) recommended this reading view for
these cases.

Fix

The change is client only. server/, shared/, herdr and the way devices share a pane's size are
unchanged.

  • lib/terminalFold.ts: whether to fold is now decided by foreign() (this browser does not size
    the grid) and wrap() (the user's choice). The fold also reports when a foreign grid is wider than
    its mount.
  • lib/useTerminalMount.ts: a grid counts as foreign when it is a mirror, a watched grid, or a grid
    held under the keyboard. A split's following half never counts. The folded rows come from the xterm
    buffer the attach stream wrote: there is no pane.read, scrollback stays 0, and nothing resizes,
    takes over or refocuses the pane. Cursor, IME and hidden-input handling is fix(terminal): fold a mirrored pane wider than a phone to the screen #1961's: the hidden grid
    still moves under the folded cursor row.
  • Pane menu: a small wideGrids store, fed by the fold, lets the tab's ⋯ sheet offer Original
    width
    / Wrap to screen for such panes, as it already did for mirrors. It uses the same device
    setting (terminalMirrorWrap, key ovis:mirror-wrap), which is not renamed, so choices already
    saved still apply. A driving phone's menu does not show the item.
  • Under the keyboard the grid is not adopted, so its mount had overflow: hidden. Every fold now
    scrolls up and down, and a finger pans the folded rows before it scrolls the pane at their edges.
  • Pane switch: switching panes decides the fold again, so the menu stops offering the width
    choice for the pane left behind.
  • Taps: a tap on folded rows opens a link or focuses the terminal, as on a mirror. It does not
    send a mouse click to the program. herdr's attach turns on mouse reporting for every pane
    (?1000h ?1002h ?1003h ?1006h, seen on a plain bash pane and on btop), so the client cannot tell
    which program wants clicks. Original width still passes clicks through. DESIGN.md says so.

Design precedent: the same as #1961. The apps this repo compares against size their terminal to
the window, which a watching device cannot do. This follows Collie (AltanS/collie,
ansi-output.tsx), which never resizes a pane. No code was copied. DESIGN.md extends #1961's entry.

Validation

scripts/mirror-fold-demo-regression.ts (browser lane) now covers an attached pane whose pty grid
(120x40) is wider than the phone. The fixture replays herdr's mouse modes, as a real attach does.

  • Driving: the grid is the phone's own and fitted, nothing is folded, and the menu has no
    Original width.
  • Watching: folded whole at 390, 412, 915x412 and 844x390. The folded rows are the stream's rows
    in order, and the Korean words fold whole.
  • Toggle: the menu offers Original width. Original width shows a btop-like box at full width,
    and real swipes pan it to its right edge. Wrap to screen folds it again. Both choices are kept
    in ovis:mirror-wrap.
  • Back to driving: the grid is fitted again and unfolded.
  • Under the keyboard at another device's grid: folded, cursor row in view, a Korean preedit
    drawn over the folded rows, panned by wheel and by swipe. Once the keyboard goes, the catch-up fit
    unfolds it.

What fails without the change:

  • On origin/main the script fails at foldState(watching): {"folded":false}.
  • Each mutation of the change fails at the assertion that names it: dropping the keyboard case,
    showing the menu item only for mirrors, gating the keyboard fold on "no mouse reporting", and the
    first commit without the scroll fixes (scrollTop stuck under the keyboard).
  • All of fix(terminal): fold a mirrored pane wider than a phone to the screen #1961's mirror assertions still pass, including the preedit-topmost check.

Other checks:

  • src/lib/terminalFold.test.ts: the wideGrids store keeps a pane wide while any terminal that
    draws it says so.
  • bun run check fast is green on a091b37 (unit tests: 7176 pass, 0 fail).

Real apps were run against an isolated test herdr (bun run check run) with an omp-like pane (long
Korean lines, a status box) and btop. A 1700 px desktop client held the pane at 174x53. The phone was
a device paired to watch: the server reported via: device, role: watch. "Before" is an origin/main
server and "after" is this branch's.

Device origin/main This branch
Headless Chrome, watching, 390x844, 412x915, 915x412, 844x390 174 columns, every row end cut, 1365 px to pan folded, every row end in view, nothing to pan sideways
Android app 1.0 (3), WebView 145, 412x915 dp portrait and 957x411 landscape, watching omp and btop cut off folded. A real tap on the tab ⋯ opens Original width; an adb swipe pans it (scrollLeft 535); Wrap to screen folds it again
Android app, driving with the keyboard up, then the desktop takes the pty back the desktop's 174 columns, cut off folded. Text typed with adb reaches the pane and shows in the fold. With the keyboard gone the phone takes the pane back at 50x49, unfolded
iOS Simulator app (built from main), iPhone 17e, watching - folded
Headless Chrome, driving, 412x915 redrawn at 50 columns the same: 50x44, not folded, no menu item

Screenshots and the Korean summary for the maintainer (tailnet): http://100.123.228.51:7593/

Reviews

An omo-native-code-reviewer pass approved with notes, and a Codex (gpt-6.1-sol, xhigh) pass ran on
the first commit. Neither found anything HIGH.

Finding Resolution
Codex MEDIUM: a keyboard-held fold kept overflow: hidden, so a wheel could not pan it overflow-y on every fold; the regression wheels it, and the first commit fails there
Codex MEDIUM: a touch on a keyboard-held fold scrolled the pane instead of panning the folded rows a touch pans the folded rows up to their edges first; covered by a real swipe
Codex LOW: "pans sideways" set scrollLeft directly real swipes and wheels
Reviewer MEDIUM: a keyboard-held fold eats taps meant for a mouse program tried gating on mouse reporting. The real Android app then showed that herdr's attach reports the mouse for every pane, so the gate turned the fold off everywhere. Reverted and documented as a trade-off (Original width keeps clicks); the fixture now replays herdr's modes, and the gate makes the script fail
Reviewer MEDIUM: the first "driving is never folded" checks only test the fit relabelled as fit checks; driving→unfolded is covered after watching and after the keyboard
Reviewer LOW: stale wideGrids key after a pane switch; comments; the geometry push before the keyboard step; try/finally in the unit test fixed

Known limits

  • A line the PC program already wrapped at the pane's width folds again (as in fix(terminal): fold a mirrored pane wider than a phone to the screen #1961 and Collie).
  • A full-screen TUI is hard to read folded. Original width is there for it.
  • The iOS Simulator runs headless on the test MacBook Air, with no taps or rotation, so iOS was
    checked in portrait only. Android covered the toggle and landscape.

mp added 2 commits October 26, 2026 15:35
An attached pane drawn at a grid this device does not size (a device paired to
watch, or a driving phone under its soft keyboard while the pty keeps another
device's size) is folded to the screen's width with #1961's view, drawn from the
xterm buffer the attach stream wrote. The pane menu offers Original width / Wrap
to screen for such panes under the same terminalMirrorWrap setting
(ovis:mirror-wrap kept). A phone that drives the pty is never folded.
Review follow-ups. A grid held under the soft keyboard at another device's size is
not adopted, so its mount kept overflow hidden: a wheel or a finger now pans its
folded rows (overflow-y on every fold; a touch pans before it scrolls the pane at
the folded rows' edges). A pane switch decides the fold again, so the pane menu
forgets the last pane's width. A tap on folded rows stays a link or a focus, not a
mouse click: herdr's attach reports the mouse for every pane, so a program that
wants clicks cannot be told apart (Original width keeps clicks). The regression
replays herdr's mouse modes in its attached fixture and drives real swipes and
wheels instead of setting scroll positions.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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

Run configuration

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e299f81 and a091b37.

Files selected for processing: 13
  • DESIGN.md
  • changelog.d/wrap-view-attach.md
  • docs/development.md
  • scripts/mirror-fold-demo-regression.ts
  • src/components/PaneCanvas.tsx
  • src/components/PaneTerminal.css
  • src/components/PaneTerminal.tsx
  • src/lib/settings.ts
  • src/lib/terminalFold.test.ts
  • src/lib/terminalFold.ts
  • src/lib/usePaneAttach.ts
  • src/lib/useTerminalMount.ts
  • src/lib/useTerminalRefits.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features
    • Phones watching panes sized for a wider screen can view them folded to fit, including while the soft keyboard is open.
    • Choose Original width to view the pane at its original size and pan across it. Pan folded rows vertically with touch gestures.
  • Bug Fixes
    • Panes that phones control remain fitted to the phone’s screen, while panes that fit their display stay unfolded.
    • Improved cursor and keyboard preedit visibility when folded views are used.

Attached panes with foreign grids wider than a phone now fold when the phone watches the pane or holds the grid under the keyboard. The pane menu can switch to original width. Phones driving the pane and panes following split halves remain unfolded.

Changes: Foreign Grid Folding

Layer / File(s) Summary
Foreign-grid fold state
src/lib/terminalFold.ts, src/lib/useTerminalMount.ts, src/lib/settings.ts, src/lib/terminalFold.test.ts
Folding now checks foreign-grid status, width, and the wrap setting. Mounts track wide-grid ownership and synchronize fold state during keyboard hold and restoration.
Pane menu and folded-grid interaction
src/lib/usePaneAttach.ts, src/lib/useTerminalRefits.ts, src/lib/useTerminalMount.ts, src/components/PaneTerminal.tsx, src/components/PaneTerminal.css, src/components/PaneCanvas.tsx
The pane menu offers the wrap choice for tracked wide grids. Folded rows support vertical scrolling. Attach resets and observed-terminal refits call the cursor-follow callback.
Attached-pane regression coverage
scripts/mirror-fold-demo-regression.ts, DESIGN.md, changelog.d/wrap-view-attach.md, docs/development.md
The regression fixture checks driving and watching phones, original-width panning, keyboard-held folding, and IME visibility. Documentation describes the attached-pane behavior.

Priority: Normal

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

Change: Bug fix

Possibly related issues

  • devswha/herdr-web-ui issue 1053: The PR covers related phone sizing and touch-scroll behavior for watched or keyboard-held foreign grids.
Merge Risk: Minimal · up to a091b

The attached-pane folding change is mergeable after normal checks; no concrete blocking risk remains identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 9 files. (4 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title "fix(terminal): fold an attached pane a phone watches at a wider grid" matches the main change. The PR folds attached panes whose pty grid is wider than a phone that does not size the grid. …
Description check Passed The description follows the repository template. It has a Change section that describes the problem, the fix, and the resulting behavior. It has a Validation section that lists the browser regression …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "fix(terminal): fold an attached pane a phone watches at a wider grid" matches the main change. The PR folds attached panes whose pty grid is wider than a phone that does not size the grid. The title is specific and uses a conventional commit prefix. It omits the keyboard-held case and the pane menu option, which the title does not need to cover.

Full details: Description check

Explanation

The description follows the repository template. It has a Change section that describes the problem, the fix, and the resulting behavior. It has a Validation section that lists the browser regression script, unit tests, the bun check run, and manual runs on Headless Chrome, Android, and iOS Simulator. The template asks for contract-test coverage for HTTP/WS changes. The PR states the change is client only and does not touch server/ or shared/, so that item does not apply. The description does not link an issue for the feature. The template only requires that for features, and this is a fix that references #1961 and #1045. The description also refers to a changelog entry only in the summary, not in the description body. The template asks authors to see CONTRIBUTING.md for the changelog. The changelog.d entry exists in the PR summary, so this is covered.

Full details: Docstring Coverage

Explanation

Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 9 files. (4 skipped: 4 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review of a091b37, part 1: Codex Sol (gpt-6.1-sol, xhigh) requests changes. The Astra gate is still running, and I will post its result separately.

Checks so far:

  • terminalFold.test.ts and settings.test.ts pass 91/91, and typecheck is clean.
  • The expanded scripts/mirror-fold-demo-regression.ts passes locally, including the new attached-pane, watching and keyboard cases.
  • Codex found no new bridge-trap violation: no pane.read, scrollback stays 0, and nothing resizes, takes over or refocuses the pane. The settings migration is fine.

MAJOR: an editable attached fold has no cursor, and the preedit lands on the wrong segment (src/lib/useTerminalMount.ts:410, src/lib/terminalFold.ts:315)

  • What happens:
    • Folding an attached pane that can be typed into hides xterm's cursor, and nothing replaces it.
    • The IME position also ignores the cursor's column: terminalFold.ts:315 aligns to the bottom of the whole wrapped row.
    • In a shell held under the keyboard, moving the caret back into an earlier wrapped segment leaves no visible caret, and the preedit shows on the wrong segment.
  • Fix: map the cursor cell into the folded text, and anchor both the drawn caret and the IME there.
  • Test gap: the browser fixture hides the cursor and checks only that the preedit is visible, so it does not catch this. Please add a case with the caret in an earlier wrapped segment.

MINOR: a pan that takes over a swipe leaves queued history reports (src/lib/useTerminalMount.ts:1634)

  • When a swipe switches from history scrolling to local panning, touchPixels is cleared but the reports already queued in touchPacer are not. A source-level probe queued four reports, reversed into local panning, and saw all four fire afterwards.
  • Fix: cancel the pending reports when panning claims the gesture.

The re-review will cover only the new commits.

@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review of a091b37, part 2: the Astra gate (omo-native-gate-reviewer, gpt-6-astra) approves the attached-pane fold against the criteria it was given.

  • Bridge traps hold: no pane.read for an attached pane, scrollback stays 0, and folding causes no resize, take-over or herdr focus change.
  • Mutations: both foreign() mutations were caught.
    • Mirror-only eligibility failed the watching assertion at line 552.
    • Removing keyboard-held eligibility failed at line 646.
  • Tests: mirror-fold-demo-regression.ts passes, and the 91 unit tests pass.
  • Note (predates this PR): Original width under a driving phone's held keyboard still can't be panned sideways locally.

Its scenarios did not cover the case Codex found in part 1: a caret moved back into an earlier wrapped segment. So Codex's MAJOR (no drawn cursor and a misplaced IME preedit on an editable fold) still needs the fix and a regression case before merge. The MINOR (queued touchPacer reports) should be fixed along with it.

…t on it

Merge-pass review follow-ups. A folded attached pane hid xterm's cursor and drew none,
and the IME stood at the end of the cursor's folded row. The cursor's cell is now drawn
in the folded rows in the grid's shape and colours (read from the cell xterm's DOM
renderer marks, so a mirror's hidden cursor stays hidden), and the hidden grid moves
down to the folded line the caret is on and sideways to its column: it scrolls within
.xterm as far as that goes and moves by left for the rest, so the input stays inside
.xterm's clip. A swipe that turns into a local pan also cancels the history reports it
had already queued.

The regression moves a shell's caret back into the first line of a long folded command
and onto its last, and checks the drawn caret and that a Korean preedit stands on it.
@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Review follow-up in 66aada6 (one commit, pushed after bun run check fast passed on it).

MAJOR, no caret and a misplaced preedit on an editable fold: fixed.

  • The cursor's cell is drawn in the folded rows in the grid's shape and colours (block when focused, outline when not, bar and underline from DECSCUSR). The shape is read from the cell xterm's DOM renderer marks, so a mirror's hidden cursor stays hidden.
  • The hidden grid now moves to the folded line the caret is on, not the end of the cursor's row, and sideways to its column. It scrolls within .xterm as far as its content goes and moves by left for the rest. left alone moved .xterm's own clip box, and the browser then scrolled .xterm to bring the input back: the preedit ended up 441 px away. Scrolling alone could not reach columns near the grid's right edge.
  • Regression: a shell's caret in a 108-character command that folds into 4 lines at 390 px, under the keyboard at another device's grid. The caret is moved back to the first folded line, then onto the last. For each, it checks that the caret is drawn on that line and that a Korean preedit stands on it (left within 2 px, same line).
  • On a091b37 the case fails (no caret is drawn). Removing the sideways move fails it (preedit at x 375, caret at x 8), and so does removing the line alignment (preedit on line 405, caret on line 354).

MINOR, queued history reports after a pan takes the swipe: fixed. The pan now also cancels touchPacer's queued reports.

Astra note (Original width under a driving phone's held keyboard does not pan sideways): this predates the PR, as Astra says (#1045's held grid is not adopted, so its mount clips). It is left as is here.

Checks on 66aada6: scripts/mirror-fold-demo-regression.ts passes, unit 92/92 for terminalFold and settings, typecheck clean, bun run check fast passes.

@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Merge pass re-review of 66aada6 (Codex Sol): one small fix is left.

Fixed:

  • Caret and IME (MAJOR): the caret is now drawn in an attached fold, and the IME/preedit is anchored to the cursor cell, including a caret in an earlier wrapped segment. The new regression case fails without the fix.
  • Touch reports (MINOR): queued touchPacer reports are cancelled when panning claims the swipe.
  • Bridge traps: this range adds no new violation.
  • Checks: unit tests pass 92/92, typecheck is clean, and mirror-fold-demo-regression.ts passes locally.

MINOR, still open: cursor at the right margin (src/lib/terminalFold.ts:401, and the same at :363)

  • Clamp buffer.cursorX to term.cols - 1, as xterm's renderer and IME do.
  • A probe that wrote abcde into a five-column terminal got cursorX === 5. rowHtml then returned abcde with no cursor span, so the folded caret and the horizontal IME alignment disappear at the right margin.
  • A unit case for this pending-wrap position would pin it.

Only this change will be re-reviewed.

…sor waits to wrap

A write that fills a row leaves xterm's cursor one past the last column until the next
character wraps it; xterm draws it and stands its input on the last cell. The fold now
does the same, so the caret and the preedit stay at the right margin.
@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Re-review follow-up in fed0c50: the cursor at the right margin.

  • rowHtml and the alignment now clamp the cursor column to cols - 1, as xterm's renderer and IME do. A row a write filled leaves cursorX === cols until the next character wraps it. The folded caret now stands on the last cell, and the input aligns to it.
  • The unit case rowHtml(... "abcde", 5 cols, cursor x 5) now expects abcd<span class="terminal-fold-cursor" …>e</span>. It fails on 66aada6 (no cursor span) and passes here.
  • Checks on fed0c50: scripts/mirror-fold-demo-regression.ts passes, typecheck is clean, and bun run check fast passes (unit 7177 pass, 0 fail).

@devswha

devswha commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review report. Overall: PASSED. This is an ordinary client change in the terminal attach path. Reviewers: the Astra gate (omo-native-gate-reviewer, gpt-6-astra), Codex Sol (gpt-6.1-sol, xhigh) and my own QA.

Astra gate (a091b37): APPROVE.

  • The bridge traps hold. An attached pane never uses pane.read, scrollback stays 0, and folding causes no resize, take-over or herdr focus change. The folded rows come only from the xterm buffer that the attach stream wrote.
  • foreign() folds only a mirror, a watched grid, or a grid held under the soft keyboard. It never folds a split's following half, or a pane this device drives at its own width. Both mutations were caught: mirror-only failed line 552, and dropping keyboard-held eligibility failed line 646.

Codex findings and how each was resolved:

  • Caret and IME (MAJOR, a091b37): an editable attached fold had no drawn caret, and the IME/preedit anchored to the bottom of the whole wrapped row. Fixed in 66aada6: the cursor cell is mapped into the folded text, and the caret and IME anchor there, including a caret in an earlier wrapped segment. A browser regression case fails without the fix.
  • Swipe reports (MINOR, a091b37): queued touchPacer history reports outlived a swipe that panning claimed. Fixed in 66aada6; they are cancelled.
  • Right-margin caret (MINOR, 66aada6): at the right margin (pending wrap), cursorX === cols lost the folded caret and the IME's horizontal alignment. Fixed in fed0c50, which clamps the column to cols - 1 in rowHtml and in the IME anchor. The unit case abcde in 5 columns draws the caret on the last cell.

Tests:

  • terminalFold.test.ts and settings.test.ts pass, and typecheck is clean.
  • scripts/mirror-fold-demo-regression.ts passes locally. It covers attached panes folded while watched or held under the keyboard at another device's wider grid, finger and wheel panning, and the caret and preedit in an earlier wrapped segment.

Known gap (predates this PR, from the Astra gate): Original width under a driving phone's held keyboard still has no local horizontal panning.

This PR shares files only with outside PRs #1639 and #1911, which already conflict with main. git merge-tree against main is clean.

@devswha
devswha merged commit 7b2422e into main Oct 26, 2026
11 checks passed
@devswha
devswha deleted the feat/wrap-view-attach branch October 26, 2026 08:00
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.

1 participant