Skip to content

Proposal: make an agent turn readable at a glance on a phone - #67

Open
jankarres wants to merge 27 commits into
shellular-org:devfrom
jankarres:jan/chat-transcript-readability
Open

Proposal: make an agent turn readable at a glance on a phone#67
jankarres wants to merge 27 commits into
shellular-org:devfrom
jankarres:jan/chat-transcript-readability

Conversation

@jankarres

Copy link
Copy Markdown

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:

Today Proposed
row-before row-after

On the left, both rows say Ran command and then the start of the shell command, which on a phone is the part every call has in common: the cd into 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:

Today Proposed
repetition-before repetition-after

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:

Today Proposed
live-turn-before live-turn-after

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 calls and +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.

Today Proposed
settled-turn-before settled-turn-after

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.

Today Proposed
permission-before permission-after

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.

away-after

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.

Today Proposed
composer-before composer-after

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.

  • How much of a turn is on screen. Today a turn shows only its most recent action, with the earlier ones behind a counter. This shows all of them, oldest behind a single control, because not being able to see what the agent just did is the complaint this started from. Latest-only stays available as a setting rather than being removed.
  • Which repeated rows collapse. A run of file reads differs only in a filename, so it becomes one row with the names as chips. A run of shell calls does not collapse, because each carries its own description and folding a dozen would replace a dozen sentences with a number. The alternative is one rule for both, which either throws away the descriptions or leaves a wall of near-identical file rows.
  • Whether the transcript is read aloud as it grows. It is marked up as a log but announces nothing, because a turn can append twenty-five rows and hearing each one is unusable; the turn header speaks once, when the state changes. The alternative is the automatic announcement a log role normally brings.
  • Where the rule for missed steps sits. It goes above the whole turn rather than between the exact two steps, because the new steps arrive inside a single message and placing it precisely would mean rendering that message's work log in two halves. The alternative is exact placement at the cost of that split.

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!

biraj21 and others added 27 commits June 15, 2026 19:11
* 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.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.
@biraj21
biraj21 changed the base branch from main to dev August 20, 2026 18:08
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.

2 participants