Let a closed terminal take its own status line with it

The strip along the bottom is one line for the whole window, and connecting
writes to it, so "Connected to prod-db." outlived the tab it was about: close
prod-db, look at the terminal beside it, and the bar was still reporting on a
session that is no longer there.

A connection's own lines are now owned by the attempt that wrote them, and
closing a tab clears the line only when that is what the bar is holding.
Ownership rather than a blanket clear on close, because the alternative loses
the sentences this bar exists for — a failed save or a refused sync is what the
design deleted the bar's other four fields to make room for, and closing a
terminal is no answer to one.
This commit is contained in:
2026-08-10 11:41:55 +02:00
parent 7f77539ba6
commit 05c56f20a4
5 changed files with 169 additions and 10 deletions
@@ -1254,6 +1254,68 @@ public sealed class ShellFlowTests : IAsyncLifetime
shell.IsTerminalShowing.ShouldBeTrue();
}
/// <remarks>
/// The status line is one line for the whole window, so a sentence about a session that has been closed is
/// a sentence the user reads over some other terminal.
/// </remarks>
[Fact]
public async Task ClosingATab_TakesItsOwnStatusLineWithIt()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
vault.Status.ShouldContain("Connected", Case.Insensitive);
await shell.CloseTabCommand.ExecuteAsync(shell.Tabs[0]);
vault.Status.ShouldBeEmpty();
}
/// <remarks>
/// The other half, and the reason closing does not simply blank the bar: everything this application has
/// to say about a save, a sync or a refusal goes through the same line — see <c>StatusBar.axaml</c> — and
/// closing a terminal answers none of it.
/// </remarks>
[Fact]
public async Task ClosingATab_LeavesAStatusLineThatIsAboutSomethingElse()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
vault.Status = "The keychain could not be saved.";
await shell.CloseTabCommand.ExecuteAsync(shell.Tabs[0]);
vault.Status.ShouldBe("The keychain could not be saved.");
}
/// <remarks>
/// Two sessions to the same host, which is the case a label could not tell apart: the line belongs to the
/// attempt that wrote it, so closing the other one leaves it alone.
/// </remarks>
[Fact]
public async Task ClosingATab_LeavesTheLineAnotherTabWrote()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
var first = shell.Tabs[0];
// The second connection's own line is what the bar holds now.
await vault.ConnectCommand.ExecuteAsync(null);
var reported = vault.Status;
await shell.CloseTabCommand.ExecuteAsync(first);
vault.Status.ShouldBe(reported);
}
// ---- Connecting, while it is still happening ----
//
// A handshake is a network round trip and no longer holds the vault while it runs, so there is a stretch