From 22b4d82ba8349e7bf159c30283e5891619810da9 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Fri, 2 Oct 2026 13:54:24 +0200 Subject: [PATCH] Desktop: stop the status bar's session timer when the shell ends --- .../ViewModels/MainWindowViewModel.cs | 9 +++++++-- .../ViewModels/TerminalTabViewModel.cs | 14 +++++++++++++- tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs | 9 +++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs index f133838..05f8f9a 100644 --- a/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs +++ b/src/DodoSSH.Client.Shell/ViewModels/MainWindowViewModel.cs @@ -4518,7 +4518,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp { if (Tabs.FirstOrDefault(tab => tab.SessionId == e.SessionId) is { } tab) { - tab.Ended(e.Outcome.End, e.Outcome.Description); + tab.Ended(e.Outcome.End, e.Outcome.Description, clock.GetUtcNow()); // Only for the tab being looked at, which is what these three already ask. The rectangle does not // change — a dead tab keeps its pane and its scrollback — but the status bar under it does, and @@ -4998,7 +4998,12 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp return null; } - var elapsed = clock.GetUtcNow() - started; + // Up to the end, for a shell that has one: how long it ran, not how long ago it started. + var until = Surface is ShellSurface.Terminal && SelectedTab?.EndedAt is { } ended + ? ended + : clock.GetUtcNow(); + + var elapsed = until - started; if (elapsed < TimeSpan.Zero) { diff --git a/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs index 120f4bc..67a7ef5 100644 --- a/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs +++ b/src/DodoSSH.Client.Shell/ViewModels/TerminalTabViewModel.cs @@ -274,6 +274,15 @@ internal sealed partial class TerminalTabViewModel : ObservableObject /// internal DateTimeOffset? StartedAt { get; set; } + /// When the session behind this tab ended, or null while it has not. + /// + /// What stops the status bar's timer. It used to go on counting from to now over a + /// shell that had exited an hour earlier, which made "how long that shell ran" — the fact the remark above + /// keeps for — the one thing it no longer said. Stamped by from + /// the shell's clock, and cleared whenever a new session takes the tab. + /// + internal DateTimeOffset? EndedAt { get; private set; } + /// /// The negotiated server-to-client cipher, for the status bar — or null while there is no session. /// @@ -620,6 +629,7 @@ internal sealed partial class TerminalTabViewModel : ObservableObject // stale-fact failure this pair exists to remove. EndReason = null; EndDescription = null; + EndedAt = null; IsLive = true; State = TerminalTabState.Open; @@ -679,13 +689,14 @@ internal sealed partial class TerminalTabViewModel : ObservableObject /// live attempt dead. guards its own reports for the same reason. /// /// - internal void Ended(SshSessionEnd end, string description) + internal void Ended(SshSessionEnd end, string description, DateTimeOffset at) { if (State is not TerminalTabState.Open) { return; } + EndedAt ??= at; EndReason = end; EndDescription = description; IsLive = false; @@ -733,6 +744,7 @@ internal sealed partial class TerminalTabViewModel : ObservableObject // Cleared with the session they were facts about. Keeping them would put the previous connection's // cipher in the status bar beside a handshake that has not negotiated one yet. StartedAt = null; + EndedAt = null; Cipher = null; HostKeyAlgorithm = null; IdentityLabel = null; diff --git a/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs b/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs index c5445f1..bbd04c7 100644 --- a/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs +++ b/tests/DodoSSH.Client.App.Tests/ShellFlowTests.cs @@ -2075,6 +2075,15 @@ public sealed partial class ShellFlowTests : IAsyncLifetime // Still worth offering, because the tab and the machine are both still there — what it is not is an // alarm. shell.CanReconnectSelectedTab.ShouldBeTrue(); + + // And the timer stopped with it: how long that shell ran, rather than how long ago it started. The + // shell's clock here is the system's, so the second has to actually pass for the two to differ. + await Task.Delay(TimeSpan.FromSeconds(1.2), Token); + + var ran = tab.EndedAt.ShouldNotBeNull() - tab.StartedAt.ShouldNotBeNull(); + + shell.SessionElapsedText.ShouldBe(string.Create( + CultureInfo.InvariantCulture, $"session {(int)ran.TotalHours:00}:{ran.Minutes:00}:{ran.Seconds:00}")); } ///