From 4300d917a852027539c84be48ade4df73bb0fa4d Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Fri, 31 Jul 2026 22:59:33 +0200 Subject: [PATCH] Stop making people wait for a handshake, and give the host list a pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/manual-checks.md | 111 ++++++ .../Views/PhoneShell.axaml | 19 +- .../Views/TerminalScreen.axaml | 38 +- src/DodoSSH.Client.App/App.axaml | 31 ++ .../Views/ConnectingCard.axaml | 77 ++++ .../Views/ConnectingCard.axaml.cs | 13 + .../Views/HostSidebar.axaml | 23 ++ .../Views/HostSidebar.axaml.cs | 286 ++++++++++++++- src/DodoSSH.Client.App/Views/MainWindow.axaml | 9 + .../Views/QuickConnect.axaml | 7 +- .../Views/QuickConnect.axaml.cs | 27 ++ .../Views/TerminalTabs.axaml | 24 +- .../ViewModels/MainWindowViewModel.cs | 287 +++++++++++++-- .../ViewModels/TerminalTabViewModel.cs | 144 +++++++- .../ViewModels/TransfersViewModel.cs | 48 ++- .../ViewModels/VaultViewModel.cs | 338 ++++++++++++++++-- .../HostSidebarTests.cs | 208 +++++++++++ .../QuickConnectTests.cs | 27 ++ .../ScreenLayoutTests.cs | 61 ++++ tests/DodoSSH.Client.App.Tests/FakeSsh.cs | 22 +- .../ShellFlowTests.cs | 315 +++++++++++++++- 21 files changed, 2003 insertions(+), 112 deletions(-) create mode 100644 src/DodoSSH.Client.App/Views/ConnectingCard.axaml create mode 100644 src/DodoSSH.Client.App/Views/ConnectingCard.axaml.cs create mode 100644 tests/DodoSSH.Client.App.Layout.Tests/HostSidebarTests.cs diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 1aa2226..97d69da 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -580,3 +580,114 @@ Repeat 6.1 and 6.3 with the endpoint blank, a real region, and path-style **off* **Pass:** it lists. This is the path that exercises `RegionEndpoint.GetBySystemName` and virtual-host addressing, neither of which MinIO covers. + +--- + +## Phase 7 — Connecting without waiting, and the host list's pointer + +Connecting no longer holds the vault while it happens: a tab appears at once and the handshake runs behind +it. The state machine around that is covered in `ShellFlowTests` — the tab, the card, the two kinds of +failure, two connections at once — and the card itself is measured. What is left needs a real window: a +native drag, a real menu popup, and a host that takes its time answering. + +### 7.1 The window stays usable while a connection is being made · **the point of the change** + +Connect to something that will take a while — an address that is routable but silent, so the handshake sits +there rather than failing at once (a firewalled IP is ideal; `10.255.255.1` will do). While it runs: click +around the host list, open KEYS, edit something, connect to a second host. + +**Pass:** everything works. The strip shows a tab named for the host with `connecting…` beside it, and the +terminal area shows the host, the address it is dialling, and the same word. + +**Failure means:** the connect has ended up back inside `RunAsync`'s busy gate, which is the one thing +`VaultViewModel.ConnectAsync` must never be put inside again. + +### 7.2 Navigating away during a connection unlights the tab + +While 7.1's connection is still running, click KEYS. + +**Pass:** the tab stays in the strip and stops being marked as the active one. No accent bar along its top +edge while a page is showing. + +**Failure means:** the tab is bound to `IsSelected` again rather than to `IsShowing`, and the strip is +claiming to be what the window is showing when it is not. + +### 7.3 A refusal ends up in the tab, not only in the status line + +Connect to a host with the wrong password, and navigate to PREFS before it answers. + +**Pass:** the tab goes grey and carries the refusal; selecting it shows the reason in the middle of the +window with a CLOSE TAB button. Closing it returns to the page underneath. + +**Failure means:** a connection that failed while the user was elsewhere has left no trace anywhere they +would look. + +### 7.4 An unknown host key still wins the window + +Connect to a host whose key is not yet approved. + +**Pass:** no tab is left behind, and the window is on HOSTS with the fingerprint prompt. Approving it +connects and gives the tab a pane. This is 1.5 with the tab rework underneath it. + +### 7.5 Giving up on a connection, and what arrives afterwards + +Start 7.1's slow connection and press GIVE UP (or the tab's cross) while it is still trying. + +**Pass:** the tab goes at once. If the host does eventually answer, a tab appears for the session that +opened — it is a real shell, and one running with nothing naming it would be worse than one that comes +back. + +### 7.6 Dragging a host into a group · **least covered, like all drag and drop** + +Make two groups and file a host into one. Drag a host row onto another group's heading; onto a host row +inside another group; and onto UNGROUPED. + +**Pass:** the row under the pointer washes accent while the pointer is over it, the cursor shows a move +rather than a refusal, and the drop files the host — it moves under that heading and the counts on both +headings change. Dropping onto its own group's heading is refused while still in the air. + +**Failure means:** headless Avalonia cannot synthesise a platform drag, so nothing about this gesture is +automated. The write it performs is: `MovingAHostToAGroup_FilesItAndLeavesItSelected`. + +### 7.7 A click still selects, and a double click still connects + +Click host rows; drag one a few pixels without releasing; double-click one. + +**Pass:** a click selects, a small movement starts nothing, and a double click connects. + +**Failure means:** the 5-pixel threshold in `HostSidebar.axaml.cs` is not doing its job — the same failure +as 2.16 on the other screen, and here it would make the list unusable. + +### 7.8 The highlight clears after a drag that goes nowhere + +Drag a host over a heading and release outside the list, or press Escape mid-drag. + +**Pass:** the wash goes away. + +### 7.9 The right-click menu acts on the row under the pointer + +With host A selected, right-click host B and choose Delete. + +**Pass:** the question names **B**. Then right-click a group heading. + +**Pass:** no menu opens at all, and the host selection has not moved. + +**Failure means:** a menu acting on the selection rather than on the row under the pointer deletes the wrong +machine. `HostSidebarTests` covers both halves headlessly, so this is a confirmation that a real popup +behaves as the headless one did. + +### 7.10 Clicking a host in the palette connects + +Ctrl+K, then click a result with the mouse rather than pressing Enter. + +**Pass:** the palette closes and the connection starts. + +### 7.11 A host or a bucket made now can be picked on FILES now + +With the files screen's picker open on an empty or short list, go to HOSTS and add a host — or to KEYS and +add a bucket — then come back. + +**Pass:** the new one is in the picker, without locking and unlocking. + +**Failure means:** the screen has gone back to copying the vault's lists once at unlock. Covered by +`TheTransfersScreen_FollowsTheVaultsHostList`; this is the version of it with a real picker in front of it. diff --git a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml index 9a645f7..3e23d2e 100644 --- a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml +++ b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml @@ -77,7 +77,13 @@ - + + - + + @@ -143,7 +156,7 @@ The terminal is a surface rather than a page — see ShellSurface — so this one does not go through ShowScreen. Its own command is on the shell. --> - - + + @@ -81,12 +83,33 @@ - + + + + + + + + @@ -96,8 +119,9 @@ path, or reach the previous command. Ctrl and Alt latch — pressed once they apply to the next key and then release, because holding a modifier while typing is not possible one-thumbed. --> + + BorderThickness="0,1,0,0" IsVisible="{Binding IsTerminalShowing}"> diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml index 321f146..985b678 100644 --- a/src/DodoSSH.Client.App/App.axaml +++ b/src/DodoSSH.Client.App/App.axaml @@ -452,6 +452,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + +