summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 22:20:09 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitfe9786bf845f844315221e08eba793ac1d27ae9d (patch)
tree4e9a1591a79f43b6d80f3fba93ec6867182b7740 /src
parent4793520998969439bb569c968c3143ae46aefc66 (diff)
downloadpardes-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.zig35
-rw-r--r--src/dump.zig13
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;