Compare commits

...
2 Commits
Author SHA1 Message Date
laurensandClaude Opus 5 7ee2aacdc9 Put check 1.12 back in step with the splash it describes
ci / build and test (push) Successful in 2m57s
ci / android head (push) Successful in 3m38s
ci / api image (push) Successful in 22s
ci / desktop nightly (push) Successful in 1m9s
The progress bar went in "Take the frozen bar off the splash, and leave the line
that is true" and the manual check still told a person to watch it move, and then
to watch it stop. A check that names something not on the screen is worse than no
check: whoever runs it has to decide whether they are looking at a regression or
at a stale document, and the honest answer is not available from the page.

Says what the failure state looks like now — the line changes and nothing else on
the card is there to contradict it — and records where the bar went, so the next
reader is not left wondering whether it was lost by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:26:52 +02:00
laurensandClaude Opus 5 dec2cf164a Let the connecting card's host name and address be taken away
The card that fills the terminal's rectangle while a connection is being made
prints two lines saying which machine and at what address, and both were
TextBlocks — which in Avalonia is a drawing of a string rather than a string.

It is the one place the shell says what it is dialling *before* anything has
answered, and on a refusal it is the only copy of that on screen: there is no
prompt yet and there may never be one. That is exactly when the value is wanted
somewhere else — in a ticket, in a message to whoever owns the host, in the ssh
command being written in the window beside this one — and retyping it off the
screen is how a digit goes missing from an address that is mostly digits.

SelectableTextBlock, the same change the session column's own address got in
"Let the session address be selected, and copied whole" and for the same reason.
Nothing else about either line moves: same font, size, weight, colour, trimming
and centring. The refusal underneath them was already selectable, which made the
pair above it the odd ones out.

Both lines rather than only the name. They are two lines of one identifier, and a
pair where the first can be taken and the second cannot is a worse answer than
either.

Tooltips are new, and they are what makes the trimming safe rather than a trap.
A drag reaches only the glyphs that were drawn, so a name or an address wearing
an ellipsis has to be taken with select-all — which takes the whole value,
because the ellipsis is a drawing and not a shorter string. The hover says what
the whole value is, so nobody copies half of one without having been told there
is a half. Same argument, same shape, as the sidebar's.

The test asserts that half rather than the easy one, on values long enough to be
trimmed inside the card's 460 pixels; values that fit would pass whether the
trimming were render-only or not. Confirmed it bites by putting the two
TextBlocks back and watching it fail.

The phone draws the same two lines in the same state — TerminalScreen.axaml's
IsConnectingShowing block — and is deliberately left alone. Its captions are
styled by TextBlock.title and TextBlock.detail selectors in Phone.axaml, so the
same substitution there silently drops the styling until those rules gain
SelectableTextBlock siblings, which is a theme change rather than this one. Worth
doing; not worth smuggling in here.

Also: WindowDecorations on the splash instead of SystemDecorations, which
Avalonia 12 has deprecated in favour of it. That warning was mine, added two
commits ago; the main window's own is older and is not this change's to move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:25:43 +02:00
4 changed files with 116 additions and 13 deletions

No files matched your search

+8 -4
View File
@@ -256,8 +256,8 @@ registered in the capture phase, that no native menu is drawn once it is prevent
Close DodoSSH and start it again. Watch the first second.
**Pass:** a small panel — about 420 pixels wide, no titlebar — appears saying **DodoSSH** and *Starting…*
with a bar moving under it, and is then replaced by the main window. Measured on a debug build against a cold
**Pass:** a small panel — about 420 pixels wide, no titlebar — appears saying **DodoSSH** and *Starting…*,
and is then replaced by the main window. Measured on a debug build against a cold
profile: the panel at 668 ms, the window at 3.1 s. A release install is quicker and the shape is the same.
What must never come back is the old behaviour, which was nothing at all on screen for the whole of it.
@@ -270,8 +270,12 @@ over whatever you gave up and went back to.
*directory* where `cache.db` should be — `%LOCALAPPDATA%\DodoSSH.Nightly\cache.db` for the nightly — and
start the application.
**Pass:** the panel stays, the bar stops moving, and it reads *DodoSSH could not start:* followed by the
reason and *Press Escape to close.* Escape closes it. Remove the directory afterwards.
**Pass:** the panel stays and the line becomes *DodoSSH could not start:* followed by the reason and
*Press Escape to close.* Escape closes it. Remove the directory afterwards.
There is deliberately nothing else on the card to contradict that line — the progress bar this check used to
describe was taken off in "Take the frozen bar off the splash, and leave the line that is true", because an
indeterminate bar over a composition that had stopped was the one thing a splash must never do.
**Failure means:** a panel left saying *Starting…* is the main window having failed to show with nothing
catching it, which is the one outcome worse than the crash it replaced — `DodoSshApp.ComposeBehind` exists to
@@ -111,13 +111,33 @@
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="18" MaxWidth="460"
Margin="24">
<!--
Which machine, and at what address. Both selectable, for the reason the session column's own address
is — see SessionSidebar.axaml: a TextBlock in Avalonia is a drawing of a string rather than a string,
and this pair is the one place the shell says what it is dialling *before* anything has answered.
That is exactly when the value is wanted somewhere else: in a ticket, in a message to whoever owns
the host, in the ssh command being written in the window beside this one. It is also the moment it is
least available anywhere else, because there is no prompt yet and may never be one — a refused
connection leaves this card holding the only copy on screen.
Both rather than only the name. They are two lines of one identifier, and a pair where the first can
be taken and the second cannot is a worse answer than either.
The trimming stays, and the tooltips are what make it safe rather than a trap. A drag reaches only
the glyphs that were drawn, so a name or an address wearing an ellipsis has to be taken with
select-all — Ctrl+A, then Ctrl+C — which takes the whole value, because the ellipsis is a drawing and
not a shorter string. The hover says what the whole value is, so nobody copies half of one without
having been told there is a half. Same argument, same shape, as the sidebar's.
-->
<StackPanel Spacing="6" HorizontalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding SelectedTab.Label}" FontSize="16" FontWeight="Medium"
Foreground="{StaticResource Text}" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<TextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="11.5"
Foreground="{StaticResource TextFaint}" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<SelectableTextBlock Classes="mono" Text="{Binding SelectedTab.Label}" FontSize="16"
FontWeight="Medium" Foreground="{StaticResource Text}"
HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding SelectedTab.Label}" />
<SelectableTextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="11.5"
Foreground="{StaticResource TextFaint}" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding SelectedTab.Address}" />
</StackPanel>
<!--
@@ -10,7 +10,7 @@
CanResize="False"
WindowStartupLocation="CenterScreen"
Background="{StaticResource Chrome}"
SystemDecorations="None">
WindowDecorations="None">
<!--
── WHY THERE IS A SPLASH AT ALL ───────────────────────────────────────────────────────────────────────
@@ -30,8 +30,10 @@
look to find out whether anything is happening says nothing. It is a window, briefly, and behaves like
one.
SystemDecorations="None" rather than BorderOnly, which the main window uses: this cannot be resized, has
no titlebar of its own to drag by, and lives for about a second. What replaces the frame is the card's
WindowDecorations="None" rather than the BorderOnly the main window asks for: this cannot be resized, has
no titlebar of its own to drag by, and lives for about a second. (The main window still says
SystemDecorations, which Avalonia 12 has deprecated in favour of this property; that one is not this
change's to move.) What replaces the frame is the card's
own one-pixel border. Escape closes it only once there is nothing left to wait for — see the code-behind.
── SizeToContent, WHICH IS ABOUT THE FAILURE AND NOT ABOUT THE SPLASH ─────────────────────────────────
@@ -942,6 +942,49 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
await MeasureConnectingAsync(faults => faults.ShouldBeEmpty("with a refusal to explain"), tab);
}
/// <remarks>
/// <para>
/// The card is the one place the shell says what it is dialling <em>before</em> anything answers, and on a
/// refusal it is the only copy of that on screen — so both lines are there to be taken away, and a
/// <c>TextBlock</c> in Avalonia is a drawing of a string rather than a string. Same change, same argument,
/// as the session column's own address; see <c>SessionSidebarTests</c>.
/// </para>
/// <para>
/// Asserted on values long enough to be trimmed, which is the half that could quietly break: an ellipsis
/// is a drawing and not a shorter string, so select-all has to reach the whole name and the whole address
/// rather than the part that happened to fit inside the card's 460 pixels. Values that fit would pass
/// whether that were true or not.
/// </para>
/// </remarks>
[Fact]
public async Task TheConnectingCardsNameAndAddressSelectAndCopyWhole()
{
const string label = "customer-production-database-01.eu-west-1.internal.example.company";
const string address = "a-very-long-deploy-account@db-primary.eu-west-1.internal.example:22022";
await OnTheConnectingCardAsync(
card =>
{
// Found by their text rather than by position: the order is the visual tree's, and a third
// selectable block is already in there — the refusal reason, hidden while this tab is only
// connecting. Single rather than First, so a duplicate would be a question rather than a
// silent pick.
var takeable = card.GetVisualDescendants().OfType<SelectableTextBlock>().ToList();
var name = takeable.Single(block => block.Text == label);
var endpoint = takeable.Single(block => block.Text == address);
name.SelectAll();
name.SelectedText.ShouldBe(label, "the ellipsis is a drawing, not a shorter string");
name.CanCopy.ShouldBeTrue("Ctrl+C and the right-click Copy item are both gated on this");
endpoint.SelectAll();
endpoint.SelectedText.ShouldBe(address, "the ellipsis is a drawing, not a shorter string");
endpoint.CanCopy.ShouldBeTrue("Ctrl+C and the right-click Copy item are both gated on this");
},
new TerminalTabViewModel(label, address));
}
// ---- The logs screen ----
[Fact]
@@ -2043,6 +2086,40 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
},
Token);
/// <summary>Lays the connecting card out and hands it over, rather than measuring it.</summary>
/// <remarks>
/// The same rectangle <see cref="MeasureConnectingAsync"/> uses and for the same reasons — see its
/// remark. Separate because the claim is about a control's own state rather than about where anything was
/// arranged, and threading an assertion over the card through a method whose parameter is a fault list
/// would make both callers harder to read than two methods are.
/// </remarks>
private Task OnTheConnectingCardAsync(Action<ConnectingCard> body, TerminalTabViewModel tab) =>
LayoutHarness.OnTheUiThreadAsync(
() =>
{
shell.State = ShellState.Unlocked;
shell.Tabs.Clear();
shell.Tabs.Add(tab);
shell.SelectedTab = tab;
var card = new ConnectingCard { DataContext = shell };
var window = LayoutHarness.HostAtMinimumSize(
card,
LayoutHarness.SessionScreenWidth(sidebarVisible: true),
LayoutHarness.SessionScreenHeight);
try
{
body(card);
}
finally
{
window.Close();
}
},
Token);
/// <summary>Lays the host-key decision out in the rectangle it is drawn over.</summary>
/// <remarks>
/// <para>