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.
This commit is contained in:
Adrià Arrufat committed 2026-06-11 18:19:51 +02:00
1 parent 30c8bcbdd9
commit d292f2b342
5 files changed
+28 -14

No files matched your search

+2 -2
View File
@@ -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 = .{""},
+7
View File
@@ -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 {
+4
View File
@@ -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
+14
View File
@@ -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);
}
+1 -12
View File
@@ -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 {