a73x

4ec53cb4

feat: x on a pane still waiting is refused, not dropped

a73x   2026-08-30 16:52

Commit message
feat: x on a pane still waiting is refused, not dropped

A pending pane names a session its daemon really has: the local drop a
never-attached tile earns would last one poll, until the host's list
births it straight back — a vanish-and-return the user reads as a bug.
The bind is at most a poll away; the x can wait for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/tui/wall_test_wall.zig
Old New
@@ -1230,3 +1230,17 @@ test "State.waiting has a word of its own, and truncation drops the label before
1230 if (!std.mem.endsWith(u8, bar, " [waiting]")) return error.WaitingBarLostTheStateWord; 1230 if (!std.mem.endsWith(u8, bar, " [waiting]")) return error.WaitingBarLostTheStateWord;
1231 if (bar.len > 30) return error.WaitingBarOverranTheTerminal; 1231 if (bar.len > 30) return error.WaitingBarOverranTheTerminal;
1232 } 1232 }
1233
1234 test "endKey: x on a pane that has not bound yet is refused - a drop would last one poll" {
1235 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
1236 var tiles = fixture.diffFixture(&shared);
1237 tiles[1].pending = true;
1238 tiles[1].alive.store(false, .release);
1239 tiles[1].state = .waiting;
1240 // The refusal, not the local drop the same fields would earn a tile
1241 // whose dial never landed: this pane's session is real, and the
1242 // daemon's next list would birth a dropped tile straight back.
1243 try std.testing.expectEqual(EndKey.waiting, wv.endKey(&tiles[1], 0));
1244 tiles[1].pending = false;
1245 try std.testing.expectEqual(EndKey.drop, wv.endKey(&tiles[1], 0));
1246 }
src/tui/wallview.zig
Old New
@@ -536,6 +536,9 @@ pub const EndKey = union(enum) {
536 ask: client.SwitchIntent, 536 ask: client.SwitchIntent,
537 /// No session yet: the TILE goes, locally. 537 /// No session yet: the TILE goes, locally.
538 drop, 538 drop,
539 /// A pending pane. Its session is real — the daemon's next list would
540 /// birth a dropped tile straight back — and its bind is a poll away.
541 waiting,
539 /// Nothing to do, and a sentence owed. 542 /// Nothing to do, and a sentence owed.
540 none, 543 none,
541 }; 544 };
@@ -549,6 +552,7 @@ pub fn endKey(t: *Tile, now: i64) EndKey {
549 // back — but a tile whose host no poll can reach (a declined prompt, a 552 // back — but a tile whose host no poll can reach (a declined prompt, a
550 // box that is down) was otherwise on the wall for the wall's life with 553 // box that is down) was otherwise on the wall for the wall's life with
551 // no key that could remove it. 554 // no key that could remove it.
555 if (t.pending) return .waiting;
552 if (!t.alive.load(.acquire)) return .drop; 556 if (!t.alive.load(.acquire)) return .drop;
553 // A pump still on its FIRST dial is alive and parked in `dial`, which 557 // A pump still on its FIRST dial is alive and parked in `dial`, which
554 // polls `gone` and never `ask`: the key would be swallowed for as long 558 // polls `gone` and never `ask`: the key would be swallowed for as long
@@ -2439,6 +2443,11 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2439 "[nothing attached there yet - tile closed]"); 2443 "[nothing attached there yet - tile closed]");
2440 wall_layout.relayout(w, shared.sel); 2444 wall_layout.relayout(w, shared.sel);
2441 }, 2445 },
2446 .waiting => {
2447 asked = true;
2448 setNotice(&shared, "[waiting for its host - nothing attached to end yet]");
2449 showRefusal(tiles[0..live], &shared, z);
2450 },
2442 .none => {}, 2451 .none => {},
2443 } 2452 }
2444 if (!asked) { 2453 if (!asked) {