diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 22:20:09 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | fe9786bf845f844315221e08eba793ac1d27ae9d (patch) | |
| tree | 4e9a1591a79f43b6d80f3fba93ec6867182b7740 /src | |
| parent | 4793520998969439bb569c968c3143ae46aefc66 (diff) | |
| download | pardes-fe9786bf845f844315221e08eba793ac1d27ae9d.tar.gz pardes-fe9786bf845f844315221e08eba793ac1d27ae9d.zip | |
A restored command pane comes back finished, not as a live shell
After Restore a dogfood agent's command panes were fish shells under a stale
`(make) exit 0` tag. The dump's terminal record now keeps a command pane's
line and status, and Restore brings it back done: what it printed, its exit,
no shell and no rerun.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/Terminal.zig | 35 | ||||
| -rw-r--r-- | src/dump.zig | 13 |
2 files changed, 47 insertions, 1 deletions
diff --git a/src/Terminal.zig b/src/Terminal.zig index e50eb2df..76be29d0 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -659,6 +659,36 @@ test "replay restores terminal cells from the retained stream" { try std.testing.expectEqualSlices(u8, try replayBytes(pane, gpa), try replayBytes(restored, gpa)); } +test "a restored command pane comes back finished, its command not run again" { + if (comptime !enabled or pardes.isolated) return error.SkipZigTest; + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const cmd = p.freeSlot().?; + _ = try p.newCommand(cmd, "/tmp", "make test"); + feedOutput(p, p.panes[cmd].?, "ok\r\n"); + p.update(.{ .exited = .{ .pane = @intCast(cmd), .status = 2 } }); + try dump.dumpState(p); + const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 }, p.dump_out.?); + defer restored.deinit(); + var spawns: usize = 0; + while (restored.nextEffect()) |effect| switch (effect) { + .spawn => spawns += 1, + else => {}, + }; + try std.testing.expectEqual(@as(usize, 1), spawns); // the shell, not the command + const again = for (restored.panes) |slot| { + if (slot) |pane| if (pane.command != null) break pane; + } else return error.CommandPaneLost; + try std.testing.expectEqualStrings("make test", again.command.?); + try std.testing.expect(again.command_done); + try std.testing.expectEqual(@as(?u8, 2), again.command_status); + const text = try screenTextAlloc(again, gpa); + defer gpa.free(text); + try std.testing.expect(std.mem.indexOf(u8, text, "ok") != null); +} + test "a restored terminal keeps its history above a marker and gets a shell where it was" { if (comptime !enabled or pardes.isolated) return error.SkipZigTest; const gpa = std.testing.allocator; @@ -680,6 +710,7 @@ test "a restored terminal keeps its history above a marker and gets a shell wher try std.testing.expect(spawned); const again = restored.panes[0].?; try std.testing.expectEqualStrings("bash", again.shell.?); + try std.testing.expect(again.command == null); try std.testing.expectEqual(.spawn, again.pending_command.wait); const text = try screenTextAlloc(again, gpa); defer gpa.free(text); @@ -1439,6 +1470,8 @@ pub fn dumpPane( .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), .shell = try arena.dupe(u8, pane.shell orelse ""), + .command = try arena.dupe(u8, pane.command orelse ""), + .status = pane.command_status, .stream = try arena.dupe(u8, text), .stream_b64 = &.{}, .cursor = .{ .col = 0, .row = 0 }, @@ -1489,6 +1522,8 @@ pub fn dumpPane( .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), .shell = try arena.dupe(u8, pane.shell orelse ""), + .command = try arena.dupe(u8, pane.command orelse ""), + .status = pane.command_status, .stream = stream[0..len], .stream_b64 = try dump.encodeBytes(arena, try screenVt(pane, arena)), .stream_is_screen = true, diff --git a/src/dump.zig b/src/dump.zig index 446776d4..54fd1da1 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -102,6 +102,10 @@ pub const Terminal = struct { /// The shell it runs when not the configured one (`Tty bash`); empty, /// the configured one, as in dumps from before. shell: []const u8 = "", + /// A command pane's line and how it ended (null: unknown, or still + /// running when dumped); empty for an interactive terminal. + command: []const u8 = "", + status: ?u8 = null, /// The rendered scrollback and screen as plain text, one row per line. stream: []const u8 = "", stream_b64: []const u8 = "", @@ -745,12 +749,19 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo // A host that runs shells gets them back: the old history // above a new shell in the directory it was in. The web // shell and an isolated build only ever replay it. + // A command pane comes back finished, showing what it said + // and how it ended: its command is not run again. const live = comptime (terminal_panes and platform != .web and !isolated); - const revive = live and t.cwd.len <= effect_path_cap; + const revive = live and t.cwd.len <= effect_path_cap and t.command.len == 0; const restored = try pardes.panes.Terminal.restore(p, src, revive); p.installPane(i, restored); try restored.setOwnedCwd(t.cwd); if (t.shell.len > 0) restored.shell = try gpa.dupe(u8, t.shell); + if (t.command.len > 0) { + restored.command = try gpa.dupe(u8, t.command); + restored.command_done = true; + restored.command_status = t.status; + } if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty; if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } }); break :terminal restored; |
