Let the session address be selected, and copied whole
ci / build and test (pull_request) Successful in 2m49s
ci / desktop nightly (pull_request) Skipped
ci / android head (pull_request) Successful in 3m10s
ci / api image (pull_request) Successful in 5s

The address at the head of the session column is the one place the desktop
head prints the endpoint it actually dialled, and it was a TextBlock — which
in Avalonia is a drawing of a string rather than a string. The next thing
people do with an endpoint is paste it somewhere: into a second client, into
a ticket, into the ssh command being written in the window beside this one.
Retyping is how a digit goes missing from an address that is mostly digits.

SelectableTextBlock, which is what the rest of this application already
reaches for wherever a string is on screen to be taken away — a fingerprint,
a public key, a recovery code, a snippet's command. Nothing else about the
row moves: same font, weight, colour and trimming, same tooltip, same close
button beside it. The sidebar is one control drawn on both surfaces, so SFTP
gets it for the same reason it gets everything else in that block.

The trimming and the selection do disagree at the long end, and the fix for
that is not to drop the trimming: the ellipsis is there because a long
account@host:port would otherwise push the close button off the edge of a
panel whose whole point is that it can be got rid of. A drag reaches only the
glyphs that were drawn, so a trimmed address has to be taken with select-all
— and select-all takes the whole thing, because the ellipsis is a drawing and
not a shorter value. That is the half a test can quietly lose, so it is the
half the new test asserts, on the longest address the suite has rather than
on one that fits and would pass either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
laurensandClaude Opus 5 committed 2026-08-24 15:18:49 +02:00
1 parent cfc2e3ae0c
commit 06369274dc
2 files changed
+42 -4

No files matched your search

@@ -70,12 +70,25 @@
The address, and the button that closes the column. Both on one row, and the address is the
trimming one: a long account@host:port is exactly the string that would otherwise push the
close button off the edge of a panel whose whole point is that it can be got rid of.
SelectableTextBlock rather than TextBlock, which is what the rest of this application already
reaches for whenever a string is on screen to be taken somewhere else — a fingerprint, a public
key, a recovery code. This is the one place the shell prints the endpoint it actually dialled,
and the endpoint is a thing people paste: into another client, into a ticket, into the ssh
command they are writing beside this window. Retyping it off the screen is how a digit gets
dropped from an address that is mostly digits.
The trimming and the selection do disagree at the long end, and the ToolTip is why the
disagreement is tolerable rather than a trap: a drag reaches only the glyphs that were drawn,
so an address wearing an ellipsis has to be taken with select-all — Ctrl+A, then Ctrl+C — and
the hover already shows what the full string is, so nobody is copying half of one without
having been told there is a half.
-->
<Grid ColumnDefinitions="*,Auto" Margin="8,0,0,0">
<TextBlock Grid.Column="0" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
FontSize="12.5" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Text="{Binding SessionAddress}" ToolTip.Tip="{Binding SessionAddress}" />
<SelectableTextBlock Grid.Column="0" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
FontSize="12.5" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Text="{Binding SessionAddress}" ToolTip.Tip="{Binding SessionAddress}" />
<Button Grid.Column="1" Classes="flat sidebargrip"
Command="{Binding ToggleSessionSidebarCommand}"
ToolTip.Tip="Close this column. The terminal takes the width, and the rail it leaves behind brings it back.">
@@ -128,6 +128,31 @@ public sealed class SessionSidebarTests : IAsyncLifetime
});
}
/// <remarks>
/// The address is the one string in this column somebody has to be able to take away with them, and the
/// claim under test is the half of that which the trimming could quietly break: an ellipsis is a drawing,
/// not a shorter string, so select-all reaches the whole address and not the part that happened to fit.
/// Asserted on the longest address this suite has — the case where the two could disagree — rather than
/// on one that fits, which would pass whether the trimming were render-only or not.
/// </remarks>
[Fact]
public async Task TheAddressSelectsAndCopiesWholeEvenWhereItIsTrimmed()
{
await OnTheSidebarAsync((sidebar, _) =>
{
// Single rather than First: this column prints exactly one string that is there to be copied, and
// a second selectable one appearing beside it is a question this test should ask rather than skip.
var address = sidebar.GetVisualDescendants().OfType<SelectableTextBlock>().Single();
address.Text.ShouldBe(LongAddress);
address.SelectAll();
address.SelectedText.ShouldBe(LongAddress);
address.CanCopy.ShouldBeTrue("Ctrl+C and the right-click Copy item are both gated on this");
});
}
/// <remarks>
/// Closed, the control is still drawn and is still the thing the window asks for a width — see
/// <c>MainWindow.axaml</c>'s own "Auto" column. What it must not be is nothing: a rail with the way back