Proposal: make an agent turn readable at a glance on a phone - #67
Open
jankarres wants to merge 27 commits into
Open
Proposal: make an agent turn readable at a glance on a phone#67jankarres wants to merge 27 commits into
jankarres wants to merge 27 commits into
Conversation
* feat: show CLI version and update status * feat: add shift to terminal keyboard toolbar & update terminal input handling * feat: add Reach Out page and integrate with About and More tabs * feat: enhance in-app browser's new tab page Kevin (a user) said that he didn't "feel" that he opened a browser when he went to our in-app browser. * feat: enhance EmptyState with description across multiple tabs * feat: simplify bookmarked sessions state management * feat: add isSettledSessionStatus function & FIX permission handling in ChatConversationPage * chore: v0.0.29
v0.0.33: dev to main
v0.0.34: Ajit/login (shellular-org#13)
v0.0.35: fix build
v0.0.36[to main]: multi-region
v0.0.38: Merge pull request shellular-org#47 from shellular-org/biraj/session-msgs-reverse-…
fix(v0.0.39): create chat sessions lazily on first message (shellular-org#50)
* Biraj/fixes (shellular-org#52) * fix(chat): preserve draft config picks and pass them at session create A draft chat has no ACP session, so mode/model picks made before the first send were kept in local state and replayed afterwards. Two gaps made them get lost or apply late: - The draft effect reset configOptions from the host-cached agent config on every run, clobbering picks the user had already made. Skip the reset while pending changes exist. - Agent metadata is only loaded on connection, so a draft opened after another chat populated the CLI's config cache rendered a stale toolbar. Refresh agents once per draft. Also send configOptions with AI_SESSION_CREATE so the first turn starts with the chosen config instead of the agent default, and bump @shellular/protocol to 0.0.31 for the field. * fix: re-use ticket on re-connects for speed * fix: new chat prompt should not get cleared on reconnect * fix(chat): stop selection jumping and copy burying the answer Two issues reported on Android with a long OpenCode response: Selecting text and dragging down jumped the selection to the top of the conversation. A selection drag is auto-scrolled by the WebView, which fires `scroll` but not `wheel`/`touchmove` — the events wired to scroll-intent — so the top sentinel came into view, prepended ~30 older messages, and corrected scrollTop out from under the drag. Suppress load-more, auto-scroll, and stick-to-bottom while a selection is active in the transcript. Copying a response also copied the reasoning and every tool call with its full JSON input/output, burying the actual answer. The message copy button now emits only the answer; each folded section (reasoning, tool call, tool-call group, command) carries its own copy button, so nothing became unreachable. * feat: Add git tree view for changes (shellular-org#46) * feat: git tree view for changes * chore: update diffs & increment version --------- Co-authored-by: Biraj <biraj.pub@gmail.com> --------- Co-authored-by: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
v0.0.41: prompt queue & check git changes from chat page
Four pure modules behind the work log, with no visual change yet: type-aware elision that keeps a basename and a command head, a per-agent row model that reads the caller's own `description` instead of the raw payload, an output summary sized to the output, and the fold/window/commentary/count layout. `deriveToolActivityPresentation` and its private helpers are replaced by `deriveActivityRow`, whose two existing cases are ported first so the swap is provably not a regression.
The row grammar on screen: a glyph, a verb, the object, a status, and the step's own outcome under it. Short output is the row's second line, longer output a faded peek, and a failed row opens with the whole thing. The work log now shows the full trail with a window control instead of the latest call only, folds runs of file reads into one row with basename chips, and merges consecutive touches of the same file. Command runs are never folded: each carries its own sentence. The old latest-only view survives as a window of one, behind a new Agent Work Log setting.
The live turn now names what the agent is doing, for how long, and where it is going: a state word, the elapsed time, and the turn's latest commentary promoted out of the rail. Four states, so waiting for a permission reads differently from waiting for an answer. This retires `streamingStatusLabel`, which pasted a tool title straight into a sentence, and which had become unreachable anyway: it was non-empty only when a tool call existed, and the line it fed rendered only when none did. The timer moves here from the work log and gains its WCAG SC 2.2.2 branch: under prefers-reduced-motion it refreshes once a minute with a coarse label instead of ticking once a second.
The fold header gains counts by kind and, when there are failures, their count: `Worked 1m 12s . 2 ran . 3 read . 1 failed`. A duration alone says how long a turn took and nothing about what it was. Under the answer, a changed-files block lists the files the turn touched with a `+n -m` badge. Nothing in it is new data: these are the same file_change parts the work log already carries, lifted out because "which files did it touch" is the question a phone reader asks first and the transcript answered last.
A permission card now renders the command it is asking about directly, in a block that wraps in full with no height cap: truncating a string that a tap will execute is a safety defect, not a layout choice. When the agent gave no reason, the command is no longer printed twice. Status gains the waiting glyph the set was missing, so a call blocked on a permission stops claiming the machine is busy, and an unrecognised terminal status resolves to failure rather than to a neutral glyph that reads as "fine". Every state carries a shape and an accessible name, not only a colour, and the transcript gets its log role.
An appending, auto-scrolling transcript is auto-updating content under WCAG SC 2.2.2 at Level A, which asks for a mechanism to pause it. Scrolling up is that mechanism, so this adds the way back: a labelled Jump to latest pill, 44px tall, that appears only while a turn streams and the view is scrolled away from the end. Beside it, a rule marks where reading stopped when a turn advanced while the app was backgrounded. The marker is a stored timestamp per chat, advanced only on leaving the chat, never on scroll position: scrolling past twenty dense rows is scanning, not reading. The rule carries its count as real text rather than through role="separator" plus aria-label. That role makes every descendant presentational, which would drop the one thing the rule is there to say.
At 390px the three session settings collapsed to single letters, because a max-width squeezed them while four controls shared one row. The context meter becomes a 21px ring, which costs one slot instead of a bar plus a figure, and each setting carries the category glyph the app already ships: a glyph for the kind, the text for the value, which is the transcript's own rule one surface down. Three bare words told apart only by colour is what WCAG SC 1.4.1 forbids everywhere else here. Tapping the ring drops the figure onto its own line, and tapping the figure opens the context sheet, so nothing is lost, it just does not spend width until it is asked for. The bar loses its filled boxes: the controls are 40px of ink with the hit area extended to 44px by an inset pseudo-element, so the row can be lighter without being harder to hit. Send and stop keep their fill; they are the primary action in that slot.
A path elided to 44 characters is 317px of mono, and the narrowest column it renders in, the settled fold's rail at 390px, is 281px. CSS then truncated it a second time at the tail, which is exactly where the basename is, so a row lost the one part that tells it apart from its neighbours. Measured on the real components rather than assumed.
…ping Four corrections from a design review of the screenshots. The away rule and the jump pill were two controls fighting for one corner, which is the collision Discord shipped twice in two months. They are now one: the rule that says what is new is also the way back to it, centred on the line. The floating pill survives only for the case with no rule, and is smaller, so the two can never appear together. The changed-files counts drifted into the middle of the row, because the shared card gives its chevron `margin-left: auto` and the stat claimed it too, so the free space was split between them. Only the stat pushes now, and `+41` and `-12` line up in one right-hand column. The three session settings lose their per-category colours. With a glyph carrying the category the colour told nothing apart, it only made the row loud. The context figure becomes a popover instead of a line under the row. A line grew the composer by 20px and pushed every control up from under the thumb that had just tapped one; measured, the popover leaves it at 132px in both states. A readout must not move what it was read from.
Three corrections from a second design review. **The away rule could never appear.** It compared a stored timestamp against `part.timestamp`, and the wire carries no timestamp per part — only `AcpMessage` has one, and a turn already running when you left keeps its original stamp however many steps it adds while you are away. The count was therefore always zero and the rule was dead code. The marker now stores which turn was on screen and how many work steps it had, and the count is what that turn gained since; a different turn means all of it is new. That is the only measurement the available data supports, and it is the one the reader means. **The send glyph read as low and left.** Its bounding box is a centred square, but the paper plane's mass sits toward the upper right: traced on a canvas, the ink centroid is 7.3% right and 8.1% above the box centre. It is nudged by that amount, in em, and the circle drops from 40px to 36px inside the same 44px target. **`role="log"` gets an explicit `aria-live="off"`.** The role is for structure; its implicit politeness would read a twenty-five-row work log aloud a row at a time. The turn header is a status region and announces the state change once, which is the part worth hearing.
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.
This is an opinionated proposal for the chat view, and a proposal is really all it is: I have not thought every element through to the end, and some of it will touch parts of the app I have not looked at closely. I am mostly putting it up to ask whether there is any interest in this direction at all. A no is a perfectly good answer, and I would rather hear it now than keep polishing something nobody wants.
It comes out of how Claude Code presents itself in Visual Studio Code. I live in that view all day, and following the same turn on my phone is much harder, which is where this started.
Interactive before and after mockup: mockup.html
Why
The row is the thing a chat produces most: one per tool call, and dozens of them in a single conversation. It is the element I spend the most time looking at, and it is the one that tells me least.
Two shell calls, as the app renders them today and as this proposes:
On the left, both rows say
Ran commandand then the start of the shell command, which on a phone is the part every call has in common: thecdinto the project. The two rows are identical up to the point where they are cut, so nothing on screen tells them apart, and nothing says what either call was for or what came back. Both rows are only visible at all because I expanded them; by default the second one sits behind+1 previous tool call.On the right the same two calls say what they were for and what they produced, without expanding anything. The command is still there, one tap away.
The change that does most of that work is one lookup. Every shell call already arrives with a short description of what it is for,
"Check branch state", and the row renders the raw command instead. It was set on all 147 shell calls in the transcripts I went through, so it is a field that is there and unused rather than one that is occasionally available.It gets worse when an agent works through several files in one directory, which it does constantly. A row is cut at its end, and for a path the end is the filename, so the one part that tells the rows apart is the part that goes:
Four reads, four rows, one string repeated. This is where I started: not with a feature, but with the feeling of scrolling past a wall of rows that all look the same and tell me nothing. Where rows really are interchangeable they now become one, with the filenames kept as chips, which also gives back three rows of height.
At the scale of a whole turn it compounds, because the rows that look alike are also the rows that get folded away:
Same turn, same data, both at phone width. On the left, four file reads and five shell calls collapse into two rows plus
+5 previous tool callsand+2 previous tool calls; the paths are cut at the end, so the filenames are gone; and a step that failed is hidden inside a fold. On the right the same turn reads as a list of sentences, each with its outcome, and the failure is visible without opening anything.What changes
A finished turn says what it did. Counts by kind next to the duration, and the files it touched underneath the answer, both built from things the transcript already carried.
A permission shows the command once, in full. Today it appears twice, as the heading and again below it, in a block that caps its own height, so half the card is a duplicate and the reply buttons get pushed down.
Coming back to a turn that ran without you. Much like the unread line in a Slack channel, a rule marks where you stopped reading. On a phone that earns its place more than on a desktop, because you put the phone down mid-turn and come back to twenty new steps. Here the rule is also the control that takes you to the newest work, and it is anchored to what was on screen when you left rather than to how far you scrolled.
The composer. The three session settings share one row and are told apart only by their colour, and when the values run long they collapse to single letters. Each now carries a small glyph for its category, so the mode and the model are distinguishable without decoding a colour, and the context meter becomes a ring you can tap for the figure.
Long values still get cut, though: three settings and three buttons do not fit one line at this width. Fitting all of them needs a second line, which would be a proposal of its own.
What it touches
src/pages/chat/only. No protocol change, no host version, no new theme token, no new dependency, no new copy to translate. It reads fields the host already sends, so it works against every CLI in the field.It comes with 103 new tests. The decisions about what a row should say now sit in small pure functions with tests beside them rather than inside the components, which is the split this repository already uses elsewhere. That refactor also turned up dead code: the line meant to say what the agent is doing could never render, so a running turn only ever said
Working for 34s.Decisions you may want to overrule
Four places where I picked one option. The alternative is named in each.
Feedback
I would be glad of any. If the direction appeals, I am happy to keep working on it inside this pull request, particularly the last details and the places where these elements meet other parts of the app, which is where my thinking is thinnest. And if you would rather take the idea and shape it yourself, please do!