a73x

src/gui/picker.zig

Ref:   Size: 29.1 KiB   History

//! Host/session opening policy, with a modal picker or automatic naming on
//! the focused target. Discovery jobs own IO; requests retain their destination
//! and attachment generation until creation and pane insertion finish.
const std = @import("std");
const client = @import("client");
const proto = @import("term").protocol;
const model = @import("workspace.zig");
const runtime = @import("runtime.zig");
const popover = @import("popover.zig");
const discovery = client.discovery;
pub const Level = enum { hosts, sessions, host_name, session_name, busy };
pub const Key = enum { up, down, enter, escape, backspace };
const Host = struct { label: []const u8, target: ?client.Target, reason: []const u8 = "" };
pub const Picker = struct {
    pub const Mode = enum { insert, replace, quick };
    alloc: std.mem.Allocator,
    arena: std.heap.ArenaAllocator,
    rt: *runtime.Runtime,
    origin: ?model.Attachment,
    origin_tab: model.TabId,
    pending: ?model.Pending,
    mode: Mode = .insert,
    ticket: discovery.Ticket,
    next_generation: *u64,
    level: Level = .hosts,
    host_index: usize = 0,
    hosts: std.ArrayList(Host) = .empty,
    sessions: [proto.sessions_max][proto.session_name_max]u8 = undefined,
    session_lens: [proto.sessions_max]u8 = @splat(0),
    session_count: usize = 0,
    presentation: popover.State,
    job: ?*discovery.Job = null,
    wake_ctx: ?*anyopaque,
    wake: ?*const fn (?*anyopaque, discovery.Ticket) void,
    catalogue: ?[]const u8 = null,
    key_path: ?[]const u8,
    width: u32,
    height: u32,
    metrics: model.Metrics,
    closed: bool = false,
    inserted: bool = false,
    collisions: usize = 0,

    pub fn init(alloc: std.mem.Allocator, rt: *runtime.Runtime, next_generation: *u64, key_path: ?[]const u8, width: u32, height: u32, metrics: model.Metrics, wake_ctx: ?*anyopaque, wake: ?*const fn (?*anyopaque, discovery.Ticket) void) !*Picker {
        return initMode(alloc, rt, next_generation, key_path, width, height, metrics, wake_ctx, wake, .insert);
    }
    pub fn initMode(alloc: std.mem.Allocator, rt: *runtime.Runtime, next_generation: *u64, key_path: ?[]const u8, width: u32, height: u32, metrics: model.Metrics, wake_ctx: ?*anyopaque, wake: ?*const fn (?*anyopaque, discovery.Ticket) void, mode: Mode) !*Picker {
        if (mode != .replace and rt.workspace.tab().pending == null) rt.workspace.arm(.beside);
        const pending = rt.workspace.tab().pending;
        const id = if (mode == .replace) rt.workspace.tab().focus else if (pending) |p| p.pane else null;
        const pane = if (id) |p| rt.workspace.pane(p) orelse return error.MissingPane else null;
        if (mode != .insert and pane == null) return error.MissingPane;
        const origin: ?model.Attachment = if (pane) |p| .{ .pane = p.id, .generation = p.generation } else null;
        var presentation = try popover.State.init(alloc, .{});
        const self = alloc.create(Picker) catch |err| {
            presentation.deinit();
            return err;
        };
        self.* = .{ .alloc = alloc, .arena = std.heap.ArenaAllocator.init(alloc), .rt = rt, .origin = origin, .origin_tab = rt.workspace.active_tab_id, .pending = pending, .mode = mode, .ticket = .{ .generation = next_generation.*, .owner = if (origin) |v| v.pane else 0, .attachment_generation = if (origin) |v| v.generation else 0 }, .next_generation = next_generation, .key_path = key_path, .width = width, .height = height, .metrics = metrics, .wake_ctx = wake_ctx, .wake = wake, .presentation = presentation };
        next_generation.* += 1;
        errdefer self.deinit();
        if (mode == .quick) {
            // Check geometry before any request, and retain the exact target,
            // including custom socket, key and SSH arguments.
            _ = try self.preflight();
            try self.includeTarget(pane.?.identity.target);
            try self.setNoticeFmt("Opening on {s}... (Esc cancels)", .{self.host()});
            try self.list();
            return self;
        }
        const a = self.arena.allocator();
        self.catalogue = client.hosts.statePath(a) catch null;
        if (self.catalogue) |path| {
            const loaded = client.hosts.load(a, path) catch |err| blk: {
                self.setNotice(@errorName(err));
                break :blk client.hosts.Hosts{};
            };
            for (loaded.lines.items) |line| {
                const target = client.Target.fromSpec(a, try client.hosts.parse(line), key_path, client.quic_idle_ms_default, false) catch |err| {
                    try self.hosts.append(a, .{ .label = line, .target = null, .reason = @errorName(err) });
                    continue;
                };
                try self.hosts.append(a, .{ .label = line, .target = target });
            }
        }
        if (pane) |p| try self.includeTarget(p.identity.target);
        try self.show(.hosts);
        return self;
    }
    pub fn includeTarget(self: *Picker, current: client.Target) !void {
        const a = self.arena.allocator();
        const label = switch (current) {
            .sock => |path| try std.fmt.allocPrint(a, "--sock {s}", .{path}),
            .quic => |q| try std.fmt.allocPrint(a, "quic://{s}", .{q.host_port}),
            .hand => |h| h.host,
            .via => "Current connection",
        };
        var found = false;
        for (self.hosts.items) |*entry| if (std.mem.eql(u8, entry.label, label)) {
            entry.target = try discovery.cloneTarget(a, current);
            entry.reason = "";
            found = true;
            break;
        };
        if (!found) try self.hosts.append(a, .{ .label = try a.dupe(u8, label), .target = try discovery.cloneTarget(a, current) });
        if (self.level == .hosts) try self.show(.hosts);
    }
    pub fn deinit(self: *Picker) void {
        if (self.job != null) self.cancel();
        self.presentation.deinit();
        self.arena.deinit();
        self.alloc.destroy(self);
    }
    pub fn noticeText(self: *const Picker) []const u8 {
        return self.presentation.noticeText();
    }
    pub fn host(self: *const Picker) []const u8 {
        return if (self.hosts.items.len > 0) self.hosts.items[self.host_index].label else "";
    }
    pub fn text(self: *Picker, bytes: []const u8) !void {
        const accepted = self.presentation.append(bytes) catch |err| switch (err) {
            error.TextTooLong => {
                self.setNotice("Name is too long");
                return;
            },
            else => return err,
        };
        if (accepted) self.clearNotice();
    }
    pub fn key(self: *Picker, pressed: Key) !void {
        switch (pressed) {
            .up => self.presentation.navigate(false),
            .down => self.presentation.navigate(true),
            .backspace => if (self.presentation.backspace()) self.clearNotice(),
            .escape => try self.back(),
            .enter => if (self.presentation.submit()) |value| try self.event(value),
        }
    }
    pub fn pointerDown(self: *Picker, x: u32, y: u32) void {
        self.presentation.pointerDown(self.layout(), x, y);
    }
    pub fn pointerUp(self: *Picker, x: u32, y: u32) !void {
        if (self.presentation.pointerUp(self.layout(), x, y)) |value| try self.event(value);
    }
    pub fn cancelPress(self: *Picker) void {
        self.presentation.cancelPress();
    }
    pub fn setDirection(self: *Picker, direction: model.Direction) void {
        if (self.mode != .insert or (self.level != .hosts and self.level != .sessions) or !self.validOrigin()) return;
        if (self.pending) |*pending| {
            pending.direction = direction;
            self.rt.workspace.tab().pending = pending.*;
        }
    }
    pub fn cancel(self: *Picker) void {
        self.clearNotice();
        self.show(.hosts) catch {};
        self.closed = self.mode == .quick;
        if (self.closed) self.setNotice("Pane opening cancelled");
        if (self.job) |job| {
            job.cancel();
            const result = self.finishJob();
            self.ticket.generation = self.next_generation.*;
            self.next_generation.* += 1;
            if (result.may_have_created) {
                self.show(.session_name) catch {};
                self.outcomeNotice(result.phase == .created, "Esc refreshes sessions; no automatic retry.");
            }
        }
    }
    /// Pointer dismissal owns its complete gesture and tears down the modal
    /// after cancelling work. Escape retains the established back/refresh
    /// screen when an outcome is uncertain.
    fn dismiss(self: *Picker) void {
        var previous: [1024]u8 = undefined;
        const old = self.noticeText();
        const len = @min(old.len, previous.len);
        @memcpy(previous[0..len], old[0..len]);
        self.cancel();
        if (self.noticeText().len == 0 and len != 0) self.setNotice(previous[0..len]);
        self.closed = true;
    }
    fn back(self: *Picker) !void {
        if (self.mode == .quick) return self.cancel();
        self.clearNotice();
        switch (self.level) {
            .hosts => self.closed = true,
            .sessions, .host_name => {
                try self.show(.hosts);
            },
            .session_name => try self.list(),
            .busy => self.cancel(),
        }
    }
    fn choose(self: *Picker) !void {
        self.clearNotice();
        switch (self.level) {
            .hosts => {
                const selected = self.presentation.selected orelse return;
                if (selected == self.hosts.items.len) {
                    try self.show(.host_name);
                    self.presentation.input.clearRetainingCapacity();
                } else {
                    self.host_index = selected;
                    try self.list();
                }
            },
            .sessions => {
                const selected = self.presentation.selected orelse return;
                if (selected == self.session_count) {
                    try self.show(.session_name);
                    self.presentation.input.clearRetainingCapacity();
                } else try self.insert(self.sessions[selected][0..self.session_lens[selected]], false);
            },
            .host_name => {
                const a = self.arena.allocator();
                const spelling = self.presentation.input.items;
                const spec = client.hosts.parse(spelling) catch |err| {
                    self.setNotice(client.hosts.reason(err));
                    return;
                };
                const target = client.Target.fromSpec(a, spec, self.key_path, client.quic_idle_ms_default, false) catch |err| {
                    self.setNotice(@errorName(err));
                    return;
                };
                const path = self.catalogue orelse {
                    self.setNotice("Host catalogue path is unavailable");
                    return;
                };
                _ = client.hosts.record(self.alloc, path, spelling) catch |err| {
                    self.setNotice(@errorName(err));
                    return;
                };
                for (self.hosts.items, 0..) |h, i| if (std.mem.eql(u8, h.label, spelling)) {
                    self.host_index = i;
                    try self.list();
                    return;
                };
                try self.hosts.append(a, .{ .label = try a.dupe(u8, spelling), .target = try discovery.cloneTarget(a, target) });
                self.host_index = self.hosts.items.len - 1;
                try self.list();
            },
            .session_name => try self.create(),
            .busy => {},
        }
    }
    fn create(self: *Picker) !void {
        const name = self.presentation.input.items;
        if (!proto.validSessionName(name)) {
            self.setNotice("Choose a valid session name (no spaces)");
            return;
        }
        const placement = self.preflight() catch |err| {
            self.setNotice(@errorName(err));
            self.closed = self.mode == .quick;
            return;
        };
        try self.start(.{ .create = .{ .name = name, .cols = placement.cols, .rows = placement.rows } });
    }
    fn validOrigin(self: *Picker) bool {
        if (self.rt.workspace.active_tab_id != self.origin_tab) return false;
        if (self.origin) |origin| {
            if (!self.rt.accepts(origin)) return false;
        } else if (self.rt.workspace.tab().tree.root != null) return false;
        return self.mode == .replace or std.meta.eql(self.rt.workspace.tab().pending, self.pending);
    }
    fn preflight(self: *Picker) !model.Placement {
        if (!self.validOrigin()) return error.OriginChanged;
        if (self.mode == .replace) {
            const workspace_layout = self.rt.workspace.layout(self.width, self.height, self.metrics);
            if (!workspace_layout.fits) return error.TooSmall;
            return workspace_layout.get(self.origin.?.pane) orelse error.MissingPane;
        }
        return self.rt.workspace.preview(self.width, self.height, self.metrics);
    }
    fn insert(self: *Picker, name: []const u8, created: bool) !void {
        if (!self.validOrigin()) {
            self.insertionFailed(error.OriginChanged, created);
            return;
        }
        const target = self.hosts.items[self.host_index].target orelse {
            self.setNotice("Target is unavailable");
            return;
        };
        if (self.mode == .replace) {
            const workspace_layout = self.rt.workspace.layout(self.width, self.height, self.metrics);
            self.rt.replace(self.origin.?.pane, target, name, workspace_layout.get(self.origin.?.pane).?, false) catch |err| {
                self.insertionFailed(err, created);
                return;
            };
        } else {
            const focus = self.rt.workspace.tab().focus;
            _ = self.rt.addWithPolicy(target, name, self.width, self.height, self.metrics, true) catch |err| {
                self.insertionFailed(err, created);
                return;
            };
            // Typing may have moved to a different pane during discovery.
            if (self.mode == .quick and focus != self.origin.?.pane) if (focus) |id| {
                _ = self.rt.workspace.focus(id);
            };
        }
        self.inserted = true;
        self.closed = true;
    }
    fn insertionFailed(self: *Picker, err: anyerror, created: bool) void {
        if (created) self.outcomeNotice(true, "Pane insertion failed. Esc refreshes available sessions.") else self.setNotice(@errorName(err));
        self.closed = self.mode == .quick;
    }
    fn list(self: *Picker) !void {
        if (self.hosts.items[self.host_index].target == null) {
            try self.show(.hosts);
            self.setNotice(self.hosts.items[self.host_index].reason);
            return;
        }
        try self.start(.list);
    }
    fn start(self: *Picker, operation: discovery.Operation) !void {
        std.debug.assert(self.job == null);
        self.ticket.generation = self.next_generation.*;
        self.next_generation.* += 1;
        self.job = discovery.Job.start(self.alloc, .{ .ticket = self.ticket, .target = self.hosts.items[self.host_index].target.?, .operation = operation, .use_poll_recipe = self.mode != .quick, .wake_ctx = self.wake_ctx, .wake = self.wake }) catch |err| {
            self.setNotice(@errorName(err));
            self.closed = self.mode == .quick;
            return;
        };
        try self.show(.busy);
    }
    pub fn poll(self: *Picker) !bool {
        const job = self.job orelse return false;
        if (!job.done.load(.acquire)) return false;
        const creating = job.opts.operation == .create;
        try self.applyResult(self.finishJob(), creating);
        return true;
    }
    fn finishJob(self: *Picker) discovery.Result {
        const job = self.job.?;
        const result = job.join();
        job.stop();
        self.job = null;
        return result;
    }
    fn applyResult(self: *Picker, result: discovery.Result, creating: bool) !void {
        if (!std.meta.eql(result.ticket, self.ticket) or !self.validOrigin()) {
            try self.show(.hosts);
            if (result.phase == .created) self.outcomeNotice(true, "Destination changed. Refresh sessions to choose it.") else if (result.may_have_created) self.outcomeNotice(false, "Destination changed. Refresh sessions before retrying.") else self.setNotice("The pane for this request changed");
            self.closed = self.mode == .quick;
            return;
        }
        switch (result.phase) {
            .sessions => {
                if (self.mode == .quick) {
                    var buf: [proto.session_name_max]u8 = undefined;
                    self.presentation.input.clearRetainingCapacity();
                    try self.presentation.input.appendSlice(self.alloc, client.nextFreeName(&buf, result.text()));
                    try self.create();
                    return;
                }
                self.session_count = 0;
                var iter = proto.sessionsIter(result.text());
                while (iter.next()) |name| {
                    if (self.session_count == self.sessions.len) break;
                    @memcpy(self.sessions[self.session_count][0..name.len], name);
                    self.session_lens[self.session_count] = @intCast(name.len);
                    self.session_count += 1;
                }
                try self.show(.sessions);
            },
            .created => {
                try self.show(.session_name);
                try self.insert(self.presentation.input.items, true);
            },
            .exists => {
                // A definite collision is safe to retry. A lost reply is not:
                // it may already have created the shell under this name.
                if (self.mode == .quick and self.collisions < proto.sessions_max) {
                    self.collisions += 1;
                    try self.list();
                } else {
                    try self.show(.session_name);
                    self.closed = self.mode == .quick;
                    self.setNotice(if (self.mode == .quick) "Session names keep changing; try again" else result.reasonText());
                }
            },
            else => {
                if (!creating) self.session_count = 0;
                try self.show(if (creating) .session_name else .sessions);
                if (result.may_have_created) self.outcomeNotice(false, "Esc refreshes sessions; no automatic retry.") else self.setNotice(result.reasonText());
                self.closed = self.mode == .quick;
            },
        }
        return;
    }
    fn outcomeNotice(self: *Picker, created: bool, suffix: []const u8) void {
        const outcome = if (created) "Session created" else "Creation outcome unknown for";
        var message: [1024]u8 = undefined;
        const message_text = if (self.mode == .quick)
            std.fmt.bufPrint(&message, "{s} '{s}' on {s}. Prefix Enter to check sessions.", .{ outcome, self.presentation.input.items, self.host() }) catch "Creation outcome is unknown"
        else
            std.fmt.bufPrint(&message, "{s} '{s}'. {s}", .{ outcome, self.presentation.input.items, suffix }) catch "Creation outcome is unknown";
        self.setNotice(message_text);
    }
    pub fn layout(self: *Picker) popover.Layout {
        return self.presentation.layout(self.width, self.height, .{ .cell_w = self.metrics.cell_w, .cell_h = self.metrics.cell_h });
    }
    fn event(self: *Picker, value: popover.Event) !void {
        switch (value) {
            .select => |index| {
                self.presentation.selected = index;
                try self.choose();
            },
            .submit => try self.choose(),
            .dismiss => self.dismiss(),
        }
    }
    /// Picker screens remain workflow state, but each screen has exactly one
    /// owned presentation.  Replacement completes before the screen changes.
    pub fn show(self: *Picker, next: Level) !void {
        var rows: std.ArrayListUnmanaged(popover.Row) = .empty;
        defer rows.deinit(self.alloc);
        switch (next) {
            .hosts => {
                for (self.hosts.items) |entry| try rows.append(self.alloc, .{ .label = entry.label });
                try rows.append(self.alloc, .{ .label = "Add host..." });
            },
            .sessions => {
                for (0..self.session_count) |i| try rows.append(self.alloc, .{ .label = self.sessions[i][0..self.session_lens[i]] });
                try rows.append(self.alloc, .{ .label = "New session..." });
            },
            else => {},
        }
        const title = switch (next) {
            .hosts => "Choose host",
            .sessions => "Choose session",
            .host_name => "Add host",
            .session_name => "New session",
            .busy => if (self.job != null and self.job.?.opts.operation == .create) "Creating session..." else "Loading sessions...",
        };
        const editing = next == .host_name or next == .session_name;
        const hint = if (editing) "Enter confirms | Esc goes back" else if (next == .busy) "Esc cancels" else if (self.mode == .insert) "j/k choose | Enter selects | v below, b beside | Esc back" else "Up/Down or j/k choose | Enter selects | Esc goes back";
        try self.presentation.replace(.{ .title = title, .context = if (next != .hosts and next != .host_name) self.host() else "", .notice = self.noticeText(), .hint = hint, .rows = rows.items, .editor = if (editing) .{ .limit = if (next == .session_name) proto.session_name_max else 1024 } else null });
        self.level = next;
        if (next == .hosts and self.host_index < self.hosts.items.len) self.presentation.selected = self.host_index;
    }
    fn clearNotice(self: *Picker) void {
        if (self.noticeText().len == 0) return;
        self.presentation.setNotice("");
    }
    fn setNoticeFmt(self: *Picker, comptime format: []const u8, args: anytype) !void {
        var bytes: [1024]u8 = undefined;
        self.setNotice(try std.fmt.bufPrint(&bytes, format, args));
    }
    fn setNotice(self: *Picker, text_: []const u8) void {
        self.presentation.setNotice(text_);
    }
};

test "picker validates fit before create and preserves remote outcome after origin or tab changes" {
    const a = std.testing.allocator;
    var rt = runtime.Runtime.init(a, .{});
    defer rt.deinit();
    const metrics: model.Metrics = .{ .cell_w = 8, .cell_h = 16 };
    const id = try rt.add(.{ .via = "cat" }, "origin", 800, 600, metrics);
    rt.workspace.arm(.beside);
    var next: u64 = 2;
    var picker: Picker = .{ .alloc = a, .arena = std.heap.ArenaAllocator.init(a), .rt = &rt, .origin = rt.get(id).?.key, .origin_tab = rt.workspace.active_tab_id, .pending = rt.workspace.tab().pending.?, .ticket = .{ .generation = 1, .owner = id, .attachment_generation = 1 }, .next_generation = &next, .key_path = null, .width = 1, .height = 1, .metrics = metrics, .wake = null, .wake_ctx = null, .presentation = try popover.State.init(a, .{}) };
    defer picker.arena.deinit();
    defer picker.presentation.deinit();
    try picker.hosts.append(picker.arena.allocator(), .{ .label = "fixture", .target = .{ .via = "cat" } });
    try picker.includeTarget(.{ .via = "local" });
    try std.testing.expectEqualStrings("Current connection", picker.presentation.view().rows[1].label);
    try picker.show(.session_name);
    try picker.text("new-shell");
    try picker.key(.enter);
    try std.testing.expect(picker.job == null);
    try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
    try picker.text("\x01");
    try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
    picker.dismiss();
    try std.testing.expect(picker.closed and std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
    picker.closed = false;
    try picker.show(.session_name);
    try picker.show(.busy);
    try picker.key(.backspace);
    try std.testing.expectEqualStrings("new-shell", picker.presentation.input.items);
    try picker.show(.session_name);
    picker.session_count = 1;
    @memcpy(picker.sessions[0][0..3], "old");
    picker.session_lens[0] = 3;
    try picker.show(.sessions);
    try picker.applyResult(.{ .ticket = picker.ticket, .phase = .failed }, false);
    try std.testing.expectEqual(@as(usize, 0), picker.session_count);
    try std.testing.expectEqualStrings("New session...", picker.presentation.view().rows[0].label);
    try picker.show(.session_name);
    try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);

    picker.width = 800;
    picker.height = 600;
    const next_id = rt.workspace.next_pane_id;
    _ = try picker.preflight();
    try std.testing.expectEqual(next_id, rt.workspace.next_pane_id);
    rt.workspace.active_tab_id += 1;
    try picker.applyResult(.{ .ticket = picker.ticket, .phase = .created, .may_have_created = true }, true);
    try std.testing.expect(!picker.inserted and !picker.closed);
    try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "Session created 'new-shell'") != null);
    rt.workspace.active_tab_id = picker.origin_tab;
    rt.workspace.cancel();
    try picker.applyResult(.{ .ticket = picker.ticket, .phase = .cancelled, .may_have_created = true }, true);
    try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "outcome unknown") != null);
    try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);
    rt.workspace.tab().pending = picker.pending;
    const placement = rt.workspace.layout(800, 600, metrics).get(id).?;
    const old_key = rt.get(id).?.key;
    try rt.retry(id, placement);
    try std.testing.expect(!rt.accepts(old_key));
    try picker.applyResult(.{ .ticket = picker.ticket, .phase = .created, .may_have_created = true }, true);
    try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "Session created 'new-shell'") != null);
    try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);
}

test "quick creation retries collisions but retains unknown outcomes and its original destination" {
    const a = std.testing.allocator;
    var rt = runtime.Runtime.init(a, .{});
    defer rt.deinit();
    const metrics: model.Metrics = .{ .cell_w = 8, .cell_h = 16 };
    const id = try rt.add(.{ .via = "cat" }, "origin", 1200, 900, metrics);
    const neighbor = try rt.add(.{ .via = "cat" }, "neighbor", 1200, 900, metrics);
    _ = rt.workspace.focus(id);
    rt.workspace.arm(.stacked);
    var next: u64 = 2;
    var opening: Picker = .{ .alloc = a, .arena = std.heap.ArenaAllocator.init(a), .rt = &rt, .origin = rt.get(id).?.key, .origin_tab = rt.workspace.active_tab_id, .pending = rt.workspace.tab().pending, .mode = .quick, .ticket = .{ .generation = 1, .owner = id, .attachment_generation = 1 }, .next_generation = &next, .key_path = null, .width = 1200, .height = 900, .metrics = metrics, .wake = null, .wake_ctx = null, .presentation = try popover.State.init(a, .{}) };
    defer opening.arena.deinit();
    defer opening.presentation.deinit();
    defer if (opening.job) |job| job.stop();
    try opening.includeTarget(rt.workspace.pane(id).?.identity.target);
    var result: discovery.Result = .{ .ticket = opening.ticket, .phase = .sessions };
    const names = "0\n2\n";
    @memcpy(result.bytes[0..names.len], names);
    result.len = names.len;
    try opening.applyResult(result, false);
    try std.testing.expectEqualStrings("1", opening.job.?.opts.operation.create.name);
    opening.job.?.stop();
    opening.job = null;
    try opening.applyResult(.{ .ticket = opening.ticket, .phase = .exists }, true);
    try std.testing.expect(opening.job.?.opts.operation == .list and !opening.closed);
    opening.job.?.stop();
    opening.job = null;
    const updated = "0\n1\n2\n";
    @memcpy(result.bytes[0..updated.len], updated);
    result.len = updated.len;
    result.ticket = opening.ticket;
    try opening.applyResult(result, false);
    try std.testing.expectEqualStrings("3", opening.job.?.opts.operation.create.name);
    opening.job.?.stop();
    opening.job = null;
    try opening.applyResult(.{ .ticket = opening.ticket, .phase = .failed, .may_have_created = true }, true);
    try std.testing.expect(opening.closed and opening.job == null and !opening.inserted);
    try std.testing.expect(std.mem.indexOf(u8, opening.noticeText(), "outcome unknown for '3'") != null);

    opening.closed = false;
    rt.workspace.active_tab_id += 1;
    try opening.applyResult(.{ .ticket = opening.ticket, .phase = .created, .may_have_created = true }, true);
    try std.testing.expect(opening.closed and !opening.inserted);
    try std.testing.expect(std.mem.indexOf(u8, opening.noticeText(), "Session created '3'") != null);
    rt.workspace.active_tab_id = opening.origin_tab;

    opening.closed = false;
    _ = rt.workspace.focus(neighbor);
    try opening.applyResult(.{ .ticket = opening.ticket, .phase = .created, .may_have_created = true }, true);
    try std.testing.expect(opening.closed and opening.inserted);
    try std.testing.expectEqual(neighbor, rt.workspace.tab().focus.?);
    const flat = rt.workspace.layout(1200, 900, metrics);
    try std.testing.expectEqual(@as(usize, 3), flat.len);
    const inserted = flat.items()[1];
    try std.testing.expect(inserted.outer.y >= flat.get(id).?.outer.y + flat.get(id).?.outer.h);
    try std.testing.expectEqualStrings("3", rt.workspace.pane(inserted.id).?.identity.session);
    try std.testing.expect(rt.get(inserted.id).?.pump.opts.existing_only);
}