- commit
- d12b7cc
- parent
- 954f8db
- author
- Eric Bower
- date
- 2026-03-03 20:22:16 -0500 EST
fix(run): when no client is attached, send DA response query from daemon
1 files changed,
+49,
-0
+49,
-0
1@@ -1322,6 +1322,46 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
2 }
3 }
4
5+const DA1_QUERY = "\x1b[c";
6+const DA1_QUERY_EXPLICIT = "\x1b[0c";
7+const DA2_QUERY = "\x1b[>c";
8+const DA2_QUERY_EXPLICIT = "\x1b[>0c";
9+const DA1_RESPONSE = "\x1b[?62;22c";
10+const DA2_RESPONSE = "\x1b[>1;10;0c";
11+
12+fn respondToDeviceAttributes(pty_fd: i32, data: []const u8) void {
13+ // Scan for DA queries in PTY output and respond on behalf of the terminal.
14+ // This handles the case where no client is attached (e.g. zmx run)
15+ // and the shell (e.g. fish) sends a DA query that would otherwise go unanswered.
16+ //
17+ // DA1 query: ESC [ c or ESC [ 0 c
18+ // DA2 query: ESC [ > c or ESC [ > 0 c
19+ // DA1 response (from terminal): ESC [ ? ... c (has '?' after '[')
20+ //
21+ // We must NOT match DA responses (which contain '?') as queries.
22+ var i: usize = 0;
23+ while (i < data.len) {
24+ if (data[i] == '\x1b' and i + 1 < data.len and data[i + 1] == '[') {
25+ // Skip DA responses which have '?' after CSI
26+ if (i + 2 < data.len and data[i + 2] == '?') {
27+ i += 3;
28+ continue;
29+ }
30+ if (matchSeq(data[i..], DA2_QUERY) or matchSeq(data[i..], DA2_QUERY_EXPLICIT)) {
31+ _ = posix.write(pty_fd, DA2_RESPONSE) catch {};
32+ } else if (matchSeq(data[i..], DA1_QUERY) or matchSeq(data[i..], DA1_QUERY_EXPLICIT)) {
33+ _ = posix.write(pty_fd, DA1_RESPONSE) catch {};
34+ }
35+ }
36+ i += 1;
37+ }
38+}
39+
40+fn matchSeq(data: []const u8, seq: []const u8) bool {
41+ if (data.len < seq.len) return false;
42+ return std.mem.eql(u8, data[0..seq.len], seq);
43+}
44+
45 fn findTaskExitMarker(output: []const u8) ?u8 {
46 const marker = "ZMX_TASK_COMPLETED:";
47
48@@ -1436,6 +1476,15 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
49 try vt_stream.nextSlice(buf[0..n]);
50 daemon.has_pty_output = true;
51
52+ // When no clients are attached, respond to terminal
53+ // queries (e.g. DA1/DA2) on behalf of the terminal.
54+ // This prevents shells like from fish from waiting 2s
55+ // and then sending a no DA query response warning because
56+ // there's no client terminal to respond to the query.
57+ if (daemon.clients.items.len == 0) {
58+ respondToDeviceAttributes(pty_fd, buf[0..n]);
59+ }
60+
61 // In run mode, scan output for exit code marker
62 if (daemon.is_task_mode and daemon.task_exit_code == null) {
63 if (findTaskExitMarker(buf[0..n])) |exit_code| {