Audit follow-ups: keyboard, wording, performance, transfer, sync, retry and relay fixes #22

Merged
jaap-jan merged 21 commits from claude/audit-followups into main 2026-10-02 12:59:56 +00:00
5 changed files with 118 additions and 22 deletions
Showing only changes of commit 3ccafdd53b - Show all commits

No files matched your search

@@ -256,7 +256,8 @@
</Border> </Border>
<!-- ============ The table ============ --> <!-- ============ The table ============ -->
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*" Background="{StaticResource Pane}"> <Grid Grid.Column="1" RowDefinitions="Auto,Auto,*" Background="{StaticResource Pane}"
Grid.IsSharedSizeScope="True">
<Border Grid.Row="0" Padding="16,0" Height="44" <Border Grid.Row="0" Padding="16,0" Height="44"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"> BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
@@ -275,12 +276,24 @@
item this application stores carries a timestamp; see the file-level remark above and item this application stores carries a timestamp; see the file-level remark above and
docs/design-import-gaps.md. docs/design-import-gaps.md.
--> -->
<Grid Grid.Row="1" ColumnDefinitions="2,3*,1.1*,0.9*,1.2*" Margin="0,10,16,6" <!--
VAULT is sized to what is in it, and shared between this heading and every row so the two agree:
with one vault no row names one, and a fixed column was a heading over a stripe of nothing.
-->
<Grid Grid.Row="1" Margin="0,10,16,6"
IsVisible="{Binding HasVaultItems}"> IsVisible="{Binding HasVaultItems}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="1.1*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="KeychainVault" />
<ColumnDefinition Width="1.2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1" <TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
Margin="12,0,8,0" /> Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="3" Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1"
Margin="0,0,16,0" IsVisible="{Binding ShowsVaultColumn}" />
<TextBlock Grid.Column="4" Classes="label" Text="USED BY" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="4" Classes="label" Text="USED BY" FontSize="9.5" LetterSpacing="1" />
</Grid> </Grid>
@@ -290,7 +303,14 @@
Margin="8,0,8,10"> Margin="8,0,8,10">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultItemRowViewModel"> <DataTemplate x:DataType="vm:VaultItemRowViewModel">
<Grid ColumnDefinitions="2,3*,1.1*,0.9*,1.2*" Height="40" Margin="6,0,10,0"> <Grid Height="40" Margin="6,0,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="1.1*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="KeychainVault" />
<ColumnDefinition Width="1.2*" />
</Grid.ColumnDefinitions>
<!-- <!--
A Grid rather than a horizontal StackPanel: the StackPanel measured the name with infinite A Grid rather than a horizontal StackPanel: the StackPanel measured the name with infinite
width, so TextTrimming never fired and a long name printed over the TYPE and USED BY width, so TextTrimming never fired and a long name printed over the TYPE and USED BY
@@ -307,7 +327,7 @@
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10.5" <TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10.5"
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" Foreground="{StaticResource TextGhost}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<Border Grid.Column="3" Classes="chip" HorizontalAlignment="Left" <Border Grid.Column="3" Classes="chip" HorizontalAlignment="Left" Margin="0,0,16,0"
VerticalAlignment="Center" IsVisible="{Binding HasVaultBadge}"> VerticalAlignment="Center" IsVisible="{Binding HasVaultBadge}">
<TextBlock Text="{Binding VaultBadge}" FontSize="10" /> <TextBlock Text="{Binding VaultBadge}" FontSize="10" />
</Border> </Border>
@@ -81,11 +81,15 @@
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel> <StackPanel>
<Grid ColumnDefinitions="1.4*,*,*,Auto" Margin="20,14,20,10"> <!--
No VAULT column, although the design draws one: TagRowViewModel does not carry which vault a tag
lives in (tags are read from the active vault alone; see VaultViewModel.NewTag's own remark), so
the column was a heading over nothing on every row.
-->
<Grid ColumnDefinitions="1.4*,2*,Auto" Margin="20,14,20,10">
<TextBlock Grid.Column="0" Classes="label" Text="TAG" /> <TextBlock Grid.Column="0" Classes="label" Text="TAG" />
<TextBlock Grid.Column="1" Classes="label" Text="USED BY" /> <TextBlock Grid.Column="1" Classes="label" Text="USED BY" />
<TextBlock Grid.Column="2" Classes="label" Text="VAULT" /> <TextBlock Grid.Column="2" Classes="label" Text=" " />
<TextBlock Grid.Column="3" Classes="label" Text=" " />
</Grid> </Grid>
<ItemsControl ItemsSource="{Binding Vault.Tags}"> <ItemsControl ItemsSource="{Binding Vault.Tags}">
@@ -97,7 +101,7 @@
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TagRowViewModel"> <DataTemplate x:DataType="vm:TagRowViewModel">
<Border Classes="settingsrow last" Padding="20,10"> <Border Classes="settingsrow last" Padding="20,10">
<Grid ColumnDefinitions="1.4*,*,*,Auto"> <Grid ColumnDefinitions="1.4*,2*,Auto">
<Border Grid.Column="0" Classes="chip" HorizontalAlignment="Left" VerticalAlignment="Center"> <Border Grid.Column="0" Classes="chip" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding Label}" /> <TextBlock Text="{Binding Label}" />
@@ -107,14 +111,7 @@
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
Text="{Binding Description}" /> Text="{Binding Description}" />
<!-- <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
No vault chip here — unlike a group, TagRowViewModel does not carry which vault it lives
in (tags are read from the active vault alone; see VaultViewModel.NewTag's own remark),
so there is no second vault this column could honestly name.
-->
<TextBlock Grid.Column="2" />
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Button Classes="paneicon" Width="30" Height="30" FontSize="13" <Button Classes="paneicon" Width="30" Height="30" FontSize="13"
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.EditTagRowCommand}" Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.EditTagRowCommand}"
CommandParameter="{Binding}" ToolTip.Tip="Rename this tag."> CommandParameter="{Binding}" ToolTip.Tip="Rename this tag.">
@@ -167,8 +167,12 @@
TextAlignment="Center" MaxWidth="360" TextAlignment="Center" MaxWidth="360"
IsVisible="{Binding !HasVisible}" /> IsVisible="{Binding !HasVisible}" />
<!--
Only with a snippet chosen. Three greyed buttons under a list nobody has clicked in read as
something broken rather than something waiting, and the pane on the right already says what to do.
-->
<Border Grid.Row="1" Padding="16,10" BorderBrush="{StaticResource BorderSubtle}" <Border Grid.Row="1" Padding="16,10" BorderBrush="{StaticResource BorderSubtle}"
BorderThickness="0,1,0,0"> BorderThickness="0,1,0,0" IsVisible="{Binding HasSelection}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="ghost" Content="Edit" Command="{Binding EditCommand}" <Button Classes="ghost" Content="Edit" Command="{Binding EditCommand}"
IsEnabled="{Binding HasSelection}" /> IsEnabled="{Binding HasSelection}" />
@@ -2404,6 +2404,14 @@ internal sealed partial class VaultViewModel(
internal bool HasVaultItems => VaultItems.Count > 0; internal bool HasVaultItems => VaultItems.Count > 0;
/// <summary>Whether any row in the keychain table names a vault, which is whether its VAULT column has a heading.</summary>
/// <remarks>
/// Read off the rows rather than off how many vaults there are, because a row's badge is what the column
/// holds: the two agree today — see <see cref="VaultItemRowViewModel.VaultBadge"/> — and if they ever stop
/// agreeing, a heading over nothing is the thing this exists to avoid.
/// </remarks>
internal bool ShowsVaultColumn => VaultItems.Any(row => row.HasVaultBadge);
internal bool HasSelectedVaultItem => SelectedVaultItem is not null; internal bool HasSelectedVaultItem => SelectedVaultItem is not null;
/// <summary> /// <summary>
@@ -13457,6 +13465,16 @@ internal sealed partial class VaultViewModel(
}; };
} }
/// <summary>Re-reads what the keychain table says about its rows as a whole.</summary>
private void RaiseVaultItemFacts()
{
OnPropertyChanged(nameof(SectionSummary));
OnPropertyChanged(nameof(HasVaultItems));
OnPropertyChanged(nameof(ShowsVaultColumn));
OnPropertyChanged(nameof(TotalItemCount));
OnPropertyChanged(nameof(EmptySectionMessage));
}
/// <summary>── v5b ── Narrows <see cref="VaultItems"/> to rows <see cref="ItemFilter"/> matches.</summary> /// <summary>── v5b ── Narrows <see cref="VaultItems"/> to rows <see cref="ItemFilter"/> matches.</summary>
/// <remarks> /// <remarks>
/// Its own method for the reason <see cref="AddTagRows"/> is out of <see cref="RebuildVaultItems"/> — /// Its own method for the reason <see cref="AddTagRows"/> is out of <see cref="RebuildVaultItems"/> —
@@ -13552,10 +13570,7 @@ internal sealed partial class VaultViewModel(
// sync every minute would otherwise move the detail pane out from under whoever was reading it. // sync every minute would otherwise move the detail pane out from under whoever was reading it.
SelectedVaultItem = VaultItems.FirstOrDefault(row => row.EntityId == selectedId); SelectedVaultItem = VaultItems.FirstOrDefault(row => row.EntityId == selectedId);
OnPropertyChanged(nameof(SectionSummary)); RaiseVaultItemFacts();
OnPropertyChanged(nameof(HasVaultItems));
OnPropertyChanged(nameof(TotalItemCount));
OnPropertyChanged(nameof(EmptySectionMessage));
// A reload replaces every row object, and the selection is restored by id — so the setter above may // A reload replaces every row object, and the selection is restored by id — so the setter above may
// not have fired even though the row this answers about is a different instance. Asked again here, // not have fired even though the row this answers about is a different instance. Asked again here,
@@ -2296,6 +2296,66 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
}, },
Token); Token);
/// <remarks>
/// Three greyed buttons under a list nobody has clicked in read as something broken rather than something
/// waiting. The row appears with a snippet to act on, and goes with it.
/// </remarks>
[Fact]
public async Task TheSnippetButtonsAppearWithASnippetToActOn()
{
await SeedSnippetsAsync();
var snippets = NewSnippetsScreen();
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var screen = new SnippetsScreen { DataContext = snippets };
var window = LayoutHarness.HostAtMinimumSize(
screen, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
try
{
var edit = screen.GetVisualDescendants().OfType<Button>()
.Single(button => button.Content is "Edit");
snippets.Selected = null;
LayoutHarness.Settle(window, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
edit.IsEffectivelyVisible.ShouldBeFalse("nothing is chosen for it to edit");
snippets.Selected = snippets.Visible.First();
LayoutHarness.Settle(window, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
edit.IsEffectivelyVisible.ShouldBeTrue();
}
finally
{
window.Close();
}
},
Token);
}
/// <remarks>
/// With one vault no row names one, and a fixed VAULT column was a heading over a stripe of nothing
/// between TYPE and USED BY. It is sized to what its rows hold now, and its heading goes with them.
/// </remarks>
[Fact]
public async Task TheKeychainsVaultColumn_TakesNoRoomWithOneVault()
{
await OnTheVaultAsync((screen, _) =>
{
vault.HasVaultItems.ShouldBeTrue("an empty table has no columns to judge");
vault.ShowsVaultColumn.ShouldBeFalse("this suite's keychain has one vault");
var heading = screen.GetVisualDescendants().OfType<TextBlock>()
.Single(block => block.Text is "VAULT");
heading.IsEffectivelyVisible.ShouldBeFalse();
heading.Parent.ShouldBeOfType<Grid>().ColumnDefinitions[3].ActualWidth.ShouldBe(0);
});
}
private SnippetsViewModel NewSnippetsScreen(InsertTarget? target = null) => private SnippetsViewModel NewSnippetsScreen(InsertTarget? target = null) =>
new( new(
vault, vault,