Public Access
Merge branch 'claude/gallant-brahmagupta-1f8244'
Writes down that locking the vault leaves shells running, and shows the count on the unlock screen rather than leaving it to be inferred. Conflict resolution: - ShellFlowTests' fixture keeps main's FakeSshConnectionFactory. The branch added an IdleSshConnectionFactory for exactly what main's fake already does — a shell that is open, silent and never closes on its own — so FakeSshConnections.cs is dropped rather than merged, leaving one fake SSH stack in the suite instead of two that would drift apart. - MainWindowViewModel and TerminalWorkspace: both sides added their own members, so both are kept. - TerminalWorkspaceTests was added by both branches, with the renderer gate on one side and session lifetime on the other. Merged into one class over one set of helpers; the gate tests now use FakeConnectionFactory rather than an NSubstitute stub, since the suite already has the fake. gallant's polling Timeout constant is PollTimeout, which no longer reads as the renderer's. - platform-flags.md keeps main's measured focus section and drops the short "nothing hands the terminal keyboard focus" entry the branch still carried, which that section supersedes. One genuine disagreement between the branches, left visible rather than flattened: this branch measured that a collapsed WebView cannot be typed into and attributed it to a hidden WS_CHILD window being ineligible for keyboard focus, while main's focus work measured Win32 focus still held by that hidden window and added a lock path that moves the keyboard off it. Both results stand; the mechanism sentence now defers to the focus entry, which makes the input barrier something the lock path maintains rather than something the platform guarantees. Full suite green, including the container-backed SSH tests.
This commit is contained in:
@@ -139,6 +139,22 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
[ObservableProperty]
|
||||
private VaultViewModel? vault;
|
||||
|
||||
/// <summary>
|
||||
/// Shells that were left running when the vault was locked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Refreshed by <see cref="LockAsync"/>, which is where the policy this reports is explained.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private int liveSessionCount;
|
||||
|
||||
internal bool HasLiveSessions => LiveSessionCount > 0;
|
||||
|
||||
/// <summary>The count as a sentence, because a bare number on a lock screen explains nothing.</summary>
|
||||
internal string LiveSessionSummary => LiveSessionCount == 1
|
||||
? "1 shell is still connected and still running."
|
||||
: $"{LiveSessionCount} shells are still connected and still running.";
|
||||
|
||||
/// <summary>Where the embedded browser should navigate.</summary>
|
||||
internal Uri TerminalPageUrl => workspace.PageUrl;
|
||||
|
||||
@@ -351,7 +367,34 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
}).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>Closes the vault and forgets every key it held.</summary>
|
||||
/// <summary>
|
||||
/// Closes the vault and forgets every key it held. Open shells keep running.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Lock is a vault operation, and deliberately not a disconnect.</b> The reason a person locks is
|
||||
/// that they are walking away from the machine, which is exactly the moment a long upgrade, build or
|
||||
/// transfer is most likely to be in flight — so killing every shell would make Lock a button that
|
||||
/// destroys work, and the predictable response is to stop using it and leave the vault open instead.
|
||||
/// The same argument decides it for the idle auto-lock this will grow: an unattended timeout that
|
||||
/// terminated a running job would be worse than the exposure it removes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>What "locked" therefore describes.</b> Disposing the vault zeroes the identity keys, the vault
|
||||
/// keys and the cache key, so nothing on disk can be read without the passphrase again. It says
|
||||
/// nothing about this machine's access to remote hosts: an SSH channel authenticated at connect time
|
||||
/// needs no vault key to keep running, and the credential it used was already spent. Locking cannot
|
||||
/// retroactively un-authorise a session any more than revocation can — the same honest limit the
|
||||
/// README records for a removed team member. So a locked DodoSSH still holds open, authenticated
|
||||
/// channels, and <see cref="LiveSessionCount"/> is shown on the unlock screen rather than left to be
|
||||
/// inferred from a terminal that the lock screen hides.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The count is a snapshot taken here. While locked it can only fall — opening a session needs the
|
||||
/// vault — so a stale value over-reports and never under-reports, which is the safe direction for a
|
||||
/// warning of this kind.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private async Task LockAsync()
|
||||
{
|
||||
@@ -361,6 +404,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
await open.DisposeAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
LiveSessionCount = workspace.LiveSessionCount;
|
||||
|
||||
State = ShellState.Locked;
|
||||
StatusMessage = "Locked.";
|
||||
}
|
||||
@@ -500,6 +545,12 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
private void OnVaultSessionOpened(object? sender, EventArgs e) =>
|
||||
TerminalSessionOpened?.Invoke(this, e);
|
||||
|
||||
partial void OnLiveSessionCountChanged(int value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasLiveSessions));
|
||||
OnPropertyChanged(nameof(LiveSessionSummary));
|
||||
}
|
||||
|
||||
partial void OnStateChanged(ShellState value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsStarting));
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
<Button Content="Sign in" Command="{Binding SignInCommand}"
|
||||
IsVisible="{Binding !IsOnline}" />
|
||||
<Button Content="Sync" Command="{Binding Vault.SyncCommand}" />
|
||||
<Button Content="Lock" Command="{Binding LockCommand}" />
|
||||
<!--
|
||||
The tooltip carries the policy to the point of action, because the button's name implies
|
||||
the opposite of what it does to a running shell.
|
||||
-->
|
||||
<Button Content="Lock" Command="{Binding LockCommand}"
|
||||
ToolTip.Tip="Closes the vault and forgets its keys. Open shells keep running and reappear when you unlock." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -319,6 +324,23 @@
|
||||
<TextBlock Classes="hint" Text="{Binding StatusMessage}" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="This works with no network: the salt and the wrapped key are already on this machine." />
|
||||
|
||||
<!--
|
||||
Stated here because the lock screen is what hides it. The terminal's WebView is collapsed
|
||||
while locked, so a shell left running is invisible as well as unstopped — and a screen
|
||||
saying "Unlock your vault" over a machine that still holds authenticated SSH channels is
|
||||
exactly the kind of half-truth this project writes down instead of implying. Visible only
|
||||
when there is something to disclose, so an ordinary launch stays quiet.
|
||||
-->
|
||||
<Border Background="#1b2432" CornerRadius="4" Padding="10,8"
|
||||
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="#bcd2ea"
|
||||
FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Locking closes the vault, not your terminals: a job you started keeps running, and its output is waiting behind this screen. It also means this machine still holds an open, authenticated channel to those hosts — locked describes the vault, not the connections. Quit DodoSSH to end them." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -175,12 +175,17 @@ function activate(sessionId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Below this, a pane is not being looked at — it is minimised, dragged to nothing, or the host has
|
||||
// hidden its window. Fitting anyway would be actively harmful rather than merely useless: the fit addon
|
||||
// floors its proposal at 2 columns by 1 row, so a degenerate viewport reflows the *remote* pty to 2x1
|
||||
// through window-change, and the wrapped scrollback that produces cannot be recovered when the pane comes
|
||||
// back. A guard rather than a fix for one caller, because several paths reach here — a minimised window, a
|
||||
// splitter dragged to the edge, and a host that hides the WebView while the vault is locked.
|
||||
// Below this, a pane is not being looked at — it is minimised or dragged to nothing. Fitting anyway would
|
||||
// be actively harmful rather than merely useless: the fit addon floors its proposal at 2 columns by 1 row,
|
||||
// so a degenerate viewport reflows the *remote* pty to 2x1 through window-change, and the wrapped
|
||||
// scrollback that produces cannot be recovered when the pane comes back. A guard rather than a fix for one
|
||||
// caller, because more than one path reaches here: a minimised window, and a splitter dragged to the edge
|
||||
// once splits land.
|
||||
//
|
||||
// It is *not* what protects the vault's lock screen, which an earlier version of this comment claimed.
|
||||
// Collapsing the host's WebView hides a native child window without resizing it, so this page's viewport
|
||||
// does not change, no observer fires and this function is never called — measured with a live shell, and
|
||||
// confirmed by removing the guard and finding the lock cycle equally clean. See docs/platform-flags.md.
|
||||
const MINIMUM_FITTABLE_PIXELS = 40;
|
||||
|
||||
function resize(session, sessionId) {
|
||||
|
||||
Reference in New Issue
Block a user