Commit Graph
4 Commits
Author SHA1 Message Date
jaap-jan a2f0d4813a Take a third off both of the terminal's bars, and centre the cross in a tab
The bar the last commit put above a shell opened at 52 and the accessory row
under it at 50, both inherited from the arrangement they replaced rather than
measured against the one they are in. Neither is carrying a title or a sentence
any more — the top one holds two icons and a row of pills, the bottom one a
line of keys — so a third comes off each: 35 and 33.

Every height inside them came down too. The pills go 44 to 30, the icon squares
34 rather than 44, the keys 38 to 30. A bar that shrank around contents that
did not would not have saved anything; it would have moved the clipping
somewhere harder to see.

Two of those numbers had arguments written against them and both arguments
change rather than disappear. The pill was 44 because it contains the one
control on this head that is destructive with neither confirmation nor undo,
and that is now carried by width — the cross keeps its full 44-pixel column,
and what it gave up is vertical slack in a row where nothing sits above or
below it to be hit by mistake. The keys were 38 for the same kind of reason,
and the 44 that mattered there was always the width: ten keys flexed across
360dp is 32 pixels each, which is what the horizontal minimum exists to refuse.
Both comments say what replaced the reasoning rather than quietly showing a
smaller number.

The close cross was not vertically centred, and it was not a rounding error.
`Button.row` sets `HorizontalContentAlignment` and says nothing about the other
axis, so the glyph sat against the top of its own column while the label beside
it was centred by the stack panel it lives in. On the control that ends a
session that reads as a misprint. Both alignments are now stated, on the button
and on the text.

The `+` loses the accent and becomes the same `Button.icon` as the arrow across
from it. The two are a matched pair at either end of one bar — one leaves this
surface, one adds to it — and an accented one ranked itself above the way out.
The accent fill belongs to the floating `+` on HOSTS, which is the only action
on its screen; this one is not.

Five pixels between the renderer and the keys, as a margin rather than a
border. The renderer is a native child view and nothing Avalonia draws can sit
on top of it, so a hairline there would have to be a row of its own — and a
terminal whose last line of output is flush against a row of grey keys reads as
one surface that has gone wrong rather than as two that are different things.

The four places that named the old bar height are corrected, including manual
check 11.5, which asserted a number that would now fail.
2026-08-03 14:54:16 +02:00
jaap-janandClaude Opus 5 5593f337b6 Give the phone the second design, and both heads the palette it arrives with
The Android v2 design is what this head draws now: four destinations in a bottom bar — Hosts,
Terminal, Keychain, More — with snippets, SFTP, S3, logs and preferences one tap deeper behind the
last. The first design's four had nothing behind them, which is what made a hub worth building.

The palette moved from green-black to blue-black, and it moved in the shared project because that is
where it lives and the desktop v2 specifies the same seventeen tokens. One colour changed meaning
rather than value, and it is the only semantic change in the file. Green used to *be* the accent, so
Ellipse.dot.live filled with Accent and "the thing to press" and "a shell is open on this host" were
the same colour by construction. v2 makes the accent blue and keeps a green for status alone, which
finally separates them: Live is that green and nothing merely interactive may use it. The accent is
also two colours now — Accent fills, AccentText writes — because a row of chips in the fill colour is
a row of things that all look like the primary action.

A palette is not one file, which is the part worth knowing before the next one. Nine hex literals
lived outside it: the nav bar's own label colours, the accessory keys and their Ctrl-latched state,
two scrims, the window background Android paints before Avalonia has a frame, and the launcher
vector. The two C# sites now resolve from the dictionary by name rather than restating it. The
renderer's page cannot — it is served to a WebView over a loopback socket — so terminal.css and
terminal.js keep hand-copied values and say so at both sites.

ShellScreen gained More and Buckets, appended rather than slotted in. SFTP and S3 are one screen over
one TransfersViewModel differing only in which picker they offer, and the kind is set by the button
that navigates rather than on arrival — doing it in OnScreenChanged made every arrival at Transfers
force the picker back to hosts, including the desktop's own rail arriving at a screen with a bucket
already open. It refuses to change kind while a session is live, because there is one session behind
both destinations and switching under it would title a screen S3 while it listed an SFTP host.

What the design draws and this does not, on the usual grounds. The FORWARDING screen: nothing here
forwards anything, so every toggle would be a control with no effect — it is a paragraph on the hub
naming the absence, for the reason the desktop keeps TEAMS in its rail. The terminal's `23 ms · fwd
5432`. An ED25519 badge and a SHA256 line on keychain cards, which need an algorithm field and a
fingerprint the item type does not have. An `agent` chip, for an agent that does not exist. Snippet
run history and exit codes. The Logs FOLLOW pill, which claims a live tail over records that are
written once at close and read when the screen opens, and the severity filter, which has nothing to
count — that chip row is spent on the real choice, which of the two logs. S3 bucket totals and
lifecycle. And the + on HOSTS, which would open a host editor this head has not got.

SFTP is browse, open and delete. Both transfer commands work, and what they work against is the local
pane: QueueDownloads writes to Path.Combine(LocalPath, name), and LocalPath starts at
SpecialFolder.UserProfile, which on Android is the application's own private directory. A download
would have reported success and left the file where the person who asked for it cannot open it, which
is worse than not offering it — a refusal is visible and a file in /data/user/0/ is not. The queue is
not drawn either, since nothing here can put anything in it. Both return with the document picker.
The foreground service still counts zero transfers, and the reason moved rather than went away.

Four defects worth naming, because three of them are the kind that compile. A Button as a ListBox
ItemTemplate swallows the pointer press before the list sees it, so the files listing selected
nothing and every command reading the selection did nothing — the row is a Border now and the
phone-only single-tap-to-open is a Tapped handler, which also keeps a desktop single click from
walking into directories. Avalonia type selectors are exact, so TextBlock.fingerprint never matched
SelectableTextBlock and every fingerprint on this head rendered proportional and unwrapped: that was
breaking the never-truncated rule on the host-key sheet already. The new two-level hierarchy had no
handler for the system back gesture, so back left the application from a log screen. And the tab's
close cross had shrunk to a 30x32 target flush against the select target, which is the one control
here that ends a shell with no confirmation and no undo.

Fingerprint unlock is raised on arriving at the lock screen rather than waiting for its button, which
is still there. Only at launch: a lock the user asked for is not answered with an immediate request
to unlock, which makes LOCK look inert and trains the reflex of authenticating at a prompt nobody
asked for. And once, because a declined gesture leaves the passphrase box exactly where it was and a
prompt that came back after being dismissed would be a modal you cannot get out of to type into it.

Two fixes fall on the desktop. Its file listing coloured directories with Info and executables with
Accent, which was blue against green and is now two steps of one blue; an executable is Live now.
And a bucket's folders were drawn with a 0001-01-01 timestamp, because a prefix has no modification
time — blank now, for the reason a directory's size is blank.

Verified by the whole suite: 1309 tests over nineteen projects, none failing, including the layout
suite that stands up real Avalonia and parses every desktop screen. Both heads build. Not verified on
a device — nothing in this head ever has been; see docs/android-port.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZE3u99BNt6LzgTC5jhbz2
2026-08-02 18:23:53 +02:00
jaap-janandClaude Opus 5 5cb9ffaf9d Stop the phone's terminal being a page you can pinch
ci / build and test (push) Successful in 1m3s
ci / android head (push) Failing after 4s
ci / api image (push) Successful in 36s
The terminal on Android could be pinch-zoomed and double-tap zoomed, which a terminal must not
be: the grid is sized to the window by the fit addon and the column count is told to the remote,
so zooming makes the visible width disagree with what the far end is wrapping to, and puts the
cell under your thumb somewhere other than where you tapped.

Three things, and the first is the one that made it look worst. The page had no viewport meta tag
at all. An Android WebView with no viewport lays out at a notional 980 CSS pixels and scales the
result down to fit, so a terminal built to fill the window was drawn small and then offered as
something to zoom around in. width=device-width makes one CSS pixel one layout pixel, which is
what the fit addon has been assuming all along and what a desktop WebView gives without being
asked.

Second, the gestures. touch-action: pan-y keeps the only one a terminal wants — dragging the
scrollback — and refuses pinch-zoom and the double-tap zoom that fired on every attempt to place
a cursor. text-size-adjust stops Android's own font inflation, which resizes text it judges too
small without telling the page and leaves the characters no longer matching the grid that was
measured.

Third, the knob that actually disables zoom: BuiltInZoomControls on the WebView. user-scalable=no
is in the viewport tag for completeness and does nothing on its own — Blink has ignored it since
Chrome 48 and WebView follows Blink. That is worth knowing before somebody removes the C# and
trusts the meta tag.

The page and stylesheet are shared with the desktop head, deliberately, and none of it costs
anything there: a desktop WebView already lays out at device width, and a Windows touchscreen
should not be pinch-zooming a terminal either.

This does not settle whether the phone should keep a browser-based terminal at all, which is the
question actually asked. It does remove the reason it was asked, and every complaint about this
renderer so far has turned out to be configuration rather than the approach.

Verified by building the head in Debug and Release and by the desktop layout suite, which draws
the same assets. Not verified on a device — nothing in this head is; see docs/android-port.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:45:11 +02:00
jaap-jan 7a3a521c59 Give the phone the rest of its screens, and a way in
ci / build and test (push) Failing after 2s
ci / android head (push) Failing after 1s
All seven screens of the design, plus the two it does not draw because it starts at an
enrolled phone: naming a server, and choosing a passphrase.

The five states docs/android-port.md worried about losing at 360dp are all here and none
of them softened. The changed-key refusal is a full-screen panel rather than a bottom
sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way
forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards,
so the sentence about screenshots is true rather than decorative. The delete
confirmations keep their counts and replace the row in place.

Signing in works, and the seam it needed is worth more than the implementation:
IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two
heads differ in where the response arrives and in nothing else. PKCE, the state check,
discovery, the token exchange and the key binding stay one implementation — a second OIDC
client would be a second place for a security bug to live. The phone registers a
private-use scheme with the system rather than binding a loopback port, which on a shared
device any other app can do first.

The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from
the renderer straight down the socket, and there was no way in for the keys a software
keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is
drawn — a modifier that is on and does not look on is how somebody sends ^L to a database
prompt believing they typed an l.

597 client tests green, including two new ones for the input path and one for the
terminal surface command. Nothing has run on a device.
2026-07-31 21:43:11 +02:00