a73x

4a25816d

feat: a declined prompt leaves its reason on the tile's own bar

a73x   2026-08-30 10:59

Commit message
feat: a declined prompt leaves its reason on the tile's own bar

`[prompt declined]` as a STATE, not a notice: an unfocused tile that ends
without exiting narrates on its own bar and asks the keyboard for nothing
(`endedTile`), so a notice routed through the focus would have been a
sentence nobody ever saw. The tile stays on the wall wearing the reason,
the way a lost one does, and the wall is still there to birth from again.

src/tui/wall_pump.zig
Old New
@@ -284,6 +284,10 @@ fn dial(alloc: std.mem.Allocator, t: *Tile, target_in: client.Target) ?client.Tr
284 // from the picker, which is a thing they can choose to do. 284 // from the picker, which is a thing they can choose to do.
285 if (t.shared.prompts) |l| { 285 if (t.shared.prompts) |l| {
286 if (l.declined(d.ssh_pid)) { 286 if (l.declined(d.ssh_pid)) {
287 // The bar says it, before the pump goes: an unfocused tile
288 // that ends without exiting narrates on its own bar and
289 // asks the keyboard for nothing (`wallview.endedTile`).
290 wv.paintLabel(t, .declined);
287 endWith(t, .declined, 1); 291 endWith(t, .declined, 1);
288 return null; 292 return null;
289 } 293 }
src/tui/wall_test_wall.zig
Old New
@@ -167,9 +167,18 @@ test "endKey: x on a tile that never came up closes the TILE — there is no ses
167 try std.testing.expectEqual(EndKey{ .ask = .end_force }, wv.endKey(&t, 999)); 167 try std.testing.expectEqual(EndKey{ .ask = .end_force }, wv.endKey(&t, 999));
168 try std.testing.expectEqual(EndKey{ .ask = .end }, wv.endKey(&t, 1000)); 168 try std.testing.expectEqual(EndKey{ .ask = .end }, wv.endKey(&t, 1000));
169 169
170 // A pump that has ended has nothing to ask over. 170 // A pump that has ended has nothing to ASK over, so the tile goes —
171 // whatever it once was. Without this a tile parked on a host no poll
172 // can log into (a declined prompt, a box that is down) sits on the wall
173 // for the wall's life with no key that removes it: `ringLive` is false,
174 // so the ask reaches nobody, and the list that would vanish it never
175 // comes.
171 t.alive.store(false, .release); 176 t.alive.store(false, .release);
172 try std.testing.expectEqual(EndKey.none, wv.endKey(&t, 0)); 177 try std.testing.expectEqual(EndKey.drop, wv.endKey(&t, 0));
178 // ...including one that never came up at all, which is the same answer
179 // by the other road.
180 t.ever_up.store(false, .release);
181 try std.testing.expectEqual(EndKey.drop, wv.endKey(&t, 0));
173 } 182 }
174 183
175 test "x on a tile whose daemon refused arms a second press, and the window closes after 3 s" { 184 test "x on a tile whose daemon refused arms a second press, and the window closes after 3 s" {
src/tui/wallview.zig
Old New
@@ -536,8 +536,13 @@ pub const EndKey = union(enum) {
536 /// Whether `Ctrl-\ x` has anything to ask, and what. 536 /// Whether `Ctrl-\ x` has anything to ask, and what.
537 pub fn endKey(t: *Tile, now: i64) EndKey { 537 pub fn endKey(t: *Tile, now: i64) EndKey {
538 // A pump that has ended is the only thread that would ever swap `ask` 538 // A pump that has ended is the only thread that would ever swap `ask`
539 // out again, so an ask stored on one goes nowhere for the wall's life. 539 // out again, so an ask stored on one goes nowhere for the wall's life:
540 if (!t.alive.load(.acquire)) return .none; 540 // the TILE goes instead. Nothing is asked of the daemon — a session it
541 // still holds keeps running, and the host's next list births the tile
542 // back — but a tile whose host no poll can reach (a declined prompt, a
543 // box that is down) was otherwise on the wall for the wall's life with
544 // no key that could remove it.
545 if (!t.alive.load(.acquire)) return .drop;
541 // A pump still on its FIRST dial is alive and parked in `dial`, which 546 // A pump still on its FIRST dial is alive and parked in `dial`, which
542 // polls `gone` and never `ask`: the key would be swallowed for as long 547 // polls `gone` and never `ask`: the key would be swallowed for as long
543 // as the box is down — the picker advertises `unreachable` rows as a 548 // as the box is down — the picker advertises `unreachable` rows as a
@@ -2351,7 +2356,14 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry)
2351 exit_msg = "mux: aborted before attaching"; 2356 exit_msg = "mux: aborted before attaching";
2352 break :keys; 2357 break :keys;
2353 } 2358 }
2354 setNotice(&shared, "[nothing attached there yet - tile closed]"); 2359 // Two truths, and the tile knows which: one that
2360 // never attached leaves nothing behind, while one
2361 // whose pump died still has its session on its
2362 // daemon — `x` closed a tile, not a shell.
2363 setNotice(&shared, if (tiles[z].ever_up.load(.acquire))
2364 "[tile closed - the session is still on its daemon]"
2365 else
2366 "[nothing attached there yet - tile closed]");
2355 wall_layout.relayout(w, shared.sel); 2367 wall_layout.relayout(w, shared.sel);
2356 }, 2368 },
2357 .none => {}, 2369 .none => {},