1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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)]});
}
|