From d292f2b3422e206c6e8803ca79bfdac487d29730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 11 Jun 2026 18:19:51 +0200 Subject: [PATCH] agent: pump session during terminal idle Updates isocline to support idle callbacks and registers a hook to pump the session's curl multi while waiting for user input. This prevents connection timeouts. Also refactors the idle pumping logic into `Session.idleSlice` to share it with the MCP server. --- build.zig.zon | 4 ++-- src/agent/Agent.zig | 7 +++++++ src/agent/Terminal.zig | 4 ++++ src/browser/Session.zig | 14 ++++++++++++++ src/mcp/Server.zig | 13 +------------ 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 3a7629814..54762a31c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -39,8 +39,8 @@ .hash = "zenai-0.0.0-iOY_VDVqBAA3mH3_90Lg7Vz3JK8oD0gIhcete_M9c1UC", }, .isocline = .{ - .url = "git+https://github.com/arrufat/isocline?ref=lightpanda#77ff0c32b344a7fcf33f418cb9ccc14531c64997", - .hash = "N-V-__8AAMFfEwCVkwwv9MwT6R1vcXwTmJn60QGSz-uPK3b9", + .url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47", + .hash = "N-V-__8AANtqEwD-jr5LWeksHDYkaQSpLbu6MRHFvlPURIik", }, }, .paths = .{""}, diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 7fc3521b1..99a4b5ba9 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -381,6 +381,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent }; // The model-list cache fills lazily on the first `/model` completion, // so startup never blocks on the network. + Terminal.setIdleCallback(&idlePump, @ptrCast(self)); } return self; @@ -403,6 +404,12 @@ pub fn deinit(self: *Agent) void { self.allocator.destroy(self); } +/// isocline idle hook; returns the delay in ms before the next invocation. +fn idlePump(arg: ?*anyopaque) callconv(.c) c_long { + const self: *Agent = @ptrCast(@alignCast(arg.?)); + return self.session.idleSlice(); +} + /// Create a fresh browser session and wire its cancel hook back to this agent /// so Ctrl-C aborts in-flight page work. Startup and `/reset`. fn startSession(self: *Agent) !void { diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index c7341412d..09ae7e493 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -937,6 +937,10 @@ fn highlightSlashArgs(henv: ?*c.ic_highlight_env_t, text: []const u8, start: usi } } +pub fn setIdleCallback(fun: ?*const c.ic_idle_fun_t, arg: ?*anyopaque) void { + c.ic_set_idle_callback(fun, arg); +} + pub fn readLine(prompt: [*:0]const u8) ?[]const u8 { // Kitty-keyboard "disambiguate" (Ctrl+Enter as a distinct CSI-u, not bare // \r) only while isocline reads: while active, Ctrl-C arrives as a CSI-u diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 4423d9352..c355bee53 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -401,6 +401,20 @@ pub fn runner(self: *Session, opts: Runner.Opts) !Runner { return Runner.init(self, opts); } +/// Page transfers run on the session thread's curl multi; left unserviced +/// while the frontend waits on input they die on curl's wall-clock timeout. +/// Returns how long the caller may block before pumping again. +pub fn idleSlice(self: *Session) u31 { + const quiet_ms = 250; + self.processDestroyQueues(); + var r = self.runner(.{}) catch return quiet_ms; + const result = r.tick(.{ .ms = 25 }) catch return quiet_ms; + return switch (result) { + .done => quiet_ms, + .ok => |next_ms| @intCast(@min(next_ms, quiet_ms)), + }; +} + pub fn scheduleNavigation(self: *Session, frame: *Frame) !void { return self.currentPage().?.scheduleNavigation(frame); } diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index a2b33359c..9861a4f16 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -66,19 +66,8 @@ pub fn deinit(self: *Self) void { self.allocator.destroy(self); } -/// Pump the browser session for one short slice while the server waits on -/// input. Page transfers live on this thread's curl multi; left unserviced -/// between tool calls they die on curl's wall-clock timeout. Returns how -/// long the caller may block before pumping again. pub fn idle(self: *Self) u31 { - const quiet_ms = 250; - self.session.processDestroyQueues(); - var runner = self.session.runner(.{}) catch return quiet_ms; // no page yet - const result = runner.tick(.{ .ms = 25 }) catch return quiet_ms; - return switch (result) { - .done => quiet_ms, - .ok => |next_ms| @intCast(@min(next_ms, quiet_ms)), - }; + return self.session.idleSlice(); } pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {