diff --git a/docs/agent.md b/docs/agent.md index 37e2599f2..8fdb39b6f 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -307,7 +307,10 @@ See [agent-script.md](agent-script.md) for the full script format reference. the REPL, `/verbosity ` tunes the log level, `/effort ` sets the per-turn reasoning budget (saved to `.lp-agent.zon`), and `/usage` prints cumulative token usage - and the cache hit rate for the session. These are REPL-only and never recorded. + and the cache hit rate for the session — `/clear` forgets the conversation + (history, usage, recorded actions, node IDs) while keeping the loaded page and + cookies, and `/reset` goes further by also starting a fresh browser session, + dropping the page, cookies, storage, and history. These are REPL-only and never recorded. ``` > /goto https://example.com > /findElement role=button name=Submit diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 75722d109..bcda624e7 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -321,8 +321,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent try self.browser.init(app, .{}, null); errdefer self.browser.deinit(); - self.session = try self.browser.newSession(notification); - self.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel }; + try self.startSession(); self.ai_client = if (llm) |l| try zenai.provider.Client.init(allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running }) else null; errdefer if (self.ai_client) |c| c.deinit(allocator); @@ -361,6 +360,13 @@ pub fn deinit(self: *Agent) void { self.allocator.destroy(self); } +/// Create a fresh browser session and wire its cancel hook back to this agent +/// so Ctrl-C aborts in-flight page work. Used at startup and by `/reset`. +fn startSession(self: *Agent) !void { + self.session = try self.browser.newSession(self.notification); + self.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel }; +} + // Tool definitions are compile-time constant; project them once per process. var global_tools_storage: [browser_tools.tool_defs.len]ProviderTool = undefined; var global_tools_once = std.once(initGlobalTools); @@ -634,6 +640,8 @@ fn handleMeta(self: *Agent, arena: std.mem.Allocator, meta: *const SlashCommand. .verbosity => self.handleVerbosity(rest), .effort => self.handleEffort(rest), .usage => self.handleUsage(), + .clear => self.handleClear(), + .reset => self.handleReset(), .save => self.handleSave(arena, rest), .load => self.handleLoad(rest), .model => self.handleModel(arena, rest), @@ -690,6 +698,34 @@ fn handleUsage(self: *Agent) void { } } +/// Drop everything tied to the conversation: history (the system prompt +/// re-seeds lazily on the next turn), cumulative usage, the recorded action +/// buffer, and DOM node IDs. Shared by `/clear` and `/reset`. +fn clearConversation(self: *Agent) void { + self.rollbackMessages(0); + self.save_buffer.reset(); + self.total_usage = .{}; + self.node_registry.reset(); +} + +/// Forget the conversation while leaving the browser session live — the loaded +/// page stays put and cookies/logins are preserved. +fn handleClear(self: *Agent) void { + self.clearConversation(); + self.terminal.printInfo("Cleared conversation, usage, and node IDs. Page and cookies kept.", .{}); +} + +/// Full clean slate: everything `/clear` drops, plus a fresh browser session, +/// so the loaded page, cookies, storage, and history are gone too. +fn handleReset(self: *Agent) void { + self.startSession() catch |err| { + self.terminal.printError("reset failed: {s}", .{@errorName(err)}); + return; + }; + self.clearConversation(); + self.terminal.printInfo("Reset conversation and browser session. Page, cookies, and storage cleared.", .{}); +} + fn handleLoad(self: *Agent, rest: []const u8) void { const path = std.mem.trim(u8, rest, &std.ascii.whitespace); if (path.len == 0) { @@ -1173,6 +1209,14 @@ fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) vo "/usage — show cumulative token usage and cache hit rate for this session", .{}, ), + .clear => self.terminal.printInfo( + "/clear — forget the conversation (history, usage, recorded actions, node IDs); keeps the loaded page and cookies", + .{}, + ), + .reset => self.terminal.printInfo( + "/reset — full reset: everything /clear does plus a new browser session, dropping the page, cookies, storage, and history", + .{}, + ), .save => self.terminal.printInfo( "/save [filename.js] [prompt] — save the session to [filename.js] (a random session-*.js if omitted). With an LLM, synthesizes an idiomatic script from the session and the optional prompt; with --no-llm, dumps the recorded actions verbatim.", .{}, diff --git a/src/agent/SlashCommand.zig b/src/agent/SlashCommand.zig index 51c2f5cc7..74263a47d 100644 --- a/src/agent/SlashCommand.zig +++ b/src/agent/SlashCommand.zig @@ -47,7 +47,7 @@ pub const MetaCommand = struct { /// Dispatched by `Agent.handleMeta` via an exhaustive switch so adding /// a new meta command is a compile error until it's wired up there too. - const Tag = enum { help, quit, verbosity, effort, usage, save, load, model, provider }; + const Tag = enum { help, quit, verbosity, effort, usage, clear, reset, save, load, model, provider }; }; pub const meta_commands = [_]MetaCommand{ @@ -56,6 +56,8 @@ pub const meta_commands = [_]MetaCommand{ .{ .tag = .verbosity, .name = "verbosity", .hint = "", .values = &.{ "low", "medium", "high" }, .description = "Set agent verbosity" }, .{ .tag = .effort, .name = "effort", .hint = "", .values = &.{ "none", "minimal", "low", "medium", "high", "xhigh" }, .description = "Set per-turn reasoning effort" }, .{ .tag = .usage, .name = "usage", .hint = "", .values = &.{}, .description = "Show token usage and cache stats for this session" }, + .{ .tag = .clear, .name = "clear", .hint = "", .values = &.{}, .description = "Clear conversation history and usage (keeps page/cookies)" }, + .{ .tag = .reset, .name = "reset", .hint = "", .values = &.{}, .description = "Reset conversation and browser session (drops page/cookies)" }, .{ .tag = .save, .name = "save", .hint = "[filename.js] [prompt]", .values = &.{}, .description = "Save this session to a file" }, .{ .tag = .load, .name = "load", .hint = "", .values = &.{}, .description = "Load and run a script from disk" }, .{ .tag = .model, .name = "model", .hint = "[name]", .values = &.{}, .description = "Change the model" },