Commit 8ba312d
凡辞
·
2026-07-22 12:02:23 -0400 EDT
parent 935c484
fix(send): preserve client leadership
3 files changed,
+34,
-4
+1,
-0
1@@ -19,6 +19,7 @@ Use spec: https://common-changelog.org/
2 - `zmx run` will now detect heredocs and add the completion marker to a newline
3 - Race between `zmx kill X; zmx run X`
4 - Improved claim leader detection
5+- `zmx send` no longer claims client leadership or triggers a resize probe
6 - Improved `ctrl+\` key detection
7 - Support for linux kernel < 4.11 by avoiding statx calls
8
+5,
-3
1@@ -22,15 +22,16 @@ pub const Tag = enum(u8) {
2 LabelSet = 15,
3 LabelClear = 16,
4 LabelData = 17,
5+ Send = 18,
6 // Non-exhaustive: this enum comes off the wire via bytesToValue and
7- // @enumFromInt, so out-of-range values (14-255) are representable
8+ // @enumFromInt, so out-of-range values are representable
9 // rather than UB. Switches must handle `_` (unknown tag).
10 _,
11 };
12
13 comptime {
14 if (@typeInfo(Tag).@"enum".is_exhaustive) @compileError(
15- "ipc.Tag must stay non-exhaustive — old daemons rely on `_` to ignore unknown tags",
16+ "ipc.Tag must stay non-exhaustive -- old daemons rely on `_` to ignore unknown tags",
17 );
18 }
19
20@@ -57,7 +58,7 @@ pub fn getTerminalSize(fd: i32) Resize {
21 pub const MAX_CMD_LEN = 256;
22 pub const MAX_CWD_LEN = 256;
23
24-/// Frozen wire shape. Do NOT add fields — new stats go in new `Tag` values
25+/// Frozen wire shape. Do NOT add fields! New stats go in new `Tag` values
26 /// so old daemons (whose `_` arm ignores unknown tags) stay reachable.
27 /// Changing `@sizeOf(Info)` breaks `zmx list` against running daemons.
28 pub const Info = extern struct {
29@@ -306,6 +307,7 @@ test "Tag wire values are frozen" {
30 .{ Tag.Run, 9 }, .{ Tag.Ack, 10 }, .{ Tag.Switch, 11 },
31 .{ Tag.Write, 12 }, .{ Tag.TaskComplete, 13 }, .{ Tag.LabelGet, 14 },
32 .{ Tag.LabelSet, 15 }, .{ Tag.LabelClear, 16 }, .{ Tag.LabelData, 17 },
33+ .{ Tag.Send, 18 },
34 }) |p| try std.testing.expectEqual(@as(u8, p[1]), @intFromEnum(p[0]));
35 }
36
+28,
-1
1@@ -287,7 +287,7 @@ pub fn main() !void {
2 error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
3 error.OutOfMemory => return err,
4 };
5- return send(&cfg, sesh, socket_path, text_parts.items, .Input);
6+ return send(&cfg, sesh, socket_path, text_parts.items, .Send);
7 } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) {
8 const session_name = args.next() orelse "";
9 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
10@@ -1046,6 +1046,11 @@ const Daemon = struct {
11 }
12 }
13
14+ /// Queue input from `zmx send` without changing interactive client leadership.
15+ pub fn handleSend(self: *Daemon, payload: []const u8) void {
16+ self.queuePtyInput(payload);
17+ }
18+
19 pub fn handleSwitch(self: *Daemon, session_name: []const u8) !void {
20 for (self.clients.items) |client| {
21 if (self.leader_client_fd == client.socket_fd) {
22@@ -1371,6 +1376,27 @@ const Daemon = struct {
23 }
24 };
25
26+test "send queues PTY input without changing leader" {
27+ const alloc = std.testing.allocator;
28+ var daemon = Daemon{
29+ .cfg = undefined,
30+ .alloc = alloc,
31+ .clients = .empty,
32+ .leader_client_fd = 42,
33+ .session_name = "test",
34+ .socket_path = "",
35+ .running = true,
36+ .pid = 0,
37+ .created_at = 0,
38+ };
39+ defer daemon.pty_write_buf.deinit(alloc);
40+
41+ daemon.handleSend("hello");
42+
43+ try std.testing.expectEqual(@as(?i32, 42), daemon.leader_client_fd);
44+ try std.testing.expectEqualStrings("hello", daemon.pty_write_buf.items);
45+}
46+
47 fn printVersion(cfg: *Cfg) !void {
48 var buf: [256]u8 = undefined;
49 var w = std.fs.File.stdout().writer(&buf);
50@@ -3042,6 +3068,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
51 while (client.read_buf.next()) |msg| {
52 switch (msg.header.tag) {
53 .Input => try daemon.handleInput(client, msg.payload),
54+ .Send => daemon.handleSend(msg.payload),
55 .Output => try daemon.handleOutput(msg.payload, &vt_stream),
56 .Init => try daemon.handleInit(client, pty_fd, &term, msg.payload),
57 .Switch => try daemon.handleSwitch(msg.payload),