9f73893e14dda7b5421d5794fcc0986b9dd333ed
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ccaf7a8e72 |
Give the terminal back the width and the keyboard the session shell took
Seven things reported from a day's use of v5b's session shell, and they are one commit because five of them are the same complaint from different angles: the window spends too much of itself on chrome describing the session, and the parts that are not chrome do not behave. ◆ THE HOST HEADER IS GONE, and that is a deliberate departure from the design. Terminal.dc.html and SFTP.dc.html both draw a 60-pixel row above the pane: the address on the left, a cross-surface button on the right. Both facts are worth having and the strip they sat in was not — the tab already names the host, and a full-width bar repeating it was the cheapest 60 pixels in the layout to give back. The address is now the first line of the sidebar and the button is stretched across the column under it, so nothing is lost and the pane is taller. Which word that button carries and which command it runs used to be handed in from the two usage sites in MainWindow.axaml, because the control was drawn twice. One sidebar cannot do that, so SessionCrossSurfaceLabel and OpenOtherSurfaceCommand resolve it in the shell — the same place SessionAddress already decides which surface's fact to read. The two directions underneath are untouched: SelectFilesHostAsync for a tab's host, OpenTerminalForFilesHostAsync for a fresh terminal at whatever SFTP has open. SessionHeader.axaml is deleted rather than left unused, and LayoutHarness stops subtracting its 60 pixels from every session screen's budget — the same treatment the retired window-wide tab strip got, and for the same reason: a constant for chrome nobody draws is a suite quietly measuring the wrong rectangle. ◆ AND THE SIDEBAR CLOSES, which the design has no state for at all. 300 pixels of an 1081-pixel minimum is a great deal to spend on a list that is often two rows long. The column now folds to a 34-pixel rail carrying the chevron that brings it back — a rail rather than nothing, because a panel that vanishes leaving no trace is one people report as lost rather than as closed. Both states live in the one control and swap on IsSessionSidebarOpen, so MainWindow's own "Auto" column takes whichever width is showing without knowing the state exists. Written through to ClientSettings.SessionSidebarOpen rather than held for the session. It is a decision about how much of the window a terminal gets, and one that had to be made again on every launch would not really be on offer. ---- THE FOUR SMALLER ONES ---- A SNIP LANDED IN A TERMINAL NOBODY COULD TYPE AT, and looked selected when it got there. Two causes with nothing in common. The click moved Win32 focus onto the sidebar row, and term.focus() in the page cannot take it back — only the host can, so the shell raises TerminalFocusRequested and the window answers with the same posted focus every other path here uses. The highlight was bash: xterm wraps a paste in bracketed-paste markers, readline marks what arrives inside them as an active region, and it stays in reverse video until the next keystroke. Right for a clipboard paste, wrong for a snippet picked off a sidebar. Single-line snips are typed rather than pasted now, which needs no markers; multi-line still pastes, because "runs three commands unasked" is the worse of the two failures and the markers are the whole of what prevents it. A BLACK BAR UNDER THE TERMINAL, on Windows. xterm.css paints its scrolling viewport #000 — its own comment explains why, and it is a macOS scrollbar concern. Everywhere else that black is covered by the rows, except along the bottom: the fit addon floors the row count, so the remainder below the last whole row is bare viewport, up to a line tall, against this page's #171a26. The light square at its right-hand end is where WebView2's classic scrollbar corner lands. The viewport is repainted in the page's own background, and the scrollbar with it — thin and in these colours rather than a grey Windows channel down the side of a near-black terminal, and kept rather than hidden, because a surface that scrolls with no sign that it does is worse than a quiet bar. THE PINS ROW DREW A TOFU BOX. U+E946 is not in the embedded Material Icons face at all — that file is the 2019 build and its cmap skips E944 and E946 — so the rail's Pins row and the hosts screen's own pin badge have both been drawing a missing-glyph rectangle since v5b picked the codepoint. push_pin in that vintage is U+F10D, verified against the file rather than against a codepoints table for a later release of the font. Every other icon codepoint in the repository was audited the same way; this was the only miss. THE KBD CHIP CUT THE CHORD IN HALF. 34 pixels is the design's width for a chip reading ⌘K, and this build substitutes CTRL K — six characters and a space, wider than 34 at 10.5 mono. MinWidth and padding instead, so the design's footprint survives for the day this face has a ⌘ to draw. ---- AND THE POPOVER UNDER THE USER CHIP ---- Reported as not matching the design, and it was not: Button.poprow set a corner radius and a padding and never touched the Background, so every row wore the Fluent theme's own #33FFFFFF button fill. Six raised pills stacked in a menu the design draws as six lines of text — and the hover rule underneath was already correct and simply invisible against a fill that never went away. Set on the ContentPresenter as well as on the Button, the same as Button.flat, because the theme binds its brush there and a Background set only on the control loses to it. The panel itself gets this window's own radius-12 card treatment through a FlyoutPresenter class rather than by widening the shared context-menu rule, and Vaults and Preferences stop being drawn one step dimmer than Settings and Logout, which read as two disabled entries in a menu of five live ones. ---- WHAT PROVES IT ---- Three tests in the layout suite, two of them checked against the defect they describe: the popover row's resting fill (fails with #33ffffff without the style), and the kbd chip against the natural width of its own text, measured on a detached copy because a TextBlock's DesiredSize is already clipped to what it was given and reports 34 inside a 34-pixel chip either way. SessionSidebarTests is new — the sidebar has never been laid out by a test, and it now holds a string of unbounded length beside a button that has to stay clickable. In the shell suite: the cross-surface row in both directions, the closed state surviving to disk, and the focus request being made when a snip lands and not made when it does not. |
||
|
|
c3ef4bd8b4 | Flatten the hosts screen into one board of sections | ||
|
|
507cd9ff88 |
Choose more than one host card on the desktop, the way the phone already can
The chosen-hosts set has been in VaultViewModel since the phone's connect card became a contextual action bar: a set of entity ids, a tick on the row, and seven things that can be done to it. Only one head could fill it. The desktop's grid answered a press with one selection — the card the drawer, CONNECT and the context menu are about — so filing eleven imported machines under a heading was eleven drags, and clearing out a vault was eleven rounds of the deletion question. So the pointer gets three ways into the same set. Ctrl-clicks a card to tick it, Shift-clicks to tick the run between the anchor and the card, and drags a band out over the space between and below the cards to tick everything it touches. Esc, CLEAR, a plain click on a card and a click on the empty space each drop it, and Ctrl+A takes every card being drawn — VisibleHosts, so with something in the find box that is the ones on screen and not the ones it is hiding, which is the version of that shortcut whose result can be checked before Delete is pressed. TWO SELECTIONS ON ONE SCREEN, AND KEEPING THEM FROM DISAGREEING IS MOST OF THE CHANGE. Ctrl and Shift are answered on the tunnel and marked handled, so the ListBox never moves its own mark onto the card: a Ctrl-click that also selected would light the card it had just unticked and open the drawer on a machine somebody is removing from a set. A plain press drops the set unless it lands on a ticked card, and that case is deferred to the release, because the press may be the start of a drag of all of it. After any ordinary click exactly one card is in play, which is what makes every command on the screen unambiguous again. The context menu is where the seven live, and it is one markup with two halves gated on IsChoosingHosts. Connect, Browse files and Edit… are drawn only for a single ticked host, as the phone's sheet collapses them and for the same reason; the other four read better for a count. A right click on a card outside the set drops the set first, so a Delete… about the card under the pointer can never be offered while six sit ticked behind the menu — the same rule OnContextRequested has always enforced for the selection, reached from the other direction. No bar of buttons: the phone raises one because it has no other way to hold seven entries, and a strip repeating a menu that already exists would be a second home for the wording that matters most. What the desktop gains instead is a count beside the HOSTS heading, CLEAR, and a sentence saying where the actions are. A drag that starts on a ticked card carries every ticked card. The payload is a list rather than a row now, and a drop of more than one goes through FileChosenHostsUnder, which makes the refusals once — an open editor, and a group belonging to one keychain — and reports a count instead of forty status lines. Moving whichever card the pointer happened to be holding and leaving the other five where they are is a gesture that quietly does a fraction of what it looks like it does, and the five left behind look filed. The three panels the set's actions raise had never been drawn in a window: the vault picker with its key question, the group picker, and the deletion question. All three sit above the grid rather than over it, which is the arrangement the GROUPS section and the phone's list already use and for the reason written there — the ticked cards are the information the question exists to give, so the grid shortens instead. A DEFECT FOUND BEHIND IT, AND IT WAS ALREADY LIVE ON THE PHONE. The deletion question names a count and the run that answers it reads the set again, and nothing kept the two the same set: the panel is deliberately above a live list, so one more tick between "Delete these 6 hosts?" and pressing DELETE deleted seven, with the seventh named in nothing the user had read. It needed a deliberate act on a phone and a second's work with a band, which is what turned it up. VaultViewModel now remembers which hosts the question was asked about and drops the question when the set stops being them — the question rather than the set, because what somebody has just chosen is what they meant. It also covers the case nobody performs: a colleague's deletion arriving mid-question and shrinking the set under it. VERIFIED. 354 tests in App.Tests and 111 in App.Layout.Tests, build clean, no new warnings. Six gesture tests drive real pointer and key input through the headless window — the modifier click and what it must not do to the selection, the run and its re-measurement from the anchor, the band and the click that drops the set, Ctrl+A under a filter, and the menu's two halves — plus a DragOver carrying two hosts. Four layout tests measure the strip and the three panels at the window's minimum; the vault panel binds a key to its host first, or it would measure the short shape and certify the tall one. Two flow tests cover the multi-drop's write and its refusal, and the deletion question dropping itself. manual-checks gains 7.6a for dragging a set, which no test can see for the reason 7.6 gives, and 7.7a for the gestures — the rectangle actually being painted and the tick and the fill being legible together are the two things the harness cannot look at. |
||
|
|
4f9faa2fe3 |
Ask about a host key where the connection was made, not on the host list
The trust prompt was two banners at the top of the desktop's hosts screen, so the shell navigated there before letting a handshake raise one: Screen = Hosts, Surface = Page, in OnVaultConnectionFailed and again in the palette's own connect. The reason was sound — a connection can be started from Ctrl+K on any screen, and a question behind whatever somebody is looking at is a question nobody can answer — and it was answered the wrong way round. Rather than making the decision reachable from where the user is, it moved the user to where the decision was, and charged every screen for it. It is worst for the one connection that has no host at all. A machine typed into the phone's connect box is deliberately in no keychain, so a first contact from there judged it on a list it does not appear on, after taking the box that dialled it away. So both heads now draw the decision over the surface. HostKeyCard is the desktop's, and is the counterpart of the phone's HostKeySheet: a scrim with no press handler, because a question with two named answers must not be answerable by missing; the unknown key offering TRUST AND CONNECT, because judging a fingerprint against what an operator published is a decision a person is entitled to make and the only moment they can make it; and the changed key offering no way forward at all, because a button beside that warning is "continue anyway" with two clicks instead of one. The phone needed no new markup — its sheet was already a shell-level overlay, so deleting the navigation is what puts it over the Connections screen. IsHostKeyDecisionShowing is on the shell rather than on a screen because the answer decides an occlusion. A second connection can be refused while a first one is open, so this card is routinely raised over a live terminal, and that rectangle is a native child window: layered over it the card would be sliced at its left edge with TRUST AND CONNECT taking no clicks, which for the most safety-critical question in the product is the worst place for that class of bug to land. IsTerminalShowing gives the rectangle up instead. The banners are gone rather than copied. One prompt in two markups is two copies of the most safety-critical wording here, and the second is the one that goes stale. TWO DEFECTS FOUND BEHIND IT. VaultViewModel.RejectHostKey cleared only the pending key and never the mismatch, so the changed-key refusal had no working exit. That was invisible for as long as it was a banner nothing was drawn over — nothing was trapped, and the next attempt cleared it — and it was already live on the phone, where that refusal is an opaque full-screen panel whose one button runs this command: pressing it left the panel up over every screen the user went to next, including the host editor the panel tells them to open. TransfersViewModel.RejectHostKey has always cleared both; the vault's was the outlier. Its button said BACK TO HOSTS, which was wrong twice over, and now says BACK. And an assertion written for this change could not fail: the palette test asserted the renderer was collapsed in a scenario whose only tab had just been removed, so it was collapsed for want of a session whatever the occlusion rule said. It is gone, with a note pointing at the test that can fail on it. VERIFIED. 1580 tests, build clean, no new warnings, format clean. Three mutations each seen to fail and then seen green again: dropping !IsHostKeyDecisionShowing from IsTerminalShowing, caught by AChangedHostKey_CollapsesTheTerminalItIsRefusedOver; reverting RejectHostKey to clear one flag, caught by RefusingAHostKeyDecision_TakesItOffTheScreen(false) and by that same test; and dropping the two host-key arms from OnVaultPropertyChanged, caught by TheHostKeyDecision_IsAnnouncedToTheWindowWhenItArrivesAndWhenItGoes. That last one is the first test in this repository to watch PropertyChanged, and it is worth being the first: every other assertion about the flag reads it directly, and a direct read passes with the subscription deleted — while the card would never go away. The two layout tests moved with the prompts, from the hosts screen to the card. manual-checks gains 7.4a for the occlusion, 7.4b for getting out of a refusal and 11.7a for the hand-typed case, none of which a test can see; 1.5 and 7.4 are corrected rather than left describing a window that no longer moves. ONE ROUGH EDGE, DELIBERATELY LEFT. On the desktop, refusing a first contact whose tab was the only one leaves the terminal surface with no tabs — a blank rectangle under the strip's "no terminals open · press + or Ctrl+K", which is the one sentence near that rectangle Avalonia can draw. The alternative was falling back to the page, and on the phone that means the host list, which is the bug this commit is about. A desktop connect page would close it properly. |
||
|
|
780f4bf892 |
Merge branch 'main' into the group's move and its deletion question
Main took the group's EDIT and DELETE off the GROUPS heading while this branch was adding a MOVE beside them, so the conflict was about the same six pixels from both directions. Main's answer wins outright, and it is the better one for the reason its own message gives: a button beside a heading has no card under a pointer to mean, and had to work its subject out from the selection or from the trail. Moving a group had that problem worst of all — the thing it takes with it is everything on the shelf, and "which shelf" is not a question a button there could answer plainly. So the MOVE button is gone and the menu entry it was drawn beside is the whole of it. That entry was already in this branch, above the separator DELETE sits below, and it needed no change: the card menu selects whatever was right-clicked before it runs anything, which is exactly the aiming a group move wants. Three things went with the button. ShowsGroupActions, which main deleted because hiding buttons was all it did, and which this branch had extended to hide them for the move panel as well. CanMoveGroupTarget, which existed to answer whether that button was worth drawing — CanMoveSelectedHost stays, because the phone really does leave the host's MOVE out rather than offer a refusal, and a menu whose entries came and went would be a menu whose items move. And the two test assertions that read them, which were describing the button rather than the behaviour; what they were guarding is that the two panels never share the moment, and IsConfirmingGroupDeletion says that directly. The move panel and the deletion question both keep their place under the heading, which is where the buttons were and is now simply where that section puts things. They still exclude each other, by disarming rather than by a visibility flag: MoveGroup clears a pending deletion and DeleteGroup folds the move panel away. Manual checks 3.3 was rewritten by main for the menu and by this branch for the tick, and now says both; 3.3a is new and walks a two-level shelf across a vault boundary, which is the half of this feature no headless test can watch land. |
||
|
|
a86731ee08 |
Move the shelf as well as what is on it, and ask what a deletion takes
Two things about a group, and they turn out to be the same argument twice. **A group can be moved to another vault, and it takes everything on it.** The host's move shipped last week and stopped one level too low: moving twenty machines into a shared vault meant twenty trips through a menu, and each one arrived stripped of the group it had been filed under, so the shelf had to be rebuilt by hand on the other side. Moving the shelf is what people were attempting. MOVE sits between EDIT and DELETE over the group cards, and on the card's own menu above the separator DELETE is below — the same place, and the same reasoning, as the host pane's ⋯ entry. **The whole subtree goes, and taking less was never coherent.** A group's children are items of the vault it is leaving, so a parent moved alone leaves them naming a tombstone and they surface as roots in the vault the user has just emptied: half a shelf here and half there, from one gesture that said "move this". The hosts are the same argument and are the half the request was about. The groups go first, top down, and the hosts last. Each item is re-sealed under the destination's key and takes a new id — VaultItemRepository.MoveAsync, which HostGroupRepository now exposes — so nothing pointing at a group can be written until that group has landed and its new id is known, and a child's parent must already be over there. What an interruption leaves is therefore hosts still in the vault they started in, under UNGROUPED: visible, and re-movable. The reverse order would leave hosts in the destination filed under nothing. The parent stays behind and the tags are dropped, which is the host move's rule one level up: both are items of the vault being left, so a reference carried across would resolve on the machine that moved it and dangle for everybody else in the destination. The moved group arrives at the top level, and the panel says so before the press rather than the status line saying it after. Keys and passwords are kept — those genuinely resolve across vaults, and clearing them would take a working host and make one that cannot connect — and any now outside the destination is named, because that is precisely what the other members of it will not be able to resolve. Refused as a whole where anything under the group was written by a newer client, rather than skipped item by item: a move that left behind what it could not re-encode would file some of the shelf in one vault and the rest in the other, which is the state this exists to prevent. Refused with a host editor open, as the drop gesture is, because it rewrites hosts. And the walk carries a visited set, for the reason every walk over this tree does: a group that is its own parent — which two offline clients can build and no editor was ever shown — would otherwise be appended to the move list for as long as there was memory. **Deleting a group now asks what should become of the hosts under it, and that reverses a decision this repository had written down.** The deletion did not touch them: the reference was left dangling, the list resolved it to nothing, and the machines turned up under UNGROUPED. That was right for one of the two things people delete a group for and wrong for the other — a heading being tidied away should leave its machines alone, and a project that has been decommissioned is a shelf and everything on it — and nothing in the code can tell which of the two it is looking at. So it is asked. A tick rather than a pair of options, because the two answers are not equally weighted: keeping the hosts is recoverable and deleting them is not, so the safe answer is the one that needs no decision. It is off on every question, including the one that disarms it, or a tick left standing would destroy the next group's machines on the strength of a decision about the last one's. Once the deletion knows which hosts it means, leaving them naming something that has gone is a state kept for no reason, so the unticked answer writes too: N hosts with the reference cleared, where the ticked one writes N tombstones. That is the N writes HostGroupRepository refuses to hide behind a DeleteAsync overload, made where somebody asked for them and where the count is on screen first. The nested groups take the deleted group's place in the tree rather than being orphaned to the top level. A read-only host is skipped, counted and named, because unfiling it would re-encode a payload this build cannot represent — and the cost of skipping is a dangling id, which every reader here already survives. **Both are the desktop's alone**, and that is not an omission. The phone draws groups as headings in the host list and has never had a way to delete or move one; the two panels take the row of buttons over the group cards, and there is no such row on a 360dp screen to take. One test had to change its premise rather than its assertion. EditingAHostWhoseGroupIsGone built its dangling reference by deleting the group, which now unfiles instead — so it imports a host naming an id nothing resolves, which is what a group deleted on another machine actually looks like and is the only way that state still arises. The picker's placeholder is still needed and still covered. Four places said an item could not be moved between vaults. Two were about a group and were true when written; the other two were left stale by the host's move. All four now say what is true, including the design gaps document, where the chevron beside the vault name stays undrawn for the reason it already had. |
||
|
|
d02d103569 |
Take the group's EDIT and DELETE off the heading row
A pair of buttons sat at the end of the GROUPS heading, and the card's own right-click menu arrived later offering the same two things. Two controls for one job, and the buttons were the harder of the two to read: a button beside a heading has no card under a pointer to mean, so it had to work its subject out — the selected card, or failing that the group the trail ends with, which once a group is open is not a card on screen at all. The menu never has that problem, because opening it is what aims it. The menu is the whole of Edit and Delete on the desktop now. ShowsGroupActions went with the buttons, since hiding them was all it did. GroupTarget stays: the menu's two entries read it after the code-behind has selected whatever was right-clicked, and its fallback to the open group is what makes + NEW HOST open on the group somebody is standing in rather than on none. One case changes shape. Opening a group with nothing inside it folds the card grid away, so from in there nothing can be right-clicked — renaming that group means pressing the trail back one level, to where it has a card of its own. The buttons used to cover it through the fallback. The trail was already the way out of an empty group, and 3.2a says so now. The test that pressed EDIT through its binding is replaced by one holding that no button on the screen commands either of them, which is the failure worth catching: a button coming back is not a compile error, and it would draw itself in place, aimed at the group the trail ends with. What that test covered — a command that has to accept an empty parameter, and act on the card the pointer was on — the two menu tests beside it already do. |
||
|
|
742f65c204 | Merge branch 'claude/card-selection-state-sharing-f9348c' | ||
|
|
0258ec3029 |
Merge branch 'claude/groups-vault-sharing-e4b154'
# Conflicts: # src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs |
||
|
|
3f419cb19b |
Let one card be selected at a time
The hosts screen draws two grids, one above the other, and each is a ListBox with a selection of its own. Nothing joined them, so a group card and a host card could be lit at the same moment — two chosen things, under two pairs of buttons of which only one would act on whichever the eye had settled on. Both grids mark a selection the same way, so there was nothing on screen to say which of the two the next press belonged to. They share one mark now. Selecting a host clears the group and selecting a group clears the host, and that second one takes the detail pane down with it: a pane about one machine cannot go on standing beside a marked group, because nothing on it would be about what is selected. Losing a selection deliberately clears nothing. A null arrives whenever either list is rebuilt — every keystroke in the filter box and every background sync — and treating that as somebody deselecting would take the mark off a group card because a search emptied the grid beneath it. The reload needed the same guard for the same reason. It falls back to the first host when nothing is selected, which is what puts a target under CONNECT on a fresh unlock; with one mark between the two grids that fallback would have unselected a group nobody had touched, once a minute. It is skipped while a group holds the selection, and it moved into a method of its own because the comment saying why pushed ReloadHostsAsync past sixty lines. One consequence needed handling rather than accepting. The phone's only route into the group editor is a button on a heading in the host list, and it worked by selecting the group first — which under this rule takes the highlight off the machine somebody was about to connect to, on a screen that draws no group cards to say where it has gone. EditGroup takes the group as an argument now: the heading passes its own row, and the desktop's button beside the cards passes nothing and still means "the card that is selected". What the tests hold is the half that lives in the controls. Clearing the property has to reach the list that is drawing the card, and a selection nulled in the view model while the card stays highlighted is the exact failure this is about — so the rule is driven on the real screen, in both directions, against the ListBoxes' own SelectedItem. The desktop's EDIT is pressed through its binding for the same kind of reason: a command refusing the button's empty parameter would be a button that never fires, and nothing about the markup would say so. |
||
|
|
c39df3f51e |
Share the shelf as well as what is on it, and ask a group which vault
A group is where hosts are filed and what lends them a port, a username and a key, and until now it could only ever be made in the vault this machine files new items into. So sharing a vault shared the machines and not the arrangement: a colleague opened four hosts filed under a group they could read the name of and nothing else, and the group a teammate made had no card, no heading and no way to be corrected from the screen looking straight at the hosts inside it. Recorded as half shipped in docs/design-import-gaps.md, and this is the other half. The list stopped being the active vault's. It was narrow for two stated reasons — a row shown across vaults has to carry which vault it lives in, because rename and delete both need it, and two vaults may hold a "production" each, which a layout with one heading per group cannot tell apart — and both are now paid for rather than avoided. Every row carries its vault, the badge beside the name says which, and the two cards sit side by side saying what they are. The three shapes of the group read are now deliberately different sizes. The list is what a person looks at, so a hidden vault's groups leave it: a card that cannot be opened onto anything is worse than no card. The per-vault lists are what a picker offers, because a picker is always asking about one vault. The map is what a host's GroupId resolves through, and it stays widest of all — including over hidden vaults, since a group lends a port and hiding a vault must never change what one of its hosts dials. RebuildGroups is the one place hiding is applied, which is what keeps those answers apart. The editor asks which vault on the terms the host editor's picker set: while adding only, hidden where there is one writable vault, and never offered afterwards, because the two are encrypted under different keys and moving an item is a delete and a retype. Its parent picker is that vault's alone, for the reason the host editor's group picker is one level down — a parent in another vault is a level half the key holders cannot resolve, and their hosts would inherit from nothing. + NEW GROUP inside an open group departs from NewHost and takes that group's vault rather than the standing preference: a group made inside another is in its parent's vault by construction, and answering "inside PLATFORM" with a group elsewhere and no parent would drop the one thing the button said. Two smaller things follow from the cards spanning vaults. Dragging a host onto a group card in another vault is refused with both names, because the write it would make is exactly the id-nobody-can-resolve the host editor's picker was fixed to prevent, and treating it as "no group" would unfile a host somebody was plainly filing. And a group being renamed says its vault in the drawer's header, since the picker is not drawn for an existing one and renaming a colleague's shelf without being told whose it is is the edit most worth naming. The save target is a nullable field behind a property that falls back to the standing preference. The group name box is bound whether or not anything raised an editor over it — that is what the desktop's group bar was, and typing a name into it and pressing ADD is still a way to make a group, which would otherwise have written to no vault at all. 1575 tests pass, five more than before: a group filed into a shared vault is listed and renamed there, the editor's picker does not move the keychain screen's, the parent picker offers only its own vault, a cross-vault drop is refused, and hiding a vault takes the cards without changing what its hosts dial. |
||
|
|
be012585b3 |
Let a group be right-clicked, as a host already can
The host cards have had a menu since the grid replaced the sidebar; the group cards above them had a double-click and two buttons beside the heading, and nothing that named the card under the pointer. Open, Edit and Delete are on them now, drawn and aimed the same way. On the list rather than in the item template, for the reason the host grid's is: the three commands are the vault's, and a ContextMenu inside a DataTemplate has the row for its data context, so every binding in it would silently resolve to nothing. The code-behind selects whatever was right-clicked before the menu opens, and that is what makes one menu act on the card under the pointer rather than on whichever was selected before. **Cancelled over the space around the cards, and here that guard is doing more than the host grid's.** GroupTarget falls back to the group whose contents are on screen when no card is selected — the right answer for a pair of buttons beside the heading, which would otherwise have no subject the moment a group with nothing inside it is opened, and the wrong one for a menu that opened on a card. Without the guard, right-clicking the gap beside the cards would offer to delete the group the trail ends with: a question about something the user is not pointing at, in the one menu where the answer is a deletion. Only Open takes a parameter, and it has to. OpenGroupCommand's null is a real argument rather than a missing one — it is the trail's first crumb, ALL HOSTS — so an entry with no parameter would not open the card, it would leave the group the user right-clicked and go back to the top level. Two tests beside the two the host menu already had. What they hold that a build cannot is the CommandParameter binding: a path that resolves to nothing compiles and draws, and the entry would then quietly do the opposite of what it says. The popup itself is still the platform's, so manual-checks 7.9 gained the group half of the same check. |
||
|
|
2a56ae3efb |
Put a host inside the group it is filed under, rather than beside it
The group cards were headings with a navigation gesture bolted on. Opening one narrowed the grid to its hosts, but the level above already held every host in the keychain — so a card could only ever subtract, filing something changed nothing but a chip, and a keychain with forty machines was forty cards however carefully anybody had arranged them. The trail said ALL HOSTS and meant it. So the grid holds one level of the tree, the way a directory pane holds one directory. A host filed under a group is inside that group and is not also on the screen the group's card sits on; the outermost level is what nothing has been filed into. A group is a place now, and the cards, the trail and the drop target were already the vocabulary for saying so. The find box is the exception and had to be one. Typed into, it searches the open group and everything under it, which from the outermost level is every machine in the keychain. A box scoped to the level it was typed on would answer "no host matches that" about a host this keychain has got, and finding a machine without first remembering where it was filed is most of what the box is for. The accent chip on a card is what tells a searched-up host from one that lives at this level. Two things came free with the change and are handled rather than left. The phone would have broken. Its list binds SidebarRows, which was a projection over VisibleHosts, and it has no group cards and nowhere to open one into — so level-scoping would have left it drawing only the hosts nobody had filed. RebuildSidebarRows takes its own pass over the hosts now, narrowed by the vault switches and the box and by nothing else, which is the whole tree flattened under headings: exactly what it drew before. And anything created inside a group disappeared the moment it was saved. The host editor opens on the group the screen is about rather than only on a selected card, and + NEW GROUP defaults its parent to the group that is open. Deliberately not the selected card there: a highlighted card is what EDIT and DELETE are aimed at, and reading it as "and the next group goes inside it" would nest one because somebody had clicked something. The host editor takes both because it always took the selection, and its picker shows the answer before anything is written. Dropping a host on a group card now takes the card off the grid, and the selection goes with it — Connect, Edit and Delete all read that property and none of them should be aimed at a card that has left the screen. The status line is what says where it went, which is why the manual check now asks for it to be read. Coming back out through the editor lands the host on this level again, and there the selection survives. An empty grid has two more things it can say: that every host is filed away, which the level-at-a-time grid made reachable and which is not the same sentence as "there are none", and that nothing under this group matches what was typed — with ALL HOSTS named as the way to widen it. Not changed, and next door: a group card counts the hosts filed directly under it, so a group holding only subgroups reads "0 hosts". That was already true and is more visible now that its subgroups' hosts are not spilled onto the level above. 1522 tests pass. Four are new — the level rule and the phone's flat list asserted together, the box reaching two levels down, the all-filed sentence, and where a host and a group made inside a group end up. MovingAHostToAGroup was asserting the old outcome and is rewritten rather than adjusted: it held that the host stayed selected, and what it holds now is that the host leaves the level it came from. |
||
|
|
1f2607cc9a | Merge branch 'claude/group-double-click-breadcrumb-7bf3f8' | ||
|
|
01d54ba4fa |
Put the trail above the GROUPS label rather than under it
It was drawn as the transfers screen's is, under the heading of the list it belongs to. That is right there and wrong here: over there a path describes the one pane beneath it, and this one describes two. Opening a group narrows the host grid as well as the row of cards, so a trail sitting under GROUPS reads as one more fact about the groups while it is also the reason the grid at the bottom of the screen is showing eleven machines instead of forty. So it is the first thing in the section, and both headings below — GROUPS and HOSTS — belong to it. Markup only: the trail moved, the note on it says why it is not placed like its sibling, and the group buttons' note now points up rather than down. 85 layout tests pass, including the one that presses a rendered crumb and would have caught the $parent binding failing to resolve from its new place. |
||
|
|
fd6c4f5155 | Merge branch 'claude/host-detail-pane-design-e98621' | ||
|
|
e3dfe5c371 |
Make the host pane something you ask for, and draw it as cards
THE DRAWER USED TO ARRIVE WITH THE SELECTION. IsDrawerOpen read "a host is selected", so touching any card took 304 pixels off the grid — including every card arrowed past on the way to the one somebody wanted. Choosing among forty machines was charged the price of the pane for one of them. A pencil now appears on the card under the pointer and on the selected card, and that is what opens it; IsHostPaneOpen is the flag, and the grid's context menu gained Details… so the pane is reachable without a pointer, which a hover-only control is not. Once open it follows the selection rather than pinning the host it was opened on: a pane about one host beside a grid marking a different one is two answers to the same question. Losing the selection closes it and clears the flag, or a filter matching nothing would leave the pane armed to spring open again on the next card merely selected — which is the behaviour the pencil exists to remove. The pencil is drawn over the card rather than in a column of its own. A column would have cost the name 30 pixels of a 232-pixel tile, permanently, for a control that is only there while the pointer is; the dot and the pencil stack in the two corners of that edge instead. IsVisible and not opacity, because a button at zero opacity still takes the click and the card underneath does not. A HEADER, A BODY THAT SCROLLS, AND A FOOTER, which is the one structural change in the pane. The header names what the drawer is about and carries the two things true of every panel; the footer carries the one thing each panel is for — CONNECT, or SAVE, or the question about deleting. Only the middle scrolls, so the button somebody came here to press can no longer be below the fold, which CONNECT could be on a host with fifteen tags. That also widens what the layout harness certifies: it skips anything inside a ScrollViewer, and the control each panel exists to offer is now outside one. THE SAME THREE CARDS TWICE. Address, General, Connection — first as rows stating what the host is, then as boxes for changing it. The detail pane's rows are buttons that open the editor: the design draws every fact as a filled box, and rather than draw an input that refuses the pointer, pressing one leads to the same card with a real box in it. Nothing here saves as you type, and that is not timidity — saving validates the key-or-credential exclusion and writes one encrypted payload, so a box committing per keystroke would be a save per character and a half-typed hostname on the wire. Every value the pane prints is the resolved one, and says "inherited" beside it where a group supplied it. The number is the same either way and the edit is not: clearing a group's default moves every host that never overrode it. A HOST CARD IS TWO LINES AND NO CHIPS. The subtitle is now "ssh, root, pci, eu-west-1" — the transport, the resolved account, then every tag — replacing both the user@host:port line and the wrapped row of tag chips under it. The address went to the card's tooltip rather than nowhere: a card is read while scanning forty machines, where the name and the kind of machine are what is being looked for, and an address is what you read once you have found it. "ssh" is a constant today and is printed anyway, which is the one thing here that argues with this codebase's own rule about constants dressed as readings. It is the first item of a list whose other items vary, and a list beginning with the account on one card and a tag on the next has no shape to scan. The remark on HostRowViewModel.Summary says so rather than leaving it to be discovered. WHAT THE DESIGN DRAWS AND THIS PANE HAS NOT GOT: Share this host, Add Telnet, "SSH ID, Certificate, FIDO2", the backspace-key mapping row, the vault picker's chevron and Show more. Sharing is per vault and not per item, every session here is an SSH channel, there are no identity or certificate item types, nothing carries a terminal setting to the renderer, and an item cannot be moved between vaults at all. Six controls with nothing behind them, listed in docs/design-import-gaps.md with what ships instead, and none drawn disabled. The credentials row is marked with ◆ rather than the ⚿ the nav rail uses for the keychain. U+26BF is outside both faces this application substitutes for the design's fonts, so it lands on whatever the platform's fallback has; every other glyph in the pane is from Geometric Shapes, which both carry. |
||
|
|
0c0ac94312 |
Open a group by double-clicking it, and say where you are
ONE PRESS WAS DOING TWO JOBS. A group card was the only place a group could be selected — it is what EDIT and DELETE aim at — and it was also the control that narrowed the grid to that group. So there was no way to name a group in order to rename it without every host outside it leaving the screen at the same moment, and no way back except a SHOW ALL button that appeared beside the heading. Two gestures instead. A click selects and does nothing else; a double-click opens, which is what the host cards below already do to get a shell and what the transfers screen's directories already do to go inside one. The grid now has one vocabulary rather than one per list. The gesture is wired in the code-behind beside the host one, and guarded the same way: a double-click on the space around the cards must not open whichever group happened to be selected. THE CARDS ARE ONE LEVEL NOW, not every group in the keychain. Groups nest, and drawing all of them flat was the only honest thing to do while a card was a filter — a filter nobody can see is a filter nobody can turn off. Once opening one became navigation the cards became its contents, and VisibleGroups is that level beside Groups the way VisibleHosts sits beside Hosts. Groups itself is untouched: it is what every lookup reads and what the phone's headings are built from, and the phone binds none of the new members. Which is what the trail is for. A level with no name and no way out is a grid that has quietly hidden things, so a breadcrumb sits above the cards — drawn exactly as the transfers screen draws a directory path, same flat crumbs and same separator, because it is the same control answering the same question and a window with two breadcrumbs that look different has two ideas of what a path is. The first crumb is always there and always goes back to every host, which is what SHOW ALL was; that button went with it, because a control that only says "stop" beside a trail that says where you are is a second control for one job, and this one also gets you back one level rather than all the way. EDIT AND DELETE AIM AT GroupTarget: the selected card, or the open group when no card is selected. Without the fallback a group with nothing inside it could be opened and then never edited, because opening a group is exactly what takes its own card off the screen. It is also what a file manager does — act on the selection, and on the current folder when there is none — and the pair is now hidden with nothing to act on rather than shown doing nothing. A DANGLING PARENT AND A CYCLE BOTH HAD TO END UP SOMEWHERE REACHABLE. Neither is prevented: a parent id may point at a group deleted on another machine, and two clients can each re-parent A under B and B under A while offline, which no merge can see because the pointer is inside the payload. EffectiveParents promotes both to the outermost level, which is the same degradation the resolver's visited set produces for inheritance. The repair for either is the group's own editor and the editor is opened from its card, so a group drawn nowhere would be a broken state with the fix locked inside it. Three tests in HostGridTests: the split rule through the properties the cards bind, the pointer gesture itself in two windows so that "one press still only selects" is asserted separately from the pair, and a nested group opened, emptied of cards and walked back out of one level. The last presses the trail as it is actually rendered rather than calling the command, because a crumb reaches the vault through a $parent binding — a string that compiles whether or not it resolves, and would otherwise leave a row of buttons that do nothing. 85 layout tests and 234 shell-flow tests pass. Manual-checks 3.2, 7.6 and 7.7 follow the new gestures, and 3.2a and 3.4a are new: nesting, and the two states above, both of which need two machines and neither of which headless Avalonia can reach. |
||
|
|
f9d08b738c |
Take the group headings out of the host grid, and drop onto a group card
A first group turned the wrap of host cards into an accordion: SidebarRows interleaves a full-width fold-away heading — chevron, name, count — between the cards, and in a grid that reads as a dropdown somebody left open. The desktop grid binds VisibleHosts now. The headings and the fold stay for the phone, whose list has no room for the row of group cards the desktop draws above the grid. What a heading said, the card says: HostRowViewModel.GroupLabel, resolved once per reload like the tag names, drawn as an accent chip and absent from a host in no group — or in one that has been deleted, which is the same thing to look at. What a heading also was is the drop target, and that moves to the group cards. Two things go with it. A host dropped onto another host card used to be filed beside it, which was legible while a heading named the band of cards it landed in and is guesswork now; it is refused. And UNGROUPED was how a host was dragged back out of a group; the way out is the picker in its own editor, which is the one place "no group" can be said in words. A drag held at either edge of the grid scrolls it. Without that the gesture only works for whoever can see both ends of it: the group cards are the first thing in the scroller, the host may be the fortieth card down, and a drag cannot use the wheel. A step per drag event rather than a timer, so it follows the pointer and stops when it stops. The two heading-shaped tests are replaced. TheHostsGridHoldsCardsAndNoGroupHeadings asserts the grid's contents rather than only measuring them, because a heading that came back would lay out perfectly cleanly. TheGroupCardsAreWhatAcceptsADroppedHost raises a real DragOver over both kinds of card and checks the effects and the mark — the nearest a headless test gets to a gesture no headless test can synthesise. manual-checks 3.1-3.2 and 7.6-7.9 follow. |
||
|
|
1b7df47537 |
Merge main into the desktop redesign branch
Four conflicts. Three were two people adding to the same spot, and one was a real collision: main gave the connect bar a REMEMBER tick in the same pass that this branch took the connect bar off the hosts screen. REMEMBER is now in the drawer, beside the password box it qualifies. Nothing about the feature changed — RemembersConnectPassword, its refusal to fire until the remote has accepted the password, and the six tests over it are main's untouched — only where it is drawn. The move improves it slightly and it is worth saying why rather than claiming a merge was neutral: the bar had one row and had to fit the box, the tick, the authentication note and CONNECT along it, which is why the tick was a bare "REMEMBER" in tracked capitals. A column has room to put it under the box as a sentence, where it reads as a property of the password rather than as a fourth control in a row. MainWindowViewModel: both sides added members after ShowTerminal — the desktop's three fixed tabs here, the phone's connect menu on main. They do not interact, so both are kept, each under its own heading. TeamsScreen: main added the team's own RENAME, HAND OVER and ARCHIVE, a member's LastActive, the role a new member arrives as, and the KEY HOLDERS list. This branch had only bumped the file's font sizes a point. Resolved by taking main's file whole and re-running the bump over it, so the new controls join the scale rather than sitting a point below everything around them. README: both sides described a different head's third pass in the same paragraph. Both kept. Two things checked rather than assumed, because this branch moved the furniture the merged commits sit on. The chrome heights main's terminal work touched are the phone's, not the desktop's — 44, 42 and 24 are unchanged, so the layout harness's budget still describes the window. And main's keychain DELETE did not reach VaultScreen.axaml, whose header this branch rearranged, so the five buttons that overflowed at the larger type are still five. 2415 tests pass, up from 2369 by the 46 main brought. |
||
|
|
208443b932 |
Move SFTP and S3 into the tab strip, and the host list into a card grid
Four asks in one pass over the desktop head, and two of them are furniture moving rather than anything new. THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at its head and cannot be closed, and open terminals follow them. SFTP and S3 left the nav rail to get there, which is the one semantic change: they are the two destinations you stay in while something runs, and a rail entry is for somewhere you go and come back from. So the rail belongs to the Vaults tab and collapses with it, which also means SFTP, S3 and a terminal each get the full 1016 rather than the 826 a page gets. The tab is expressed as "a page, and not one of the two the strip took" rather than as a fourth ShellSurface. Both are still ShellScreen members and have to be — that is what they are on the phone, where they are two rows in a hub — so a surface each would have been a second way to say a thing Screen already says. IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged. What is new is one field: the page Vaults comes back to, because it is the one tab with sub-navigation and therefore the one that can return to the wrong place. That is not the hidden field ShellSurface argues against — that one would be a second copy of "which page"; this is a tab remembering its own. THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was choosing among forty machines and editing one of them at two-thirds width, and it was narrow so the editor beneath it could be a column at all. Cards took the first job at full width; a 304-pixel drawer took the second and collapses when nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the way back. The group editor moved into the drawer as well, which finally makes IsEditingGroup mean the same thing on both heads — it was the phone's alone, because the desktop's editor was a bar that was always on screen. AreHostsExpanded and ToggleHosts went with the control that used them. They folded the whole list away under one heading, an affordance that existed because the column was narrow. Folding a single group is a different thing and is still here. THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was never stated anywhere — a bare TextBlock took TextElement's default of 12 — so raising the scale meant naming it, on Window and on UserControl. The second selector is not redundancy: the layout harness hosts a UserControl in a window it builds itself, and without it the suite would measure every screen a point smaller than it ships, silently. A selector on TextBlock would have been the obvious way and is wrong, because a style setter beats an inherited value and would collapse every deliberate step back to one number. #E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing a hue with what it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1. Every step below moved with the top, so the intervals the design chose are kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone has both changes too. TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen rather than by measuring it, and both now covered. The tile was 232 and was first written as 248, from arithmetic that left out the scrolling stack's own margins. Every layout test passed — the harness asks whether a control is inside the window, never how many fit on a line — so the grid quietly became one column wide at exactly the minimum this application guarantees, which is the shape cards exist to avoid. TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns instead, and fails at 248. And a card's text ran past its own border, because a horizontal StackPanel measures children with infinite width: a TextBlock inside one never learns it is short of room, so TextTrimming never fires. Both card rows are grids with a star column that gives way and an Auto column that does not — a hostname with its tail cut is still the machine you were looking for, where a badge or the word naming an auth method is not. The keychain header changed shape for the same class of reason. It was Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing absorbing a change of width and five buttons fell off the right edge the moment the type grew. That is how GENERATE lost the word KEY once already. The summary sits in the star column and trims now, so the buttons always get their width. HostSidebarTests became HostGridTests and moved to the grid with the gestures it drives. docs/design-import-gaps.md gains a v3 section naming the five toolbar controls in the design with nothing behind them — a view-mode switch, a tag filter, a calendar, a share control and Serial — and manual-checks.md and the README follow the controls that moved. |
||
|
|
b86ff6158b |
Keep the password that just worked, so the host stops asking for it
The vault has held credentials and host bindings since they landed, and the only route from a typed password into one ran through two screens: add a password under Keychain, open the host, bind it. The password box's own tooltip instructed people to do exactly that by hand — which means typing the secret a second time, into a screen that has no idea which host it is for, while the screen that does have the password is the one being left behind. A tick beside the box does it in one step. What it produces is an ordinary CredentialSecret, named after the host, bound through CredentialId with AsksForPassword cleared beside it. That is the whole reason nothing else here had to change: it syncs, it merges, it appears in the keychain, it can be renamed and deleted, and it can be bound to the other nineteen machines that share the account. A password field on HostSecret would have been a payload schema bump, a fourth place a secret lives, and a copy per host to rotate and forget. It waits for a handshake the remote accepted, and that is not caution for its own sake. Binding on the keystroke would store whatever was in the box — including the typo about to be refused — and the host would then stop asking, leaving a machine nobody can connect to until they work out that the keychain is where the wrong password now lives. For the same reason the password is read from the credential as dialled rather than from the box, which stays typeable throughout a handshake. Off by default, because the typed box exists precisely for the passwords that should not be in a synchronised vault — a one-off on a machine somebody will never open again. The credential is written into the host's own vault rather than the active one: in the personal vault, bound to a team's host, it would be a binding every other member can see and none of them can resolve. One thing is given up knowingly. The confirmation naming where the password went is replaced a moment later by the auto-sync's own count, exactly as SaveHostAsync's "Saved 'x'" is; this follows that rather than becoming the one write that suppresses a sync line. The feedback that lasts is the row answering "credential" and the box disappearing — which is also the only way to store a password on the phone at all, since that head lists credentials but has never had an editor to create one in. |
||
|
|
b7335743d9 |
Make a host take what its group lends it, everywhere it is read
Step 4 of docs/adding-hosts-on-the-phone.md. The domain could resolve a host against its groups; nothing asked it to. This is the wiring, and it is mostly one change repeated: read the resolved host, not the stored one. TryBuildAuthentication and TryBuildConnectionRequest now take the resolved value beside the stored one, which is where group context was being lost. It is the only authentication resolution in the product — both heads and both transports come through it — so a host inheriting its binding would otherwise have been offered a password prompt on every screen at once. The credential-username fallback becomes three levels, and Complete still refuses an empty username, but now only after the chain has been walked; refusing before it would refuse exactly the hosts inheritance exists to serve. HostRowViewModel carries its ResolvedHost, resolved once when the list is built. Address, Authentication and Dialled read it, so a row cannot disagree with itself about what it dials — and MainWindowViewModel.Rank searches Address, so a host inheriting 2222 that displayed 22 would have been unfindable by the port it actually answers on. HostsBoundTo counts over the resolved binding, which is the difference between a warning and a silence: a key bound once on a group and inherited by twenty hosts named nobody, would have been deleted, and would then have refused all twenty at connect time. HostFields.From is answered by a refusal rather than by threading a group list through the sync engine. A relay host may not inherit its port. The reason is stronger than the convenience: a plaintext column is a derived duplicate the client supplies when it pushes *this* host, so an inherited port would make it depend on another item — editing a group would change what the relay dials for every host beneath it, except that nothing re-pushes those hosts, so the server would keep dialling the old port until each was next touched for some unrelated reason. A stale wire on the relay path connects the user to the wrong service. The editor distinguishes unset from explicit in both directions. An empty port box means "take the group's" and shows what that will be as a placeholder, following the group picker as it moves — a pre-filled 2222 would have been indistinguishable from one the user typed, and saving would have pinned it. The authentication picker gains a fourth entry, offered only to a host in a group, because for an ungrouped host it would behave exactly like the first. Which found a real defect while the tests were being written. Filing an ungrouped host into a group silently pinned it to a typed password: the picker had no "Inherit" entry when it opened, so it sat on "Password (ask each time)", and saving wrote that as a decision — the host would have been pinned to a prompt nobody asked for and the group's key would never have reached it. Two guards now: the picker is rebuilt when the group changes, and BuildHost writes AsksForPassword only for a host that had the alternative on offer. The group editor is here too, and the plan never assigned it a step. Without it no group can carry a default, so every line above would have been unreachable. It grows a parent picker that leaves out the group itself and everything beneath it — a courtesy rather than the guarantee, since a cycle assembled from two offline re-parents was never offered that list — and three defaults beside the name, each of which may be left empty because "lend nothing" is an answer. Tags are stored and not editable. TagIds merges, encodes and resolves; no screen can set one yet, and the editor carries the set through a save untouched so a client that can set them does not lose them to somebody editing a port. Eight new tests, and they dial. That is the point of them: a resolved value that never reaches SshConnectionRequest is a label, and every one of these failures would be silent — a host connecting to the wrong port, or being asked for a password it does not need, with nothing on screen admitting it. Verified by the whole suite: 1390 tests over nineteen projects, none failing. Both heads build. Nothing on the phone has changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2caedd93ff |
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64. |
||
|
|
d07b336868 |
Free the terminal from the Hosts screen, and fill the room it left
The WebView sat inside the Hosts grid, so navigating to Files or the keychain hid every open terminal and the strip that named them. A connection you had opened was invisible from four of the five screens. The window now has two surfaces rather than one: a nav rail that says which page you are on, and a terminal strip that is always there and switches the whole content area to a shell. Screen keeps meaning "which page" and never becomes a sixth kind of page, which is why this is two properties instead of one enum with a terminal member in it. Every screen lives inside one wrapper panel that collapses when a terminal is showing. That is not tidiness — the WebView hosts a Win32 child window that composites above everything Avalonia draws, so a screen left visible over its rectangle is a screen sliced in half, and this window has shipped that defect once already. One decision point, IsTerminalShowing, and a nested panel rather than five compound bindings nobody would remember to extend. The focus choreography is the part no test in this repo can see. Every reveal path now focuses in the same turn the WebView appeared, so all three of them post at DispatcherPriority.Loaded and let the native control re-push its bounds first. Going the other way had a real bug: the screen-changed branch called a bare Focus() where it had to release the keyboard from the native child, so switching from a terminal to Files silently ate the first keystrokes. Rare before this commit and the primary gesture after it. The tab strip grew a cross inside each tab, a plus that opens the quick-connect palette, and middle-click close. Nested buttons are correct here: Avalonia handles a left press on the cross and deliberately does not handle other buttons, which is exactly what lets middle-click bubble up from the cross as well as the tab. The test is PointerUpdateKind rather than IsMiddleButtonPressed, because the latter reports button state and is also true for a left press made while the middle button happens to be held. The handler is on the tab and not the strip, so the background closes nothing by construction. Plus opens the palette rather than a flyout, since a menu dropping into the WebView's rectangle may or may not composite above a child HWND and this repo does not make rendering claims it has not photographed. Everything a user reads now says keychain. The wire, the database and the cryptographic spec still say vault, deliberately: renaming those is a migration and a protocol change for a word. That split is written down rather than left to be rediscovered as an inconsistency. Four things that were squeezed into the keychain's category rail, or into nothing at all, now have screens. Pinned host keys get one, with fingerprints never truncated and a filter that matches them, because comparing what you have against what the operator published is the whole workflow; the approved date is read out of the item's UUIDv7 rather than added as a column, and says so, since it means first approval and not last use. Keys can be generated in the client, which needed the openssh-key-v1 container written by hand — there is no BCL or NSec helper, and the PKCS#8 route is unverified in the SSH library this uses. The armour carries no passphrase: encrypting it needs bcrypt_pbkdf, which is Blowfish with a swizzle, in a project whose crypto is otherwise entirely libsodium, for a protection the key's own remarks argue is redundant inside a vault. Generation fills the existing editor and stops, so SAVE stays the one thing that writes. ~/.ssh/config can be imported behind a preview that is ticked per row and writes nothing until the button; IdentityFile records the path and imports the key material only on an explicit opt-in, because reading somebody's private key into a vault is precisely the act this product exists to make deliberate. Match blocks and ProxyJump are reported rather than obeyed — one cannot be evaluated statically and the other has nothing behind it to route with, and a preview that implied otherwise would be worse than one that admits it. Files can be dragged in all four directions that are honestly available. Remote to Explorer does not ship and is not pretended to: the shell wants the bytes during the drop, which needs a virtual file and a native COM data object, outside what Avalonia offers. Note for the next person that Avalonia 12 replaced the drag model outright — DataObject and DataFormats are no-op stubs and IDataObject is not in the reference assembly, so every tutorial written for 11 does not compile here. Hosts can be grouped, flat and never nested. A parent id merged as a scalar lets two offline clients each re-parent A under B and B under A, producing a cycle inside an encrypted payload that no server can police and every reader would have to detect for ever. Membership lives in that payload rather than in the one plaintext concession ADR 0001 allows, whose test is that the relay cannot function without it — nothing on the server reads a group, so what plaintext would hand over is a clustering of the estate for nothing. The plaintext column reserved for it is dropped, provably always null, and the server now refuses a client that sends one; it was never populated, was copied on apply, and was not cleared on delete, so a group id would have outlived the host it described. Snippets insert through xterm rather than through the pump, because xterm is the only thing that knows whether the remote has bracketed paste on, and that is what makes a shell treat embedded newlines as text instead of as execute. The host process moves opaque bytes and never parses output, so it would have to guess, and guessing wrong runs every line. Running is off by default and the copy says the text goes into whatever is there — the terminal has no notion of being at a prompt, and may be in vi or at a password prompt with echo off, so the Enter the user presses themselves is the entire safety property. Connections and keychain changes are recorded as synced encrypted items, which is what makes them auditable by a team later and costs the server knowledge of connection rate and timing from row counts alone. ADR 0001 already concedes it cannot hide that class of metadata; the trade is now written into it rather than left implicit. A connection entry is written once, at close, which is what makes a synced log tractable: nothing to merge, one outbox row, no chance of colliding with itself. Live sessions come from memory, not from the log. The write is void by contract and posts to a bounded channel, because putting an encrypt-and-write on the teardown path of every session is how closing the application comes to take four seconds. A ticket opened before a lock still closes afterwards, since a shell outlives the vault. The activity log hooks the one generic repository every kind writes through, so it cannot miss a caller — which is also why the log kinds themselves declare they are not audited, or the first entry would write an entry about writing an entry. It records the names of the fields that changed and never their values; a log with an old password in it would be a plaintext credential store with no vault around it. Retention is 90 days or 5,000 entries, whichever bites first, pruned on the sync loop rather than on a second timer. That log traffic then broke the status line, which is worth recording because the fix is a shape and not a patch: background sync counted its own log rows as pushed items, so the quiet rule stopped being quiet and every action's message was overwritten a second later by a sync report. The report now separates log rows from user items and the rule reads the latter. S3 buckets appear as a remote in the file browser, behind the same interface an SFTP session implements, so the queue and both panes did not have to learn what they are talking to. Uploads go through a pipe, because the queue wants to write and the SDK wants to read; memory is then bounded by the part size instead of buffering a file to disk twice. Finally, the Windows device key store moved out of the session project, which was the one thing keeping it from being portable — everything else in it is platform-neutral, and a Windows CNG dependency in the middle of the vault code meant a second head could not reference it without dragging Windows along. The seam that made the move free was already there. docs/android-port.md is the audit behind that: what ports, what does not, in order of cost, the four decisions taken, and an inventory of every screen and state the interface has to carry, written so a design can be made from it directly. dotnet build, dotnet test and dotnet format --verify-no-changes are all clean: 1240 tests at zero warnings, including the end-to-end suite against real containers. The manual checks that headless Avalonia cannot make — the drag from Explorer, a generated key against a real host, twelve tabs at the minimum window width — are listed in docs/manual-checks.md and are still outstanding. |