Skip to content

connect: row-based chat scrolling, nested tool calls, and a flat startup log - #84

Merged
hbrooks merged 1 commit into
mainfrom
chat-window-scrolling
Jul 26, 2026
Merged

hbrooks merged 1 commit into
mainfrom
chat-window-scrolling

Conversation

@hbrooks

@hbrooks hbrooks commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Reworks the agent session connect chat window: scrolling, how tool calls sit in the conversation, and the sandbox startup block.

Scrolling is row-granular. The viewport sliced by whole transcript entries using estimated heights, so a message taller than the window could never be read — it appeared whole or not at all, and one scroll notch threw all of it away. Those estimates also had to leave slack for their own rounding errors, which showed up as dead space and a phantom … 1 newer (scroll or ↓) marker. Everything on screen (startup block, messages, tool chatter, in-flight sends, the live streaming tail) is now flattened to exact one-line screen rows in a new src/ui/transcriptRows.ts, and the window is a row range over that list.

  • brings a message to the top of the frame; a message too tall to fit scrolls row-by-row through it before the highlight moves on.
  • Added pageUp/pageDown; the wheel moves 3 rows instead of a whole entry.
  • Density: dropped the blank tinted pad row above and below every message, TOP_PAD 4→2, composer interior 3→1 (it grows as you type). ~5 more content lines on a 24-row terminal.

Tool calls nest under their parent message instead of standing as their own turns:

 ◆ Run the tests and fix whatever breaks.

 ● I'll run the test suite first to see where we stand.
  ⎿ Ran 1 shell command

 ● Three failures in the parser. Let me read that file.
  ⎿ Ran 2 tool calls (Read, Edit)

Expanded (ctrl+r or ) each line keeps the glyph that says what it is — ● Bash(pytest -q) then ⎿ 3 failed, 41 passed — since the indent already conveys nesting. The live ⏺ Running Bash(…)… line nests in exactly the spot its result will land, so nothing jumps when it arrives.

The chat is an event log. Session milestones land in the transcript in feed order, so a nap between turns stops being an unexplained gap:

✦ Session asleep — your next message wakes it

 ◆ Great, now add a test for the empty case.

✦ Waking the session…
✦ Session awake — picking up where it left off

Deliberately a short list (asleep, waking, awake, retry, cancelled) — provisioning detail stays in the startup block rather than burying the chat.

The startup block is one flat log. It was a three-level tree (session → sandbox → phases → each phase's own log tail) drilled into with , which hid build output three keystrokes deep and split one chronological story across separate per-phase tails. Now a headline plus the last 10 lines of everything that happened, output inline:

  ✦ Connected to ellipsis.dev
 ⏺ Session starting…                                          (0s)
     … +13 earlier lines
       #7 [5/6] COPY . .
       #9 writing image sha256:9f2c1b
     ✓ Starting container (8s)
     ✓ Fetching repositories (3.1s)
       HEAD is now at bc67efc
     ⏺ Post-clone setup…
       $ bun install

Four pieces of drill-down state and the whole modal step-cursor key mode are gone; / show and hide the log. sandboxStepLine, SandboxStep, RUNNING_TAIL_LINES and FINISHED_LOG_LINES are deleted.

Live marks pulse. One shared 700ms timer, so every on screen breathes in step. The blink is a colour swap (bone → grey), not Ink's dimColor — that emits \x1b[2m, which several terminals drop when a 24-bit foreground is also set, so the blink would have silently done nothing there. The timer only runs while something is in flight.

Overflow bugs fixed along the way

Both were the historic failure mode of this file, where a frame taller than the pane scrolls Ink's render region and smears stale rows:

  • rowViewport could let the two hidden-count markers exceed a small budget. Now guaranteed never to, with a brute-force test over ~26k input combinations.
  • The footer counted a long notice and a typed paragraph as one row each, though both wrap. Now measured at real wrapped height, with the padding, composer and notice yielding in priority order so the frame fits from 4 rows up.

Test plan

  • npx tsc --noEmit clean
  • npx vitest run — 375 passing, with new coverage for rowViewport, itemRows, scroll anchors, snapToEntry, layOutItems, sessionLogText, lastLines and fitLines
  • Rendered via an offline Ink harness: rows exactly match pane height at heights 4–60 and widths 44–100, no over-wide lines
  • Exercised collapsed / expanded / sleep-wake / in-flight / failed-phase views, terminal resize, streamed appends (follows at the bottom, holds position when scrolled up), fold expansion, ctrl+r, markdown tables and code blocks
  • Not driven against a live session — the backend returned 503 during this work, so verification was via the render harness. Worth a manual pass on a real session before merge, particularly the sleep/wake log lines and the pulsing mark in your own terminal.

…p log

The transcript viewport sliced by whole entries using estimated heights, so a
message taller than the window could never be read: it appeared whole or not at
all, and one scroll notch threw all of it away. The estimates also left dead
space and phantom "N newer" markers. Everything on screen is now flattened to
exact one-line screen rows and the window is a row range over that list, so up
arrow brings a message to the top of the frame and reads down through it. Also
drops the per-message padding rows to fit more of the conversation on screen.

Tool calls no longer stand as their own turns: a call, its result and the
collapsed fold nest under the message that produced them.

Session milestones (asleep, waking, awake, cancelled) now land in the chat, so a
nap between turns stops being an unexplained gap.

The startup block's three-level phase tree becomes one flat ten-line log with
build and setup output inline, opened with a single arrow instead of three. Live
lines carry a pulsing mark.
@hbrooks
hbrooks merged commit 12de533 into main Jul 26, 2026
1 check passed
@hbrooks
hbrooks deleted the chat-window-scrolling branch July 26, 2026 22:57
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