//! 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 .", .run = runModel, }); try registry.register(.{ .name = "reasoning", .description = "Open the reasoning selector, or set directly: /reasoning .", .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 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)]}); }