6680fbb987bcf43a2f43191a8e5e812875f5c9b5
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6680fbb987 |
Clear from the phone, and put the numbers where they are read
Four changes to the session screen, three of them Bryan's and one that
fell out of them.
**/clear is offered like any other session command.** It joins
SESSION_COMMANDS, so it suggests itself while being typed and goes out
through the command endpoint that /compact already uses -- no new path,
and the boundary pump holds it mid-turn exactly as it holds a compaction.
**A clear draws a divider, not a deletion.** `Event::Cleared` becomes a
ClearedNote row saying that everything above stays here and is no longer
sent. That sentence is the row's whole job: the reader can see the
conversation is still on screen, so without it the divider reads as
something having been thrown away, which is the one thing it is not. It
carries no counts, because nothing was measured -- a compaction's
numbers are real and there is no equivalent here to report.
Compaction and clear now share `TranscriptDivider`. They are the same
kind of mark to somebody scrolling back -- "the session no longer has
what is above this" -- and the difference belongs in the words rather
than in how they are drawn, so the styling is written once and cannot
drift.
**The five-hour usage bar sits under the session header.** It reports
the paid service's own metering for the machine this session runs on,
fetched from that machine, refreshed every minute off the backend's
cache. It is never derived from the transcript's token counts: those are
a different quantity measured differently, and a quota-shaped bar built
out of them would be a guess wearing a measurement's clothes. Not
knowing has its own appearance and its own words -- "unknown" and why --
because a bar resting at zero because a machine is unreachable reads as
plenty of headroom, which is the opposite of the truth. The window is
selected by the API's own `kind` ("session"), added to UsageWindow in
this change, rather than by matching the label a person reads.
**The token total moved from the header to the bottom right of the
transcript**, pinned above the input rather than scrolling with it. In
the header it was one item in a run of dot-separated facts about the
session and read as another of them, rather than as the running total it
is.
SessionSummary now carries the setup id, which it deliberately did not.
The stated reason was that nothing here addressed a setup and holding
both id and name invited showing the wrong one; the usage bar addresses
one, so the reason lapsed rather than being overruled, and the comment
now carries the rule that replaces it: never display it. The server has
always sent the field, so nothing changed on the wire.
ktfmt, compileDebugKotlin and lintDebug all clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
|
||
|
|
749b2db287 |
Make a command a thing the app knows, and hold it until it can run
Typing "/" now suggests what this app understands -- `/compact` and `/rename <name>` -- with a line each about what they do, and anything else beginning with a slash is passed to whatever runs the session, because a dialect's own vocabulary grows without this list. None of them are messages, and that is the substance of the change. A line written into a running turn is read by the *model*, so a command sent mid-turn either does nothing or arrives as text somebody has to puzzle over. They now wait for the turn to end. The waiting is done once for every provider, in the pump that already watches every event for the boundary, rather than in each driver where a new provider could get it wrong by leaving it out. Waiting is a state, so it is on screen: the command sits at the reader's end of the conversation in blue, with a spinner and "waiting for this turn to end", and becomes an ordinary blue row when it goes. Blue because these are about the session rather than about the task -- the same blue a compaction already used, which is now one colour with one name rather than two. Renaming from the settings screen sends exactly this, so it waits and draws the same way. The name itself is not held: it is this server's own datum, so the list and the header change at once and only telling the session waits. Echo grew the same split, which is where the bug in it showed: its commands are its messages, so running one announced a `MessageTaken` as well, and the same line drew twice -- once blue, once purple. A command owes no announcement; the manager has already recorded that it was sent. Watched rather than reasoned about: `/compact` during a 25 second turn held with its bubble up, went out when the turn ended, and the compaction that followed reported what it recovered. |
||
|
|
bebaae7a94 |
Carry a question in the event model, not in one provider's JSON
A question is now fully described by the event that reports it: the tag it was asked under, each option's label, what it means, and the sample of what picking it would produce, plus whether several may be picked at once. The app renders from that alone. It had been reading Claude Code's tool input to find the parts the event dropped -- that dialect's schema, written out a second time in Kotlin, where no other provider could reach it and where it would drift the first time the schema moved. Echo could not describe an option at all, and llama never will. Answers travel as a list for the same reason. A question that takes one answer sends a list of one rather than being a different shape, and the one place that flattens it is where the CLI is spoken to: its answers map holds a string, so several choices are joined there. That join was in the phone. Also here because it is the same rule: the permission ask reuses the question body rather than owning a second one, so Allow/Deny renders and resolves through exactly the code an AskUserQuestion does. Verified against both, since a refactor that only satisfies the case it was written for has been tried on the half that cannot fail: a two question `/ask` answered from the phone, one option and then two, and a real sonnet session's `rm -f` permission asked, allowed, and run. |
||
|
|
3eccf7e443 |
Show the model and mode the session has, not the ones it was asked for
Picking either from the phone wrote the choice straight into the session's state and then sent the request. Asking and having are different things, and the difference is not rare: `auto` is a permission mode the CLI accepts on the command line, silently resolves to `default`, and refuses outright over the control channel -- "auto mode unavailable for this model" -- so a session spawned in auto was in default and one switched to auto stayed where it was, with the phone reporting auto in both cases. So the drivers report what they are set to and the manager follows that. Measured, because the confirmations are not uniform: a model change answers success with no value, so what was asked is remembered until the answer arrives; a mode change echoes the mode it became, and that answer wins over the request; and `init` names both -- resolving `haiku` to claude-haiku-4-5-20251001 -- which also covers a session adopted from a terminal that set them outside this app. A driver that cannot change either already says so with an error, and now that error is the whole story rather than a note beside a display that changed anyway. The config keeps the requested value, deliberately: that answers a different question, which is what to launch this session with next time. Two things fall out. Control request ids are random rather than the clock, because two in the same second shared an id and something now looks them up. And the phone shortens a resolved name for the button -- `haiku-4-5` -- since the full one is what the CLI reports and roughly twice the room that row has once Stop is in it. |
||
|
|
404066fa7d |
Say when a session is working, and what it was told
Three things a phone could not see, all of them the same shape: the session was doing something and nothing on screen said so. A turn nobody here started never reported itself. `Running` was sent where a message was *sent*, so a session picked up mid-turn, one compacting on its own, or one another agent wrote to sat there reading as idle until it finished. The driver now says it from what it observes -- output that could only come from a turn in flight -- which is the same set of events that already announced a steer, with the ends swapped. An imported session had it worse: nothing but replayed lines ever reaches it, and a status was not among them, so it was permanently whatever it was when it was adopted. Its file does not record a turn ending, but it does record why each assistant message stopped, and `tool_use` versus anything else answers it. A record that says nothing leaves the status alone rather than voting for idle. Messages from other agents were dropped outright: the CLI marks them meta, and this replayed everything except meta. They are now a row of their own, closed by default like a tool call, named for the session that sent it -- not the reader's own bubble, because they did not say it, and a session working on something this phone never asked for is exactly what one of these explains. Measured against a real session file rather than guessed: the peer record carries the sender's name and the message body in `origin`, beside a copy wrapped for the model to read. |
||
|
|
5396da76c7 |
Show a compaction happening, and what it recovered
The Compacting status had been declared, rendered in four places, and never once emitted: no driver produced it, and the app had no control to ask for a compaction in the first place. Pressing nothing for two minutes and then quietly having less context was the whole experience. The CLI turns out to announce all of it, which was worth measuring rather than guessing at. Driven through /compact against 2.1.237 it emits a `status: "compacting"` line at the start, a `status: null` carrying `compact_result` at the end -- `"failed"` with a sentence saying why, when it does -- and then a `compact_boundary` with the token counts. The same records appear in the CLI's own transcript file with camelCase keys, which is the obvious place to read the shape off and gets every field name wrong. So none of it is inferred here. The driver writes the line and says nothing; the translator reports what the CLI reports. A failed compaction surfaces the CLI's own sentence, which is specific enough to act on. The counts are the part worth keeping afterwards, so they land in the transcript rather than only in a status that vanishes: a session that went from 128,402 tokens to 9,617 has just been given its context back. They are optional throughout, because a compaction whose size nobody reported has to be able to say so -- a zero would read as "recovered nothing". Also here, all found on the way: - `rename_all` renames variants; fields need `rename_all_fields`. Every field in Event was a single word until `pre_tokens`, which went out as snake_case, was not found by the app, and rendered as the "no counts reported" case -- a state it is allowed to be in, so nothing looked wrong. There is now a test on the wire names. - The unparseable-line warning sliced bytes, not chars, on output that is full of em dashes. A panic there kills the task reading the session's stdout, and the session goes deaf with nothing on screen. The other three truncations in the tree already did this correctly. - Echo compacts too, with invented numbers and a real shape, so this screen can be looked at without spending two minutes of somebody's account to reach the state. |
||
|
|
42131c75d6 |
Send a steer into the running turn, and put an image under its call
**The queue was holding messages the CLI would have taken.** Two claims in this file contradicted each other: the module header said a mid-turn message is injected at the next tool boundary -- "the behavior this app exists for" -- and `Queue`'s own doc said a line written mid-turn simply becomes the next turn. The code followed the second, parking every message until `Status::Idle`, which is the end of the whole turn. Measured rather than argued, twice. Writing a line straight into a live session's stdin fifo mid-turn produced one `result` for the whole thing, so it was consumed inside that turn, not as a new one. The header was right and the queue was built on the wrong claim. The cost was exactly what Bryan reported: he steered after the second tool call and it sat unread until every remaining call had finished. Measured before and after on the same three-step turn -- steer sent at +13s, recorded at +24.7s before this change and at +14.1s after, which is the next tool boundary. So the line goes out immediately. What stays behind is the *announcement*: the CLI says nothing on stdout about having read a message, so `MessageTaken` now waits for the next assistant text or tool call, which is proof another model call happened and the steer was in it. That keeps a held message drawn below the working indicator until the session has actually taken it -- the thing that mattered when this was last changed -- without delaying the message to get it. Idle counts too, and is the case that must not be missed: a message written after a turn's last model call has no later output to prove anything. `closed` is untouched, and `Queue::close` still reports held messages by name rather than dropping them. **An image now names the call that produced it.** `Event::Image` gains `about`, the `tool_use_id` from the tool result it came out of, so a screenshot is drawn inside that call's card instead of floating beside it -- pairing them by position is what a page boundary breaks. `None` for a person's own attachment, which belongs to no call. The import path threads it through as well, so replayed history reads the same as live. Images show whether the card is open or closed: a call whose result *is* a picture says less closed than the one line it replaced. Verified on the emulator against a real haiku turn: the checkerboard sits inside `Read /tmp/tiny.png`, and the steer sits between that call and the next, where it was taken. 53 tests, clippy, rustfmt, lint and ktfmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8fe13634cb |
Take ktfmt's formatting on the files just added
Four files went in unformatted: I ran the formatter mid-change and then kept editing. ktfmtCheck is part of finishing, not part of starting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c5dbd1535d |
Ask for permission on the call it is about, and read the input
A bash permission request arrived as a second card repeating the tool
call's input verbatim, so the same command appeared twice and the reader
had to work out it was one event. `Event::Question` now carries `about`:
the `tool_use_id` the CLI's `can_use_tool` request already names. That
makes the pairing a measured fact rather than a match on input text --
and it stays `Option`, because AskUserQuestion is not permission for
anything and an echo session's question is about no tool at all. Those
still draw as their own card, which is what every question did before.
The card also reads the input instead of dumping it. Every tool's input
is JSON, and showing it raw makes the reader parse `{"command":"…",
"timeout":5000}` to find the line they care about. A small table says
which field is the subject of which tool -- Bash's `command`, Read's
`file_path` -- and the rest is still listed, since dropping a field
would claim the tool had no other input when it might. The subject is
syntax-highlighted with dev.snipme:highlights, for the reason the
markdown renderer is a library: lexical rules are somebody else's
specification. Its theme is Catppuccin, mapped in Theme.kt beside the
rest of the palette rather than taken from the library's defaults.
The input shows whether or not the card is expanded. A row that says
only "Bash" says nothing anyone can act on, least of all when it is
asking to run something.
Verified on the emulator against a real haiku session: one card, the
description, `grep -rn "needle" /tmp | head -3` highlighted, `timeout:
5000` pulled out, and "Allow Bash?" with its buttons inside the card --
then Allow, which resolved in place and ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0c13090d70 |
Take ktfmt's defaults for the Kotlin half
The server half went to rustfmt's defaults earlier today; this is the same move for the app, and the same reasoning. Kotlin ships no formatter with the Gradle build, so the question was which to adopt: ktfmt is Kotlin-org owned now (it moved from facebook/ktfmt), is a formatter rather than a configurable linter, and has essentially nothing to tune -- which is what rule 27 is asking for. ktlint's .editorconfig surface is the thing that rule warns against, and detekt is static analysis, whose job Android Lint already does here. One setting, and it is a choice between the tool's own two styles rather than a tuning: kotlinLangStyle() is the 4-space one, which is what this code already was. The 2-space default would have reindented every file to say nothing. ./gradlew :androidApp:ktfmtFormat to apply ./gradlew :androidApp:ktfmtCheck to verify Formatting only. The one thing worth checking by hand was the generated PEM constant, since a leading newline there costs Android's CertificateFactory its preamble sniff and fails at runtime nowhere near the cause: ktfmt moved `.trimMargin()` onto its own line and left the template alone, and the regenerated constant still starts at the opening quotes. Verified after: ktfmtCheck, compileDebugKotlin and lintDebug all pass, and the APK builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw |
||
|
|
213bc72b64 |
Phase 1 app: session list, session screen, spawn, QR enrollment over pinned TLS
Compose app mirroring local-updater's stack (single :androidApp module, pinned CA, HttpURLConnection transport) plus what this app needs on top: a bearer token sealed with an Android Keystore AES-GCM key, an aiapp://enroll intent filter so scanning the server's terminal QR with the stock camera enrolls the phone with no QR library, an SSE client that resumes by transcript cursor, and a transcript renderer folding the common event model into user bubbles, streaming text, collapsible tool cards, and answerable question cards. Verified on the tdep emulator against the real server: enrollment deep link, list, spawn, streamed echo turn, question answer round trip, tool card expansion, adjustResize keyboard behavior. Build is warning-clean (compose.* accessors replaced with direct dependencies). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw |