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}"));
}
///