Commit d12b7cc
Eric Bower
·
2026-03-03 20:22:16 -0500 EST
parent 954f8db
fix(run): when no client is attached, send DA response query from daemon
1 files changed,
+49,
-0
+49,
-0
| ... | ... | @@ -1322,6 +1322,46 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void { | |
| 1322 | 1322 | } | |
| 1323 | 1323 | } | |
| 1324 | 1324 | ||
| 1325 | + | const DA1_QUERY = "\x1b[c"; | |
| 1326 | + | const DA1_QUERY_EXPLICIT = "\x1b[0c"; | |
| 1327 | + | const DA2_QUERY = "\x1b[>c"; | |
| 1328 | + | const DA2_QUERY_EXPLICIT = "\x1b[>0c"; | |
| 1329 | + | const DA1_RESPONSE = "\x1b[?62;22c"; | |
| 1330 | + | const DA2_RESPONSE = "\x1b[>1;10;0c"; | |
| 1331 | + | ||
| 1332 | + | fn respondToDeviceAttributes(pty_fd: i32, data: []const u8) void { | |
| 1333 | + | // Scan for DA queries in PTY output and respond on behalf of the terminal. | |
| 1334 | + | // This handles the case where no client is attached (e.g. zmx run) | |
| 1335 | + | // and the shell (e.g. fish) sends a DA query that would otherwise go unanswered. | |
| 1336 | + | // | |
| 1337 | + | // DA1 query: ESC [ c or ESC [ 0 c | |
| 1338 | + | // DA2 query: ESC [ > c or ESC [ > 0 c | |
| 1339 | + | // DA1 response (from terminal): ESC [ ? ... c (has '?' after '[') | |
| 1340 | + | // | |
| 1341 | + | // We must NOT match DA responses (which contain '?') as queries. | |
| 1342 | + | var i: usize = 0; | |
| 1343 | + | while (i < data.len) { | |
| 1344 | + | if (data[i] == '\x1b' and i + 1 < data.len and data[i + 1] == '[') { | |
| 1345 | + | // Skip DA responses which have '?' after CSI | |
| 1346 | + | if (i + 2 < data.len and data[i + 2] == '?') { | |
| 1347 | + | i += 3; | |
| 1348 | + | continue; | |
| 1349 | + | } | |
| 1350 | + | if (matchSeq(data[i..], DA2_QUERY) or matchSeq(data[i..], DA2_QUERY_EXPLICIT)) { | |
| 1351 | + | _ = posix.write(pty_fd, DA2_RESPONSE) catch {}; | |
| 1352 | + | } else if (matchSeq(data[i..], DA1_QUERY) or matchSeq(data[i..], DA1_QUERY_EXPLICIT)) { | |
| 1353 | + | _ = posix.write(pty_fd, DA1_RESPONSE) catch {}; | |
| 1354 | + | } | |
| 1355 | + | } | |
| 1356 | + | i += 1; | |
| 1357 | + | } | |
| 1358 | + | } | |
| 1359 | + | ||
| 1360 | + | fn matchSeq(data: []const u8, seq: []const u8) bool { | |
| 1361 | + | if (data.len < seq.len) return false; | |
| 1362 | + | return std.mem.eql(u8, data[0..seq.len], seq); | |
| 1363 | + | } | |
| 1364 | + | ||
| 1325 | 1365 | fn findTaskExitMarker(output: []const u8) ?u8 { | |
| 1326 | 1366 | const marker = "ZMX_TASK_COMPLETED:"; | |
| 1327 | 1367 |
| ... | ... | @@ -1436,6 +1476,15 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 1436 | 1476 | try vt_stream.nextSlice(buf[0..n]); | |
| 1437 | 1477 | daemon.has_pty_output = true; | |
| 1438 | 1478 | ||
| 1479 | + | // When no clients are attached, respond to terminal | |
| 1480 | + | // queries (e.g. DA1/DA2) on behalf of the terminal. | |
| 1481 | + | // This prevents shells like from fish from waiting 2s | |
| 1482 | + | // and then sending a no DA query response warning because | |
| 1483 | + | // there's no client terminal to respond to the query. | |
| 1484 | + | if (daemon.clients.items.len == 0) { | |
| 1485 | + | respondToDeviceAttributes(pty_fd, buf[0..n]); | |
| 1486 | + | } | |
| 1487 | + | ||
| 1439 | 1488 | // In run mode, scan output for exit code marker | |
| 1440 | 1489 | if (daemon.is_task_mode and daemon.task_exit_code == null) { | |
| 1441 | 1490 | if (findTaskExitMarker(buf[0..n])) |exit_code| { |