UI overhaul: session previews, group spawning, and a Linear-ward design pass - #8
Merged
Conversation
…gn pass The app had grown five screens that each spelled their own version of the same thing, on top of shadcn defaults that were never chosen. This is one pass over all of it, plus the two features the sessions list was missing. **Previews.** A new `peek` control message returns the tail of a session's scrollback without attaching to it — no ref, no stream, no subscriber, and crucially no touch of `lastActive`, since the default ordering is by that stamp and a preview that counted as activity would sort the row under the pointer to the top of the list beneath it. The web side renders those raw bytes through a small VT interpreter (`sessions/preview.ts`) rather than stripping escapes: a strip-the-escapes reader shows a redrawing TUI as a dozen stacked copies of itself. The card hangs off each row on hover, opens after a rest so a pointer crossing twenty rows costs nothing, and refreshes while it is open. **Group spawning.** Every heading names a property its rows share, so the `+` on it makes one of *those*: on a machine's heading it spawns there, on a tag's it applies the tag once the session exists, on a directory's it spawns in that directory. `spawnFromGroup` is a pure function so the rule is arguable in a unit test, and it is what decides that "Exited" has nothing to offer. **Daemon restarts.** A revived session's seam now names the command that picks the interrupted Claude Code conversation back up, when there is evidence of one — a transcript in the project directory written during the session's own life. Printed, never run: the id is a heuristic and typing into somebody's shell on their behalf is a different kind of act. **Design.** 13px controls on a token, 8px radius (6px for buttons), an elevation scale that makes overlays read as over rather than pasted on, and focus as an offset outline instead of a 3px translucent halo. The Devices table became rows in the sessions list's own language; Remote access became one state panel and a named Integrations section; Settings became a list of what is planned rather than a promise that something will arrive. **README** is now short and points at flue.sh and docs/ for the rest. Two bugs found and fixed in this work: the group `+` rendered without an accessible name for groups that refuse one (a button a pointer could press and a screen reader could not announce), and the preview card refetched on every fleet poll because the row object's identity changed — a card that blinked while you looked at it.
TestPeekDoesNotCountAsActivityInTheSession took its baseline the moment the echo became findable in the ring, which can be one chunk of several. Every chunk read back from the pty moves LastActive, so the pump finishing its delivery a millisecond later looked exactly like a peek that had touched the stamp — a race that never fired locally and fired on CI's first run. The property is unchanged and worth keeping; the measurement now waits for the session to actually go quiet before it starts. 40 runs under -race.
The mobile pass I owed and had not done. Three real faults, all on Sessions — the other screens carry one small action each and were already fine. **The header overlapped itself.** `justify-between` on a row that cannot wrap does not push overflowing actions off the end; it prints them through the heading. At 390px the search field, the display menu and the split button ran straight over the word "Sessions" and then off the right edge of the viewport. The row now wraps, and the actions take a line of their own with the search growing into it — a toolbar rather than a pile. From `sm` up nothing moves. **The preview card had no business on a touch screen.** A finger held on a row long enough to count as a hover opened a card over the rows beneath it, and the tap that followed then fought the card instead of opening the session — at the cost of a round trip per row. `(pointer: coarse)` stands the whole thing down: the question is about the pointer, not the screen, so a narrow window on a laptop keeps its previews and a large tablet does not get them. **The machine chip ate the name.** On a browser paired with one machine it is the same word repeated down the screen; on a phone it and the timestamp took half the row, truncating a session to "karn@karn:…" with the useful half gone. A fleet of one drops it — keyed on how many machines this browser holds, not on how many are answering right now, so a laptop going to sleep does not make a column come and go. The preference is untouched: pair a second machine and the chip is back, still ticked where it never stopped being.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five screens had each grown their own spelling of the same thing, on top of shadcn defaults nobody chose. This is one pass over all of it, plus the two things the sessions list was missing.
Session previews
Hovering a row shows the last fourteen lines it drew.
A new
peekcontrol message returns the tail of a session's scrollback without attaching to it — no ref, no stream, no subscriber left behind, and no touch oflastActive. That last one matters more than it sounds: the default ordering is by that stamp, so a preview that counted as activity would sort the row under your pointer to the top of the list beneath it.The bytes come back raw, escape sequences and all, because rendering is the client's job and the client already owns an emulator. It does not use that emulator — a canvas and a WebGL context per row is not what a hover card is for. Instead
web/src/sessions/preview.tsis a small VT interpreter: a grid, a cursor, and the sequences that decide where text lands. That is the difference between a preview and a mess — a strip-the-escapes reader renders a redrawing TUI as a dozen stacked copies of itself.Nothing is asked for until the pointer has rested (400ms), so crossing twenty rows costs twenty nothings.
peekis the one request on the client with a deadline of its own, because a daemon older than the page answers it with an uncorrelatederror{bad_message}that reaches no asker.Group spawning
Every heading names a property its rows share, so the
+on it makes one of those:+doesspawnFromGroupis a pure function, so the rule is arguable in a unit test rather than by clicking headings.Daemon restarts and Claude Code
A revived session's seam now names the command that picks the interrupted conversation back up:
Only when there is evidence of one — a transcript in that project's directory written during the session's own life, which is the whole filter and the reason this is safe to print. Printed, never run: the id is a heuristic, and typing into somebody's shell on their behalf is a different kind of act from restoring what they were looking at.
The design pass
Four decisions, each visible on every screen:
shadow-low/medium/highplus an inset lip on the one filled button. Overlays used to be bordered with the same hairline the list is ruled with, so they read as pasted on rather than over.Screen by screen:
Emptyblocks, twoCards), which read as four screens sharing a heading. It is now one state panel plus a named Integrations section — the shape has to survive a second integration.README
Short, and honest about what flue is — a session manager for every machine you own, not a terminal in a browser tab. The dev-loop prose moved to
docs/DEVELOPMENT.md, which already covered it.Two bugs found in this work
+rendered without an accessible name for groups that refuse one: a button a pointer could press and a screen reader could not announce. The test that "covered" it queried by name, so it passed while the button sat there. It now counts.Verified
make test— Go, 952 web tests, 90 relay tests.make lintclean. Driven end to end in a real browser against a real daemon: previews render live terminal output, both themes, every screen.🤖 Generated with Claude Code