Draw a model's thinking, and what a reply cost to produce

A llama.cpp session's `reasoning_content` becomes `Event::Thinking` deltas
closed by an `Event::ThinkingDone` carrying the span the driver measured, and
the phone draws it as a card of its own: "Thinking" with the spinner a running
command has, then "Thought for 12.4s". Deliberately not a tool call, so a run
of calls cannot collapse the reasoning into "Called 6 tools"; the reasoning is
also kept out of the next prompt, which `conversation` already ignored.

`UsageDelta` gains `tokensPerSecond`, the provider's own figure or nothing --
llama.cpp reports `timings.predicted_per_second` and the coding CLIs report no
such thing -- and a finished reply carries a small line under it saying when it
was sent and, where there is one, how fast it came out: "3:00 PM · 149 tok/s".

The compact usage bar drops the provider's name for the window and puts its
length after the time left instead: "42% · 3h 20m left / 5h".

Three things that had to come with it: the transcript coalesces runs of
thinking deltas as it does reply deltas, so one block is one row of a page
rather than a page of its own; `joinPages` welds a block cut by a page boundary
(`healSplitThinking`), since the half with no ending spun for ever; and
`UsageDelta` now reaches the fold, which is what carries the rate to the reply.

Verified on the emulator against a real Qwen3-0.6B session and the echo rig's
new `/think [seconds]`: the spinner while it runs, "Thought for 1.4s" and
"2:54 PM · 149 tok/s" after, the reasoning on tapping the card, and the usage
bar reading "42% · 3h 19m left / 5h".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 15:07:25 -04:00
1 parent 45f249ae91
commit bb5ac1a242
18 files changed
+900 -101

No files matched your search

+43 -3
View File
@@ -135,6 +135,19 @@ seq N", so there is no separate history path to drift from the live one.
`Patch { diff }`. Patch success boilerplate is omitted and failures remain
as output. This normalization belongs in the drivers, before persistence;
the phone never decodes a provider's tool schema.
- `Thinking { delta }` / `ThinkingDone { ms }` (2026-09-19) — the model's
working, streamed the way its reply is, and its own kind because it is not
what the session *said*: the phone draws it as a card of its own, shut, and
no driver folds it back into the next prompt. Only a provider that actually
streams its reasoning sends it — llama.cpp does, as `reasoning_content`;
nothing is inferred for one that does not, since a card that appeared
whenever a turn was slow would be a guess wearing a measurement's clothes.
The duration is **measured by the driver**, because a reader only knows when
an event arrived: the last fragment of a block followed by a slow tool call
is indistinguishable from thinking that went on that long. A block with no
`ThinkingDone` is one still being thought, which is what the card's spinner
says; one closed by the turn ending without a duration says "Thought" and
names no span rather than inventing one.
- `Image { ref }` — saved under the session dir, fetched by URL.
- `Question { id, prompt, options }` — anything needing a human. Claude's
AskUserQuestion and permission requests (canUseTool) are the same shape;
@@ -149,8 +162,13 @@ seq N", so there is no separate history path to drift from the live one.
waiting for itself and will speak again with nobody having typed anything.
Reporting it as idle sent a "finished" notification at the one moment that
was untrue.
- `UsageDelta { tokens, context }` — what a turn cost and how much the model
was holding when it ended. `context` is prompt plus both cache figures,
- `UsageDelta { tokens, context, tokensPerSecond }` — what a turn cost, how
much the model was holding when it ended, and how fast it was generated.
`tokensPerSecond` (2026-09-19) is the provider's own measurement or nothing:
llama.cpp reports `timings.predicted_per_second`, and the coding CLIs report
no such figure, so dividing what this server watched a reply arrive over
would count the network, the tool calls and the reader's own permission
answers as generation. The phone draws it under the reply it measured. `context` is prompt plus both cache figures,
taken from the **last assistant message** rather than the turn's `result`:
measured 2026-08-30 against CLI 2.1.237, the result adds a turn's messages
up, so its cache read of 40,211 was the same conversation counted twice.
@@ -779,6 +797,11 @@ nothing at all for it — not a zero, and not "unknown".
The session's usage dialog applies the same machine-and-provider match and
shows every billing pool for that provider; it does not turn opening one
session into a comparison with the other providers on that machine.
The compact bar names no window (2026-09-19): the provider's own name for it
("5-hour window") became a denominator after the span instead — "42% · 3h 20m
left / 5h" — which says in one reading both how much of the cycle is to come
and which cycle it is. Where the provider reported no duration there is
nothing after the span, since the name it gave is not a measurement of one.
For a provider with several pools, the compact bar selects the pool named by
the session's model (including Luna's `gpt-reserve` name), falling back to the
provider's generic pool, and shows the shortest cycle that pool actually
@@ -1237,8 +1260,25 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
decision (`groupToolRuns`) and a cut run's pieces are keyed there — the
first piece keeps the run's name, since that name is what survives a
page of history landing in front of it.
- **The model's working is a card of its own** (2026-09-19) — "Thinking"
with the same spinner a running command has while it goes, and "Thought
for 12.4s" once it is over. Deliberately not a tool call, because a run
of tool calls collapses into "Called 6 tools" and the reasoning would be
filed as one of them; it therefore also breaks a run, which is right —
the model stopped to think in the middle of it. A block cut by a page
boundary is welded like a reply is (`healSplitThinking`), since the half
with no ending would otherwise spin for the rest of the conversation.
- **A finished reply carries a line under it saying when it was sent and,
where the provider measured one, how fast it was generated** (2026-09-19)
— "3:00 PM · 149 tok/s", small and set back, right-aligned because it
closes the message rather than opening one. The time is the transcript's
own timestamp, so every device draws the same one; the rate is the
provider's own figure or nothing at all. It is a list unit of its own
(`ReplyFoot`), because a settled reply *is* its blocks and there is no
row left to hang it on.
- **Anything that is a note *about* the conversation rather than a turn in
it is closed by default** — a tool call, a peer message, a memory note.
it is closed by default** — a tool call, a peer message, a memory note,
a thinking block.
Open-ness is the screen's, never the card's: a card that remembered for
itself forgets the moment the lazy list stops composing it, so a note
opened and scrolled past would shut behind the reader.