summaryrefslogtreecommitdiff
path: root/src/subcommand.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-04 09:50:12 -0600
committert <t@tjp.lol>2026-07-05 16:23:49 -0600
commitf759f149377942c4e04802c45162cda1c9bfb2b3 (patch)
tree397dd2fc35839d8fcbf6a5c237bee363c6ed3c07 /src/subcommand.zig
parent1ed07e2e4473b91c669c062bbfef6bb499f7d2b7 (diff)
big cli/tui gaps project
Diffstat (limited to 'src/subcommand.zig')
-rw-r--r--src/subcommand.zig133
1 files changed, 107 insertions, 26 deletions
diff --git a/src/subcommand.zig b/src/subcommand.zig
index 1372d72..46df8e0 100644
--- a/src/subcommand.zig
+++ b/src/subcommand.zig
@@ -30,6 +30,7 @@ const panto_home = @import("panto_home.zig");
const panto = @import("panto");
const c = lua_bridge.c;
+const versions = @import("versions");
pub const Action = enum {
/// Continue with the default agent REPL.
@@ -93,6 +94,13 @@ pub fn dispatch(
try runAuthSubcommand(allocator, io, environ_map, &it);
return .done;
}
+ if (std.mem.eql(u8, sub, "--version")) {
+ var buffer: [64]u8 = undefined;
+ var stdout_file = std.Io.File.stdout().writer(io, &buffer);
+ try stdout_file.interface.print("panto {s}\n", .{versions.panto_version});
+ try stdout_file.flush();
+ return .done;
+ }
if (std.mem.eql(u8, sub, "--help") or std.mem.eql(u8, sub, "-h") or std.mem.eql(u8, sub, "help")) {
try printHelp(io);
return .done;
@@ -111,6 +119,23 @@ fn printHelp(io: Io) !void {
\\ panto Start a new conversation.
\\ panto --resume Resume the most recent conversation in this directory.
\\ panto --resume <id> Resume the conversation whose id begins with <id>.
+ \\ panto -c, --continue Alias for bare --resume.
+ \\ panto -p, --print [<prompt>]
+ \\ One-shot non-interactive mode: run one agent turn,
+ \\ print the assistant text, exit 0/1. The prompt comes
+ \\ from the argument or piped stdin. Composes with
+ \\ --resume/-c and -m.
+ \\ panto -m, --model <provider:alias>
+ \\ Override the model for this run. A bare alias is
+ \\ accepted when it is unique across providers.
+ \\ panto --effort <level> Override the reasoning level for this run (the
+ \\ Ctrl+R picker's levels, e.g. low/medium/high).
+ \\ Beats models.toml knobs and [defaults] reasoning.
+ \\ panto --no-extensions Skip the Lua/luarocks runtime entirely. Built-ins
+ \\ only; since the shipped tools are extensions too,
+ \\ the agent runs with no tools. Escape hatch when an
+ \\ extension breaks startup; fast -p in CI.
+ \\ panto --version Print the panto version.
\\ panto sessions List saved sessions for this directory.
\\ panto models sync Fetch models.dev and rebuild the base models.toml.
\\ panto auth status Show configured auth sessions and login state.
@@ -128,11 +153,12 @@ fn printHelp(io: Io) !void {
\\ $XDG_DATA_HOME/panto/config.toml (base; auto-generated)
\\ $XDG_CONFIG_HOME/panto/config.toml (user)
\\ ./.panto/config.toml (project)
- \\ Define providers under [providers.<name>], pick a default with
- \\ [defaults] model = "<provider>:<alias>", and gate extensions with
- \\ [extensions] allow/deny globs (plus [extensions] paths/rocks to add
- \\ sources). Model aliases (wire name, reasoning, max_tokens, pricing)
- \\ live in models.toml.
+ \\ Define providers under [providers.<name>], pick defaults with
+ \\ [defaults] model = "<provider>:<alias>" and reasoning = "<level>",
+ \\ set TUI knobs under [tui] (editor = "...", tools_collapsed = bool),
+ \\ and gate extensions with [extensions] allow/deny globs (plus
+ \\ [extensions] paths/rocks to add sources). Model aliases (wire name,
+ \\ reasoning, max_tokens, pricing) live in models.toml.
\\
\\Environment:
\\ OPENAI_API_KEY, ANTHROPIC_API_KEY Consumed by the default providers.
@@ -335,12 +361,13 @@ fn runUpdateSubcommand(
// `panto sessions`
// ---------------------------------------------------------------------------
-/// List sessions for the current working directory.
+/// List sessions for the current working directory, newest-modified first:
///
-/// Output format (one session per line):
-/// <short-id> <created> <message-count> messages
+/// ID MODIFIED MSGS MODEL LAST MESSAGE
+/// 0197c2a4 2026-07-01 14:32 18 claude-sonnet-4 fix the segfault in…
///
-/// where `<short-id>` is the first 8 hex chars of the session UUIDv7.
+/// IDs are shortened to the shortest prefix (≥8 chars) that is unambiguous
+/// across the listing, so any printed ID pastes into `--resume` directly.
fn runSessionsSubcommand(
allocator: Allocator,
io: Io,
@@ -365,32 +392,86 @@ fn runSessionsSubcommand(
const stdout = &stdout_file.interface;
if (infos.len == 0) {
- try stdout.print("no sessions for {s}\n", .{cwd});
+ try stdout.print("no sessions for {s}\n{s}\n", .{ cwd, session_dir });
try stdout_file.flush();
return;
}
+ // Shortest prefix (≥8) that disambiguates every pair of IDs. UUIDv7
+ // leads with a millisecond timestamp, so 8 chars alone can collide.
+ var id_w: usize = 8;
+ for (infos) |a| {
+ for (infos) |b| {
+ if (a.id.ptr == b.id.ptr) continue;
+ const cp = std.mem.indexOfDiff(u8, a.id, b.id) orelse a.id.len;
+ id_w = @max(id_w, @min(cp + 1, a.id.len));
+ }
+ }
+
+ var model_w: usize = "MODEL".len;
+ for (infos) |info| model_w = @max(model_w, @min(info.model.len, 24));
+
+ const term_w: usize = blk: {
+ const cols = environ_map.get("COLUMNS") orelse break :blk 120;
+ break :blk std.fmt.parseInt(usize, cols, 10) catch 120;
+ };
+ const modified_w = "YYYY-MM-DD HH:MM".len;
+ const msgs_w = "MSGS".len;
+ const fixed = id_w + 2 + modified_w + 2 + msgs_w + 2 + model_w + 2;
+ const msg_w = term_w -| fixed;
+
+ try stdout.print("{[id]s:<[idw]} {[mod]s:<[modw]} MSGS {[model]s:<[modelw]} LAST MESSAGE\n", .{
+ .id = "ID",
+ .idw = id_w,
+ .mod = "MODIFIED",
+ .modw = modified_w,
+ .model = "MODEL",
+ .modelw = model_w,
+ });
+
for (infos) |info| {
- const short = info.id[0..@min(8, info.id.len)];
- // `created` is ISO 8601 (e.g. `2026-04-25T17:40:15.990Z`). Trim
- // to `YYYY-MM-DD HH:MM` for terseness.
- const created_short = trimCreated(info.created);
- try stdout.print(
- "{s} {s} {d} messages\n",
- .{ short, created_short, info.message_count },
- );
+ var ts_buf: [16]u8 = undefined;
+ try stdout.print("{[id]s:<[idw]} {[mod]s:<[modw]} {[n]d:>[nw]} {[model]s:<[modelw]} ", .{
+ .id = info.id[0..@min(id_w, info.id.len)],
+ .idw = id_w,
+ .mod = trimIso(info.modified, &ts_buf),
+ .modw = modified_w,
+ .n = info.message_count,
+ .nw = msgs_w,
+ .model = truncateUtf8(info.model, model_w),
+ .modelw = model_w,
+ });
+
+ // Last user message: newlines/tabs flattened, truncated to width.
+ const flat = try allocator.dupe(u8, info.last_user_message);
+ defer allocator.free(flat);
+ for (flat) |*ch| switch (ch.*) {
+ '\n', '\r', '\t' => ch.* = ' ',
+ else => {},
+ };
+ const shown = truncateUtf8(flat, msg_w);
+ try stdout.writeAll(shown);
+ if (shown.len < flat.len) try stdout.writeAll("…");
+ try stdout.writeByte('\n');
}
+ try stdout.print("{s}\n", .{session_dir});
try stdout_file.flush();
}
-fn trimCreated(iso: []const u8) []const u8 {
- if (iso.len < 16) return iso;
- // `YYYY-MM-DDTHH:MM:...` → `YYYY-MM-DD HH:MM` (T → space).
- // We can't mutate a borrowed slice, so just return a 16-byte slice
- // of the original; the caller prints character-by-character via
- // format, so the 'T' will still appear. Use a small buffer trick:
- // return the slice unmodified — the 'T' is fine and unambiguous.
- return iso[0..16];
+/// ISO 8601 → `YYYY-MM-DD HH:MM` (T → space); shorter input passes through.
+pub fn trimIso(ts: []const u8, buf: *[16]u8) []const u8 {
+ if (ts.len < 16) return ts;
+ @memcpy(buf, ts[0..16]);
+ buf[10] = ' ';
+ return buf;
+}
+
+/// Truncate to at most `max` bytes without splitting a UTF-8 sequence.
+fn truncateUtf8(s: []const u8, max: usize) []const u8 {
+ if (s.len <= max) return s;
+ var end = max;
+ while (end > 0 and (s[end] & 0xC0) == 0x80) end -= 1;
+ return s[0..end];
}
// ---------------------------------------------------------------------------