Commit Graph
9 Commits
Author SHA1 Message Date
iris 69ef6f068a Colour the composer by what its buttons do, and let the read-out breathe less
Queue is the paper plane with a clock on it (`md-send_clock`) rather than
the plain plane plus the word: the pair is now told apart by the mark, which
is what an icon is for, and the word survives as the button's accessible
name where a screen reader still needs it.

The three composer buttons take their colour from what pressing one does --
green sends now, blue sends later, red takes the running turn away -- and
Stop becomes a filled button like the other two. Outlined said it was a
qualifier on the primary action; it is a second thing you can do about the
turn, and what separates them is the colour and the mark. The fills are
named in Theme.kt with their content colour stated beside them, because a
semantic colour has to carry its own contrast: these do not change with the
surface, so nothing will rescue a foreground that stops being readable.
Worth knowing when reading that file: the action greens and reds sit next to
a `runningColor` green and a `failedColor` red, which are *states*. Nothing
in one set is pressable and nothing in the other is a state, so a reader
never has to tell them apart.

The usage read-out loses its per-machine cards. A card is a step up the
surface ladder and inside a dialog -- already a raised surface -- the step
barely rendered while costing 16dp on every side. The machine and the
service it answered for are one small quiet line instead of a heading over a
subtitle, since the numbers underneath are what somebody opened this to see.

The gaps between the bars now go *between* them rather than after each,
which is what put a band of empty dialog above Close. The rest of that band
was AlertDialog's own spacing, fixed at sizes meant for a sentence of prose
and a decision, so this is a plain Dialog with the same container colour and
corner and spacing chosen for a dense read-out.

Looked at on the emulator: green send, then blue queue beside red stop
during a `/slow 20` echo turn, and the dialog over the live session. The
account had risen to 78% by then, which showed the five-hour bar and the
header glyph going yellow on real numbers rather than forced ones.
2026-08-29 22:45:18 -04:00
iris ff39ef5cf9 Say it in icons, and put the whole backend behind four tabs
Six things Bryan asked for, which turned out to be one change: the app had
no icon set, so every one of them was blocked on having somewhere for icons
to come from.

That somewhere is dev-updater's arrangement, ported: a Nerd Fonts subset
committed as an asset, drawn as text. `Gear.kt`'s hand-drawn canvas gear
argued against icon fonts because a system font may not have the glyph and
whoever gets the empty box is never the person who wrote it. The objection
is right about *relying* on a system font and the answer is to ship the
glyph, so the file is gone and its reasoning is restated in `NerdIcons.kt`
rather than deleted -- otherwise the next reader re-derives it. `md-cog` and
`md-refresh` are dev-updater's own codepoints, because a cog means the same
thing in both apps.

The root screen's four words under the title are now four tabs, and the two
that act on the whole screen -- settings and refresh -- moved up onto the
title row as glyphs. That row's old comment recorded that a fifth word would
have had nowhere to go; tabs also say something the words did not, which is
that sessions, import, models and setups are four views of one backend
rather than four errands. Refresh feeds whichever tab is showing. Import,
models and setups lose their headings and their Back buttons, since the tab
row is now both.

Usage is a dialog. It is checked *against* what you were reading -- "can I
start this" is asked with the transcript still on screen -- and it had no
navigation of its own, so the only thing its Back could mean was "put this
away". The button that opens it is a chart glyph coloured by the worst of
the machine's windows, so the row says whether the limits are worth opening
before anybody opens them.

One `quotaColor` now colours every bar that measures a quota: blue, yellow
at 75%, red at 95%. The session bar escalates where it used to sit blue at
every level, and the dialog's thresholds moved out of it. A download keeps
plain blue at every value -- it has no limit to approach, and colouring it
like one would say the opposite of what is happening. States that are not
measurements take the ordinary control colour, since blue is the low end of
this scale and would read as "checked, and fine" about a machine nobody
could reach.

Send and stop are the filled paper plane and the filled square. Send keeps
the word "Queue" while a turn is in flight, because that is what pressing it
then does, and an icon that does two things while looking identical would
promise something immediate and do something that waits.

Looked at on the emulator: all six glyphs render, the tabs and the system
back gesture between them, the dialog over a live session, and the bar
bands at 82% and 97% forced through a scratch build, since this account is
at 72/31/5 and would only ever have shown blue.
2026-08-29 22:27:54 -04:00
iris a50d72960c Say how long is left, not that the window is five hours
The bar read "31% of 5h", which is the one thing about the window a
reader already knows. What decides whether to start something now is how
long what is left has to last: 80% with twenty minutes to go and 80%
with four hours to go are opposite answers, and the second number was a
screen away on the usage screen.

It now reads "31% - 2h 36m left", and the countdown is driven by a clock
the refresh loop advances rather than computed at draw time. A
percentage that comes back unchanged is an equal value, so Compose skips
the recomposition -- a "left" recomputed only when the quota happens to
move would have sat at a stale figure for hours while looking live.

A window can arrive with no reset time, so that keeps its own wording:
"reset time unknown" rather than "refresh soon", which would be a
recommendation nothing measured. Under a minute, including past the end,
is "refresh soon" -- "0m left" reads as a measurement.

The span arithmetic was already on the usage screen, so it moves into
`ResetCountdown.kt` and both callers supply their own sentence. That
screen still reads "resets in 2h 37m" and "resets in 5d 21h", checked on
the emulator alongside the bar it was not part of changing.

The fill is blue rather than the scheme's primary: the bar sits under
every session header, on a screen somebody opened to do something else,
and it reports a quantity rather than a verdict. The usage screen is
still where the same number turns yellow and then red, for a reader who
went there to be told where the limits are.

Also declares this project's resources for Dev Updater, whose
declaration schema changed in d27b5a3: `resources.ron` says ai-app keeps
its state as `ai-app`, so the Uninstall dialog offers the real
directories instead of saying it cannot tell where they are. Only the
name, because both XDG places are the conventional ones. What that
dialog's config toggle would delete includes the CA under `certs`, which
strands every phone running an APK pinned to it -- noted where somebody
would be standing when it matters.
2026-08-29 20:54:55 -04:00
iris 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.
2026-08-29 17:10:21 -04:00
iris 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.
2026-08-29 15:27:46 -04:00
irisandClaude Opus 5 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>
2026-08-29 06:24:05 -04:00
irisandClaude Opus 5 9f403cddab Render markdown, and stop wiping messages still waiting to be read
Two things, both about the transcript telling the truth about itself.

Markdown is rendered rather than shown as its source. The parsing is
mikepenz/multiplatform-markdown-renderer, not something written here:
markdown is somebody else's specification, and a hand-written subset of
one disagrees with it at the edges, which is where the bug reports come
from. `Markdown.kt` is only the mapping onto this app's palette, so code,
links and rules take the Catppuccin values the rest of the app uses
rather than the renderer's defaults.

The queued-message list was cleared wholesale whenever a turn ended. But
the backend holds a queue of its own and takes one message per turn, so a
turn ending is precisely the moment the *rest* are still waiting -- the
bubbles vanished while the messages were on their way, which reads as
everything after the first having been dropped. Now a held message
leaves the list exactly two ways: the session reads it, which arrives as
a UserMessage, or its send failed and there is nothing to wait for.

Measured first, because the report was that the backend dropped them:
three messages sent behind one long turn were all delivered in order
(ONE, TWO, THREE) against current main, so the loss was in the display.

Verified on the emulator: headings, emphasis, inline code, nested lists,
a quote bar, a fenced block, a rule and a link all render, and the three
queued messages sit through their turn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 06:10:22 -04:00
iris 9791afcfd6 Hold a queued message below the indicator until the turn takes it
The backend records a message the moment it is sent, so one sent into a
running turn landed in the transcript at the time *we* spoke -- above the
working indicator, in among things the session had already read. It had
not read it. Showing it there says otherwise.

Messages sent while a turn is in flight are now held below the indicator,
drawn quieter, and take their place in the conversation when the turn
ends. That is an approximation and worth naming: the CLI injects a queued
message at a tool boundary, and tells nobody when it does, so the end of
the turn is the first moment anything here can honestly say the message
was taken. It errs toward "not yet read", which is the direction that
cannot mislead.

Also in this change, from the same pass over the screen: the app draws
above the gesture strip rather than under it, and the three status colours
that were literals in two other files -- an amber, a green and a red off
Material's defaults -- are now Catppuccin members in Theme.kt beside the
rest of the scheme.

The ordering is verified by construction rather than photographed: the
list is bottom-anchored, so the first item emitted is the lowest on
screen, and the queued block is emitted before the indicator. Staging a
real long-running turn to photograph cost four model turns and never
produced one, because the model kept declining to sleep -- which is its
own finding, and the reason the next change is a test command in the echo
driver.
2026-08-28 23:49:55 -04:00
iris 31135e3f22 Wear Catppuccin Mocha, and move Usage to where the provider is
Two changes to the app, plus the one they turned up.

**The theme.** Catppuccin Mocha, copied from dev-updater rather than
shared: wg-app-link is the *link* -- the tunnel, the pinned CA, enrollment
-- and a palette is not that. The two apps looking alike is a preference
rather than a contract, and the moment one wants a different accent a
shared version becomes a thing to fight. dev-updater's ActionTone and its
ANSI table did not come across; nothing here draws a log or a destructive
button yet, and copying a vocabulary with no speakers is how a file starts
lying about what the app does.

**Usage is no longer a global button.** It belongs to the provider, and
the session view is the only place a provider is currently named, so that
is where the control sits -- beside the line that names it, rather than
collected with the app-wide controls where its scope had to be guessed. It
carries the session back with it, so Back returns to that session rather
than dumping the reader on the list.

Its real home is that provider's own settings, which do not exist yet.

**And the bit that only running it could find.** I first subtitled the
usage screen with the session's provider, which on an echo session put
"echo" directly above a card reading "claude" -- Claude's account-wide
numbers labelled as echo's, a claim about echo that nothing measured. The
subtitle is gone; each card names the service that answered, which is the
true scope, and the reason is written where the subtitle was so it does
not get re-added.

Looked at on the emulator rather than read: the palette, the session
header, the usage screen, and Back landing on the session it came from.
ktfmt, compile and Lint clean.
2026-08-28 20:26:12 -04:00