Public Access
Merge branch 'claude/trust-connect-popup-56abec'
This commit is contained in:
@@ -947,7 +947,14 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
|
||||
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
|
||||
/// locked vault (the unlock card), the page area (every screen uses the full width), the quick-connect
|
||||
/// palette, and the phone's connect sheet.
|
||||
/// palette, the phone's connect sheet, and the host-key decision.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The host-key decision is the newest of them, and the one that made it a rule rather than a list.</b>
|
||||
/// It is drawn over whatever is on screen at the moment a handshake asks the question — including an open
|
||||
/// terminal, because a second connection can be being made while a first one is being typed into. Without
|
||||
/// this condition the two buttons that answer the most safety-critical question in the product would be
|
||||
/// sliced at the WebView's left edge and take no clicks. See <see cref="IsHostKeyDecisionShowing"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The sheet is here rather than in <see cref="IsTerminalSurface"/>, and the palette is not.</b> The
|
||||
@@ -984,7 +991,36 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal bool IsTerminalShowing =>
|
||||
IsTerminalSurface && !IsConnectSheetOpen && SelectedTab is { HasSession: true };
|
||||
IsTerminalSurface
|
||||
&& !IsConnectSheetOpen
|
||||
&& !IsHostKeyDecisionShowing
|
||||
&& SelectedTab is { HasSession: true };
|
||||
|
||||
/// <summary>
|
||||
/// Whether a host key is waiting to be judged, over whatever the user is looking at.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>It is asked of the shell rather than of a screen because the answer decides an occlusion, and
|
||||
/// because the question is no longer tied to a screen.</b> The prompt used to be a banner at the top of
|
||||
/// the hosts screen, and the shell navigated there before letting the vault raise it — which is how a
|
||||
/// machine typed into the phone's own connect box came to be judged on a list it is deliberately not on.
|
||||
/// Both heads now draw the decision over whichever surface was showing when the handshake stopped, so
|
||||
/// there is nowhere it can be asked from that it cannot be answered on, and nothing has to move.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Two states, one flag, and the views keep them apart: an unknown key is a decision with two buttons and
|
||||
/// a changed one is a refusal with no way forward. What they share is that both cover the rectangle the
|
||||
/// terminal would be in. See <c>HostKeyCard.axaml</c> on the desktop and <c>HostKeySheet.axaml</c> on the
|
||||
/// phone.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The transfers screen's own copy of this question is not here, and that is not an omission: file
|
||||
/// transfer's prompt is drawn inside that screen, which is a page, so it occludes nothing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal bool IsHostKeyDecisionShowing =>
|
||||
IsUnlocked && Vault is { HasPendingHostKey: true } or { HasHostKeyMismatch: true };
|
||||
|
||||
/// <summary>
|
||||
/// Whether the terminal half of the window is the half being shown, pane or no pane.
|
||||
@@ -1623,13 +1659,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
CloseSearch();
|
||||
|
||||
// The hosts page, because that is where this connection's questions get asked. An unknown or changed
|
||||
// host key is answered by a prompt drawn on that page and the palette opens from any screen, so
|
||||
// connecting from the files screen without this would leave the question behind the screen that asked
|
||||
// it. The surface does not stay here — the tab that appears for the attempt takes it — and it does not
|
||||
// need to: a refusal that needs an answer puts the page back, which is where this leaves the screen.
|
||||
Screen = ShellScreen.Hosts;
|
||||
Surface = ShellSurface.Page;
|
||||
// ◆ IT USED TO GO TO THE HOSTS PAGE FIRST, and the reason it did no longer exists. An unknown or
|
||||
// changed host key was answered by a prompt drawn on that page, and the palette opens from any screen,
|
||||
// so connecting from the files screen without the jump would have left the question behind the screen
|
||||
// that asked it. Both heads draw the decision over the surface now — see IsHostKeyDecisionShowing —
|
||||
// and the other thing this connection can say before it dials, a refusal on Vault.Status, is in the
|
||||
// status bar, which is a row of the window rather than part of a screen.
|
||||
//
|
||||
// So the palette leaves the user where they were, which is the whole point of a palette: Ctrl+K over a
|
||||
// transfer that is still running should not cost the transfer's screen.
|
||||
vault.SelectedHost = vault.Hosts.FirstOrDefault(host => host.EntityId == row.EntityId);
|
||||
|
||||
// Null, not the token. A [RelayCommand] over a method whose only parameter is a CancellationToken
|
||||
@@ -2815,12 +2853,27 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(HasRecentConnections));
|
||||
|
||||
RaiseSyncState();
|
||||
|
||||
// The host-key decision is a property of the vault, so swapping the vault out is one of the ways it
|
||||
// stops being on screen — locking with a question still up is the case, and it is reachable: a
|
||||
// handshake that stopped on an unknown key does not hold the window, so the lock button is live
|
||||
// behind the card asking about it.
|
||||
RaiseTerminalState();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// One property is watched rather than all of them: the titlebar's sync state is the vault's outbox
|
||||
/// depth, which lives on the vault, and re-raising the shell's two derived properties on every
|
||||
/// notification a busy vault produces would repaint the titlebar on every keystroke in an editor.
|
||||
/// <para>
|
||||
/// Named properties are watched rather than all of them: the titlebar's sync state is the vault's outbox
|
||||
/// depth, which lives on the vault, and re-raising the shell's derived properties on every notification a
|
||||
/// busy vault produces would repaint the titlebar on every keystroke in an editor.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The two host-key flags are here because the decision the vault raises collapses this shell's WebView —
|
||||
/// see <see cref="IsHostKeyDecisionShowing"/>. It is a property rather than an event because it is a
|
||||
/// state the vault is in and not a moment: it is entered by a refused handshake, left by either answer,
|
||||
/// and cleared outright by forgetting a pin from a host's editor, which is a third caller that has
|
||||
/// nothing to do with connecting.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnVaultPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
@@ -2829,6 +2882,14 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
{
|
||||
RaiseSyncState();
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
e.PropertyName, nameof(VaultViewModel.HasPendingHostKey), StringComparison.Ordinal)
|
||||
|| string.Equals(
|
||||
e.PropertyName, nameof(VaultViewModel.HasHostKeyMismatch), StringComparison.Ordinal))
|
||||
{
|
||||
RaiseTerminalState();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVaultHostsChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||
@@ -2913,11 +2974,27 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// Answers a connection that did not become a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Two outcomes, because there are two kinds of not-connecting. A refusal stays in the strip as a tab
|
||||
/// carrying its reason — connecting no longer holds the window, so the user may be three screens away by
|
||||
/// now, and the status line they are not looking at is not where a failure should end. A host key
|
||||
/// question is not a refusal: it is a prompt on the hosts screen, so the tab goes and the window is put
|
||||
/// back where the question is being asked.
|
||||
/// now, and the status line they are not looking at is not where a failure should end. A host key question
|
||||
/// is not a refusal: nothing has gone wrong and there is nothing to report, so the tab goes and the
|
||||
/// decision takes its place until it is answered.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It used to move the window, and that was the wrong half of the problem to solve.</b> The prompt was
|
||||
/// a banner on the hosts screen, so this navigated there — <c>Screen = Hosts</c>, <c>Surface = Page</c> —
|
||||
/// on the reasoning that a connection can be started from the palette on any screen and a question behind
|
||||
/// the screen somebody is looking at is a question nobody can answer. True, and answered the wrong way
|
||||
/// round: what it did to the one connection that has no host at all was to judge a machine typed into the
|
||||
/// phone's connect box on a list it is deliberately not on, after taking the box away.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both heads now draw the decision over whatever is showing, so nothing has to move and the palette's
|
||||
/// case is covered without a special one. What is left here is the tab, and the surface it was on stays as
|
||||
/// it is — for the phone that surface <em>is</em> the connect box, and it is what the user comes back to
|
||||
/// whichever way they answer.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnVaultConnectionFailed(object? sender, ConnectionFailedEventArgs e)
|
||||
{
|
||||
@@ -2945,11 +3022,11 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
: Tabs[Math.Clamp(index - 1, 0, Tabs.Count - 1)];
|
||||
}
|
||||
|
||||
// The screen the question is drawn on, and the page rather than a terminal. A connection can be
|
||||
// started from the palette on any screen, so without this the prompt would be behind whatever the
|
||||
// user was looking at, with the connection waiting on an answer they cannot reach.
|
||||
Screen = ShellScreen.Hosts;
|
||||
Surface = ShellSurface.Page;
|
||||
// Nothing else. The decision is drawn over the surface the user is on rather than on a screen they
|
||||
// have to be taken to — see the remark — and the neighbour selected above is what that surface shows
|
||||
// once the question is answered. Its WebView is collapsed while the card is up, which is the one
|
||||
// thing a card in that rectangle cannot do for itself; see IsHostKeyDecisionShowing.
|
||||
RaiseTerminalState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3344,6 +3421,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(IsTerminalSurface));
|
||||
OnPropertyChanged(nameof(IsTerminalShowing));
|
||||
OnPropertyChanged(nameof(IsConnectingShowing));
|
||||
OnPropertyChanged(nameof(IsHostKeyDecisionShowing));
|
||||
|
||||
// The tabs themselves, and not only the window's own flags. A tab that stayed lit after the user
|
||||
// navigated to preferences would be a second "you are here" mark pointing at a terminal that is not
|
||||
|
||||
@@ -9960,12 +9960,37 @@ internal sealed partial class VaultViewModel(
|
||||
await AutoSyncAsync(cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>Dismisses the trust prompt without pinning anything.</summary>
|
||||
/// <summary>
|
||||
/// Dismisses whichever host-key decision is showing, without pinning or forgetting anything.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// One command for both, because both are dismissals and the two states are mutually exclusive — which is
|
||||
/// what <c>TransfersViewModel.RejectHostKey</c> has always done for the same pair of cards. The mismatch
|
||||
/// has nothing else it may offer: withdrawing a pin is a deliberate act performed in the host's editor,
|
||||
/// away from the moment of connecting, and a button here would be "continue anyway" with two clicks
|
||||
/// instead of one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It used to clear only the pending key, which left the mismatch with no working exit.</b> That was
|
||||
/// invisible for as long as the refusal was a banner at the top of a screen — nothing was drawn over, so
|
||||
/// nothing was trapped, and the next connection attempt cleared it. It is not invisible now: both heads
|
||||
/// draw the refusal over the surface, and the phone's is an opaque full-screen panel whose one button runs
|
||||
/// this command. Leaving <see cref="HostKeyMismatch"/> set would have kept that panel up over every screen
|
||||
/// the user went to afterwards, including the host editor they were being sent to.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void RejectHostKey()
|
||||
{
|
||||
var wasOffered = PendingHostKey is not null;
|
||||
|
||||
PendingHostKey = null;
|
||||
Status = "The host key was not trusted, so nothing was connected.";
|
||||
HostKeyMismatch = null;
|
||||
|
||||
Status = wasOffered
|
||||
? "The host key was not trusted, so nothing was connected."
|
||||
: "Nothing was connected.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user