9801a744baec628410c8c48082d1e4273c13496f
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
80ae586fc4 |
Give a shell the whole phone, and one bar to leave it by
A connected phone was drawing five rows of chrome around the thing the user opened it for. The vault header at 56, the terminal's own tab strip at 52, a connection line at 36, the shells strip at 46 and the four-entry bottom bar at 64: at 360dp that is about a third of the display, and every row of it was about somewhere the user was not. What replaces them is one 52-pixel bar drawn by the surface itself — back on the left, the session pills, and a `+` across from them — and then the terminal. Three of those rows belong to `PhoneShell` and each is now bound on `IsShowingPages`. That is the same question asked once rather than three conditions that could drift: the surface is either a page or a terminal, and these are the chrome a page has. The header needed a wrapper because Avalonia's bindings have no "and" and it already had a condition of its own; the strip needed one for the same reason. The bottom bar had none and is bound directly. The back arrow goes to the page the terminal was opened over rather than to Hosts by name, because the system back gesture already picks that and an arrow landing somewhere else would be the second of two answers to one question. The bar's `+` raises a sheet offering the three connections this application can make — a shell, a host's files over SFTP, a bucket — since SFTP and S3 used to be two taps through the bottom bar's MORE and the bar is not on screen here. A control that replaced it and led to one of the three would have quietly removed the other two. Two things moved rather than being dropped. The text-size buttons are pinned at the right-hand end of the accessory key row, outside its scroller: the connection line existed to keep them from scrolling out of reach, and being outside the scroller answers that argument rather than abandoning it. The dialled address moved onto the connecting card, which is the moment it is worth reading — what is being connected to, before anything has answered — and after that the shell's own prompt says it more accurately than a header derived from the keychain ever did. The sheet collapses the renderer rather than covering it. Whether Android's `WebView` composites above Avalonia content the way Win32's child window does is still unverified — `docs/android-port.md` has said so since the port — so this follows the desktop's palette and gives up the rectangle outright, which is correct under either answer. It collapses `IsTerminalShowing` and not `IsTerminalSurface`, because the bar the sheet was raised from is part of that surface and dropping it would take the bar, the tabs and the whole arrangement with it, leaving the sheet floating over the page underneath. `OnSurfaceChanged` is the one place the flag is lowered, and that is the load- bearing half. Every way out of a terminal ends there — a destination, the files screen, the palette connecting to a host, closing the last tab, a lock — and each of them would otherwise leave a sheet set over a page. Not merely untidy: the flag holds the renderer blank, so the next return to the terminal would draw the menu again over a rectangle kept blank by it. Opening is refused off the terminal surface for the same reason from the other direction. The back gesture gains a guard above the switch, in the shape of the editor guard that arrived with the phone's `+`. It is nearer than any of them: with no header and no bottom bar, while the menu is up that gesture is the only way off it other than the scrim and CANCEL. The bottom bar's Terminal entry lost its `IsCurrent` binding. The bar is collapsed on that surface, so the binding could only ever be read as false, and a rule about a state the control cannot be in is a claim that it can. Three tests in `ShellFlowTests`, which is where shared state-machine behaviour for this head goes: the collapse and its recovery, the refusal to open over a page, and the sheet lowering both by a menu entry and by a route it was never wired to. Everything visual needs a device, so it is phase 11 of `docs/manual-checks.md` — and 11.2 is the check that would finally settle the compositing question this head has carried as unverified since the port. |
||
|
|
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 |
||
|
|
c00e5dbc5c |
Let the terminal's text be made bigger, and remember how big
Taking pinch-zoom off the phone left nothing in its place, and there was nothing on the desktop either. This is the replacement, and it is deliberately not the thing that was removed: zoom scales what has already been drawn, so the remote goes on wrapping to a width that is no longer on screen. Changing the font size refits the grid and reports the new column count, so the far end is told it has fewer columns. That round trip is the feature. The size is one number, owned by the shell. It has to be, for two reasons that pull the same way: it must survive a relaunch, and it must be reachable from a phone that has no Ctrl key to press. So the page asks and the host decides — a signed step over a new client opcode, answered with a size over a new server opcode. The phone's buttons and the desktop's chords arrive at the same place, and a size set by either is the size both remember. Stored in settings.json beside the cache rather than in it, and that is not laziness about a migration. The cache is encrypted and unreadable until a vault is unlocked, and the first terminal of a locked launch needs the size already. Nothing secret may go in that file; ClientSettings says so out loud, because the next person to add a preference is the one who needs to read it. Where it is reachable from differs per head, and only here. The phone gets A− and A+ on the connection line — not in the accessory row, which scrolls, and a control that fixes unreadable text must never be the thing that is off-screen. The desktop gets the three chords every terminal emulator has, answered by the page while a terminal has focus and by the window when it does not, plus a row in preferences that shows the current value and names the chords rather than replacing them. Someone whose terminal is too small to read is not in a position to go looking. Clamped 8 to 32. Below eight a monospace grid stops being legible and becomes a texture, and every column of it is still a column the remote is being told exists; above thirty-two a phone in portrait has too few columns to hold a prompt. The buttons disable at the ends rather than accepting presses that do nothing, which on a terminal reads as the application having stopped responding. The preferences screen's header comment claimed none of the design's terminal settings could be saved, and listed the three things that were missing to make one work. All three now exist, so it says which one is real and why the other five still are not. Verified with the protocol suite — including that the step byte round-trips signed, since read unsigned a step down arrives as 255 and clamps to the largest font, making "smaller" do the most dramatic available version of "larger" — a data-plane test that the chord is heard with no session registered, and five shell tests: the default matches the renderer's, both clamps hold, reset works, and a size chosen in one shell is there in a second one over the same profile directory. Layout suite and both heads build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4300d917a8 |
Stop making people wait for a handshake, and give the host list a pointer
Connecting held the vault's busy gate, which meant a window that did nothing visible for as long as a machine took to answer — and against one that is merely asleep, that is the whole timeout. The gate is gone from that one command. A tab now appears in the strip in the same turn as the click, carrying "connecting…" rather than a pane, and the terminal's rectangle draws a card naming the host and the address being dialled. Every other screen stays usable, and two connections can be in flight at once. That splits the vault's one connection event into three, carrying an attempt id, because "which tab is this about" can no longer be answered by "the most recent one". The id also buys the two kinds of not-connecting their different endings: a refusal stays in the strip as a tab holding its reason, since by then the user is quite likely three screens away and a status line they are not looking at is not where a failure should end; a host key question takes the tab away and puts the window back on HOSTS, because the prompt is drawn there and a tab claiming failure would be competing with the thing about to resume it. ConnectAsync takes no CancellationToken any more, and that is load-bearing rather than tidying. A [RelayCommand] over a method that takes one generates a command that cancels the previous execution's token on every invocation — so asking for a second machine silently abandoned the first, measured as the first tab disappearing with "Cancelled." the instant the second was asked for. Giving up on a connection is closing its tab, and a session that lands after that is adopted rather than dropped: a shell running with nothing naming it cannot be closed at all. A tab is marked active on IsShowing rather than IsSelected. The selection survives navigating away — that is what makes the strip a way back to a terminal instead of a way to lose one — so a tab lit while preferences filled the window was a second "you are here" mark pointing at something nobody could see. The nav rail's own entries have always made this distinction. The host list grows the two gestures it looked like it already had. A right click selects the row under the pointer before opening a menu of Connect, Edit and Delete — the menu is on the list rather than in the item template, so its entries are the vault's own commands and not a row's, and it is cancelled outright over a group heading. Dragging a host onto a heading files it there, onto a host files it beside that one, and onto UNGROUPED takes it out of a group; the write is one field of one host through the same repository a save uses, refused while the editor is open because a drop is a gesture on the list and not on a half-typed form. Clicking a result in the palette connects, which is what a list of hosts under a search box looks like it does. It went through the shell's own command, so the pointer and Enter take one path. And the files screen's two pickers followed the vault's lists once, at unlock: a host or a bucket created afterwards could not be picked until the keychain had been locked and opened again, with nothing on screen explaining why the machine plainly in the host list was missing. They follow the collections now, re-finding the selection by id across the rebuild a sync pass causes every minute. 165 shell tests and 69 layout tests green, including the connecting tab, both failure endings, two connections at once, a connection in flight across a lock, and the right click acting on the row under the pointer rather than on the selection. The drag itself is in docs/manual-checks.md with the rest of phase 7 — headless Avalonia has no platform drag, and a test that claimed to have dropped something would pass while confirming nothing. |
||
|
|
7a3a521c59 |
Give the phone the rest of its screens, and a way in
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. |