eleven / ocho Public

web: message box scrolls at max height + no feed jump as it grows (#201)

The composer textarea had `overflow: hidden`, so once autogrow capped it at
max-height the earlier lines became unreachable (no wheel/drag scroll). Switch
to `overflow-y: auto` (overflow-x stays hidden — text wraps) so a long draft
scrolls its own overflow.

As the box grows it shrinks the flex feed; the existing ResizeObserver re-pins
to the bottom, but only on a later frame, so WebKit paints one frame stranded a
line short of the bottom — the visible "jump" in the Mac app (WKWebView). Re-pin
synchronously in the input handler, in the same layout pass, guarded by
feedAtBottom so a reader who scrolled up isn't yanked down.

Verified in Playwright's WebKit engine (same as WKWebView): the box becomes
wheel-scrollable at max height, and typing produces zero painted frames off the
bottom.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell 1633e749f85cc9865c759607407996c0a25697db parent f9829e2
3 files changed, +12 −1
  • changelog/2026-07-05-composer-scroll.md +1 −0
  • web/static/apps/chat.js +6 −0
  • web/static/style.css +5 −1
diff --git a/changelog/2026-07-05-composer-scroll.md b/changelog/2026-07-05-composer-scroll.md
new file mode 100644
index 0000000..855abf8
--- /dev/null
+++ b/changelog/2026-07-05-composer-scroll.md
@@ -0,0 +1 @@
+The message box now scrolls once a long draft grows past its maximum height, instead of hiding the earlier lines — and typing a multi-line message no longer nudges the conversation up a notch as the box grows.
diff --git a/web/static/apps/chat.js b/web/static/apps/chat.js
index 5721a6b..f0214a9 100644
--- a/web/static/apps/chat.js
+++ b/web/static/apps/chat.js
@@ -2610,6 +2610,12 @@ function buildSkeleton(el, ctx) {
input.addEventListener("input", () => {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 140) + "px";
+ // The composer just grew, shrinking the flex feed. Re-pin to the newest
+ // message *here*, synchronously in the same layout — the ResizeObserver below
+ // also re-pins, but only on a later frame, which leaves one painted frame
+ // stranded a line short of the bottom (the visible "jump" on WebKit / the Mac
+ // app). Guard on feedAtBottom so typing never yanks a reader who scrolled up.
+ if (feedAtBottom) feed.scrollTop = feed.scrollHeight;
refreshSend();
updateTypeahead();
// A "/command" in progress is private — never announce typing for it (and
diff --git a/web/static/style.css b/web/static/style.css
index 0e73d69..eb04291 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -1182,7 +1182,11 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr
min-height: 40px;
border-radius: 20px;
font-family: inherit;
- overflow: hidden;
+ /* Autogrow caps at max-height (see the JS); past that the field must scroll its
+ own overflow rather than swallow the earlier lines — so overflow-y is auto,
+ not hidden. overflow-x stays hidden since the text wraps. */
+ overflow-x: hidden;
+ overflow-y: auto;
/* Light: lift the field a touch above the panel. Dark: no fill — mixing the
dark panel toward white read as a bright grey slab, so blend into the band. */
background: light-dark(color-mix(in srgb, var(--panel) 50%, white 50%), transparent);