Repository navigation
Expand file tree
/
Copy pathcommands.zig
More file actions
125 lines (108 loc) · 5.16 KB
/
Copy pathcommands.zig
File metadata and controls
125 lines (108 loc) · 5.16 KB
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
const std = @import("std");
const Allocator = std.mem.Allocator;
/// A build without tracing still lists the command, because the user who types
/// it deserves to be told why nothing was recorded. What the row says is the
/// difference: the command itself is one sentence in such a binary.
const tracing = @import("../debug/trace.zig").enabled;
/// One REPL command offered to the user.
pub const Command = struct {
/// Including the leading slash, exactly as it has to be typed.
name: []const u8,
description: []const u8,
};
/// Every command the REPL understands, in the order Tab offers them. The
/// dispatcher in main.zig is the authority on behaviour; this table is what the
/// user is shown, so the two are kept in step by hand.
pub const all = [_]Command{
.{ .name = "/help", .description = "Show this help message" },
.{ .name = "/clear", .description = "Clear conversation history" },
.{ .name = "/new", .description = "Start a new session on a cleared screen" },
.{ .name = "/session", .description = "Show the current session ID and resume command" },
.{ .name = "/plan", .description = "Show the plan: /plan [clear]" },
.{ .name = "/memory", .description = "List saved memories" },
.{ .name = "/remember", .description = "Save a note to memory: /remember <text>" },
.{ .name = "/forget", .description = "Remove a saved note: /forget <n|all>" },
.{ .name = "/model", .description = "Show current model" },
.{ .name = "/status", .description = "Show token usage and account quota" },
.{ .name = "/mcp", .description = "List the MCP servers and their tools" },
.{ .name = "/debug", .description = if (tracing)
"Record every step and write an HTML report"
else
"Not in this build; rebuild with -Dtrace to record a run" },
.{ .name = "/quit", .description = "Exit zagent" },
.{ .name = "/exit", .description = "Exit zagent" },
};
/// A line that is still a command being typed: it starts with a slash and has
/// not reached a space yet. Once a space appears the user is writing a query,
/// and completing it would only be in the way.
fn isCommandPrefix(buffer: []const u8) bool {
if (buffer.len == 0 or buffer[0] != '/') return false;
return std.mem.indexOfAny(u8, buffer, " \t") == null;
}
/// Completions for Tab. The line is a command in progress, so every command
/// starting with it is a candidate; the whole line is replaced by the one the
/// user picks. An empty list (Tab on ordinary text) makes linenoise beep and
/// leave the line alone.
pub fn complete(allocator: Allocator, buffer: []const u8) Allocator.Error![]const []const u8 {
var matches: std.ArrayList([]const u8) = .empty;
errdefer {
for (matches.items) |match| allocator.free(match);
matches.deinit(allocator);
}
if (isCommandPrefix(buffer)) {
for (all) |command| {
if (std.mem.startsWith(u8, command.name, buffer)) {
const owned = try allocator.dupe(u8, command.name);
errdefer allocator.free(owned);
try matches.append(allocator, owned);
}
}
}
return matches.toOwnedSlice(allocator);
}
/// Free a completion list handed out by `complete`, matching how linenoise
/// disposes of one.
fn freeCompletions(allocator: Allocator, matches: []const []const u8) void {
for (matches) |match| allocator.free(match);
allocator.free(matches);
}
test "command completions clean up every allocation failure" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, struct {
fn run(allocator: Allocator) !void {
const matches = try complete(allocator, "/");
defer freeCompletions(allocator, matches);
try std.testing.expectEqual(all.len, matches.len);
}
}.run, .{});
}
test "complete offers every command matching the prefix" {
const allocator = std.testing.allocator;
const slash = try complete(allocator, "/");
defer freeCompletions(allocator, slash);
try std.testing.expectEqual(all.len, slash.len);
const c = try complete(allocator, "/c");
defer freeCompletions(allocator, c);
try std.testing.expectEqual(@as(usize, 1), c.len);
try std.testing.expectEqualStrings("/clear", c[0]);
const e = try complete(allocator, "/e");
defer freeCompletions(allocator, e);
try std.testing.expectEqual(@as(usize, 1), e.len);
try std.testing.expectEqualStrings("/exit", e[0]);
}
test "status has a unique completion" {
const a = std.testing.allocator;
const matches = try complete(a, "/st");
defer freeCompletions(a, matches);
try std.testing.expectEqual(@as(usize, 1), matches.len);
try std.testing.expectEqualStrings("/status", matches[0]);
}
test "complete leaves ordinary text and finished commands alone" {
const allocator = std.testing.allocator;
const none = try complete(allocator, "hello");
defer freeCompletions(allocator, none);
try std.testing.expectEqual(@as(usize, 0), none.len);
// A space means the line is a query, not a command.
const query = try complete(allocator, "/model please");
defer freeCompletions(allocator, query);
try std.testing.expectEqual(@as(usize, 0), query.len);
}