diff options
Diffstat (limited to 'src/builtin_commands.zig')
| -rw-r--r-- | src/builtin_commands.zig | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/src/builtin_commands.zig b/src/builtin_commands.zig new file mode 100644 index 0000000..8f9f851 --- /dev/null +++ b/src/builtin_commands.zig @@ -0,0 +1,169 @@ +//! Builtin TUI slash commands: `/help`, `/quit`, `/model`, `/reasoning`, +//! `/new`. Registered by `main.zig` alongside `/compact` +//! (`compaction.zig`). These steer the live TUI, so their handlers cast +//! `Context.tui` back to the `*tui_app.App` installed at bring-up; output +//! goes through `ctx.stdout` (the transcript capture buffer). + +const std = @import("std"); +const panto = @import("panto"); +const command = @import("command.zig"); +const tui_app = @import("tui_app.zig"); +const selectors_mod = @import("tui_selectors.zig"); + +pub fn register(registry: *command.Registry) !void { + try registry.register(.{ + .name = "help", + .description = "List commands and key bindings.", + .run = runHelp, + }); + try registry.register(.{ + .name = "quit", + .description = "Exit panto.", + .run = runQuit, + }); + try registry.register(.{ + .name = "model", + .description = "Open the model selector, or switch directly: /model <provider:alias>.", + .run = runModel, + }); + try registry.register(.{ + .name = "reasoning", + .description = "Open the reasoning selector, or set directly: /reasoning <level>.", + .run = runReasoning, + }); + try registry.register(.{ + .name = "new", + .description = "Start a fresh session in place (new agent, extensions reloaded).", + .run = runNew, + }); + try registry.register(.{ + .name = "resume", + .description = "Switch sessions: /resume opens a picker, /resume <id> resolves a prefix.", + .run = runResume, + }); + try registry.register(.{ + .name = "status", + .description = "Show provider, model, reasoning, session, and context usage.", + .run = runStatus, + }); +} + +fn theApp(ctx: *command.Context) !*tui_app.App { + const ptr = ctx.tui orelse return error.NoTui; + return @ptrCast(@alignCast(ptr)); +} + +/// `/help` — the command list straight from the registry (so it can't rot) +/// plus the key bindings table maintained next to the chord dispatch. +fn runHelp(_: []const u8, ctx: *command.Context) anyerror!void { + const reg = ctx.registry orelse return error.NoRegistry; + var width: usize = 0; + for (reg.list()) |cmd| width = @max(width, cmd.name.len); + + try ctx.stdout.writeAll("commands:\n"); + for (reg.list()) |cmd| { + try ctx.stdout.print(" /{[n]s:<[w]}{[d]s}\n", .{ .n = cmd.name, .w = width + 2, .d = cmd.description }); + } + + width = 0; + for (tui_app.key_bindings) |kb| width = @max(width, kb.chord.len); + try ctx.stdout.writeAll("\nkeys:\n"); + for (tui_app.key_bindings) |kb| { + try ctx.stdout.print(" {[c]s:<[w]}{[d]s}\n", .{ .c = kb.chord, .w = width + 2, .d = kb.desc }); + } +} + +/// `/quit` — the dispatch site in `tui_app.handleSubmittedLine` re-raises +/// `error.UserExit`. +fn runQuit(_: []const u8, _: *command.Context) anyerror!void { + return error.UserExit; +} + +fn runModel(args: []const u8, ctx: *command.Context) anyerror!void { + const app = try theApp(ctx); + const ctrl = app.selectors orelse return error.NoSelectors; + if (args.len == 0) return ctrl.openModel(); + ctrl.setModelByName(args) catch |err| switch (err) { + error.UnknownModel => try ctx.stdout.print( + "unknown model '{s}' (/model with no argument lists them)\n", + .{args}, + ), + error.AmbiguousModel => try ctx.stdout.print( + "ambiguous alias '{s}': use provider:alias\n", + .{args}, + ), + else => return err, + }; +} + +fn runReasoning(args: []const u8, ctx: *command.Context) anyerror!void { + const app = try theApp(ctx); + const ctrl = app.selectors orelse return error.NoSelectors; + if (args.len == 0) return ctrl.openReasoning(); + ctrl.setReasoningByLabel(args) catch |err| switch (err) { + error.UnknownReasoning => { + try ctx.stdout.print("unknown reasoning level '{s}'; available:", .{args}); + for (selectors_mod.ReasoningOption.forStyle(ctrl.live.provider.style())) |opt| { + try ctx.stdout.print(" {s}", .{opt.label}); + } + try ctx.stdout.writeAll("\n"); + }, + else => return err, + }; +} + +fn runResume(args: []const u8, ctx: *command.Context) anyerror!void { + const app = try theApp(ctx); + const ctrl = app.selectors orelse return error.NoSelectors; + if (args.len == 0) return ctrl.openSessions(); + + const store = ctx.session_store; + const maybe = store.resolve(args) catch |err| switch (err) { + error.AmbiguousSessionId => return printAmbiguousSessions(store, args, ctx.stdout), + else => return err, + }; + const session = maybe orelse { + try ctx.stdout.print("no session matching '{s}'\n", .{args}); + return; + }; + try ctrl.resumeTo(session); +} + +/// Print the session ids matching `prefix` (shared by `/resume` and the +/// `--resume` path in `main.zig` for ambiguity reporting). +pub fn printAmbiguousSessions(store: panto.SessionStore, prefix: []const u8, out: *std.Io.Writer) !void { + try out.print("'{s}' matches multiple sessions:\n", .{prefix}); + const infos = try store.list(); + defer store.freeSessionInfos(infos); + for (infos) |info| { + if (!std.mem.startsWith(u8, info.id, prefix)) continue; + try out.print(" {s}\n", .{info.id}); + } +} + +fn runStatus(_: []const u8, ctx: *command.Context) anyerror!void { + const app = try theApp(ctx); + const ctrl = app.selectors orelse return error.NoSelectors; + try ctx.stdout.print("model: {s}\n", .{ctrl.model_label}); + const r = selectors_mod.ReasoningOption.activeLabel(ctrl.live.provider); + try ctx.stdout.print("reasoning: {s}\n", .{if (r.len != 0) r else "default"}); + try ctx.stdout.print("session: {s}\n", .{ctx.agent.sessionId()}); + try ctx.stdout.print("messages: {d}\n", .{ctx.agent.conversation.messages.items.len}); + if (app.footer.context_window) |w| { + try ctx.stdout.print("context: {d} / {d} tokens\n", .{ app.footer.context_tokens orelse 0, w }); + } else if (app.footer.context_tokens) |t| { + try ctx.stdout.print("context: {d} tokens\n", .{t}); + } +} + +/// `/new` — fresh session in place, boot-equivalent: mint a session from +/// the store and rebuild the whole session world (fresh Agent, fresh Lua +/// runtime; the system prompt is re-derived from the prompt layers + +/// extension activation, exactly as a relaunch would). The rebuild hook +/// repoints `ctx.agent`, so the id printed below is the fresh session's. +fn runNew(_: []const u8, ctx: *command.Context) anyerror!void { + const app = try theApp(ctx); + try app.rebuildSession(ctx.session_store.create(), null); + const sid = ctx.agent.sessionId(); + try ctx.stdout.print("[new session {s}]\n", .{sid[0..@min(8, sid.len)]}); +} |
