string: add string.isOneOf and deduplicate helpers

This commit is contained in:
Adrià Arrufat
2026-06-08 09:04:36 +02:00
parent 7bcab1d840
commit 2a7bda3608
3 changed files with 18 additions and 22 deletions

View File

@@ -36,7 +36,7 @@ const CDPNode = @import("../cdp/Node.zig");
const Terminal = @import("Terminal.zig");
const SlashCommand = @import("SlashCommand.zig");
const settings = @import("settings.zig");
const truncateUtf8 = @import("../string.zig").truncateUtf8;
const string = @import("../string.zig");
const Agent = @This();
@@ -186,7 +186,7 @@ fn reconcileModel(
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const ids: []const []const u8 = zenai.provider.listChatModelIds(allocator, arena.allocator(), llm.provider, llm.key, base_url) catch &.{};
if (ids.len == 0 or containsString(ids, desired)) return .{ .use = try allocator.dupe(u8, desired) };
if (ids.len == 0 or string.isOneOf(desired, ids)) return .{ .use = try allocator.dupe(u8, desired) };
if (!explicit) {
if (llm.provider != .ollama) return .{ .use = try allocator.dupe(u8, desired) };
@@ -684,7 +684,7 @@ fn handleVerbosity(self: *Agent, rest: []const u8) void {
return;
}
const level = std.meta.stringToEnum(Config.AgentVerbosity, rest) orelse {
self.terminal.printError("usage: /verbosity <low|medium|high> (got {s})", .{rest});
self.terminal.printError("usage: /verbosity " ++ Config.tagHint(Config.AgentVerbosity) ++ " (got {s})", .{rest});
return;
};
self.terminal.verbosity = level;
@@ -697,7 +697,7 @@ fn handleEffort(self: *Agent, rest: []const u8) void {
return;
}
const level = std.meta.stringToEnum(Config.Effort, rest) orelse {
self.terminal.printError("usage: /effort <none|minimal|low|medium|high|xhigh> (got {s})", .{rest});
self.terminal.printError("usage: /effort " ++ Config.tagHint(Config.Effort) ++ " (got {s})", .{rest});
return;
};
self.effort = level;
@@ -788,7 +788,7 @@ fn handleModel(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
}
const ids = completionModels(self, self.allocator);
// Empty list = fetch failed or unlisted local models; can't confirm, so allow.
if (ids.len != 0 and !containsString(ids, trimmed)) {
if (ids.len != 0 and !string.isOneOf(trimmed, ids)) {
self.terminal.printError("unknown model: {s} (Tab to list)", .{trimmed});
return;
}
@@ -797,13 +797,6 @@ fn handleModel(self: *Agent, _: std.mem.Allocator, rest: []const u8) void {
};
}
fn containsString(haystack: []const []const u8, needle: []const u8) bool {
for (haystack) |s| {
if (std.mem.eql(u8, s, needle)) return true;
}
return false;
}
/// Persist the current provider/model/effort/verbosity to `.lp-agent.zon` and report it
/// as "<label>: <value>", appending "(saved to …)" when the write succeeds. With no
/// model credentials it persists `provider = null` only when that's an intentional
@@ -1254,11 +1247,11 @@ fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) vo
.help => self.terminal.printInfo("/help [name] — show help for a command, or list all when [name] is omitted", .{}),
.quit => self.terminal.printInfo("/quit — exit the REPL", .{}),
.verbosity => self.terminal.printInfo(
"/verbosity <low|medium|high> — set REPL agent verbosity (currently: {s}). Bare /verbosity prints the level.",
"/verbosity " ++ Config.tagHint(Config.AgentVerbosity) ++ " — set REPL agent verbosity (currently: {s}). Bare /verbosity prints the level.",
.{@tagName(self.terminal.verbosity)},
),
.effort => self.terminal.printInfo(
"/effort <none|minimal|low|medium|high|xhigh> — set per-turn reasoning effort (currently: {s}); saved to {s}. Bare /effort prints the level.",
"/effort " ++ Config.tagHint(Config.Effort) ++ " — set per-turn reasoning effort (currently: {s}); saved to {s}. Bare /effort prints the level.",
.{ @tagName(self.effort), settings.remembered_path },
),
.usage => self.terminal.printInfo(
@@ -1744,7 +1737,7 @@ const tool_output_max_bytes: usize = 1 * 1024 * 1024;
fn capToolOutput(allocator: std.mem.Allocator, output: []const u8) []const u8 {
if (output.len <= tool_output_max_bytes) return output;
const prefix = truncateUtf8(output, tool_output_max_bytes);
const prefix = string.truncateUtf8(output, tool_output_max_bytes);
var suffix_buf: [64]u8 = undefined;
const suffix = std.fmt.bufPrint(&suffix_buf, "\n...[truncated, original {d} bytes]", .{output.len}) catch return prefix;
return std.mem.concat(allocator, u8, &.{ prefix, suffix }) catch prefix;

View File

@@ -24,6 +24,7 @@ const std = @import("std");
const lp = @import("lightpanda");
const browser_tools = lp.tools;
const BrowserTool = browser_tools.Tool;
const string = @import("../string.zig");
const Schema = @This();
@@ -405,7 +406,7 @@ fn buildHints(arena: std.mem.Allocator, required: []const []const u8, fields: []
if (fields.len == 0 and required.len == 0) return &.{};
var optional_count: usize = 0;
for (fields) |f| {
if (!containsName(required, f.name)) optional_count += 1;
if (!string.isOneOf(f.name, required)) optional_count += 1;
}
const out = try arena.alloc(HintSlot, required.len + optional_count);
var idx: usize = 0;
@@ -419,7 +420,7 @@ fn buildHints(arena: std.mem.Allocator, required: []const []const u8, fields: []
idx += 1;
}
for (fields) |f| {
if (containsName(required, f.name)) continue;
if (string.isOneOf(f.name, required)) continue;
out[idx] = .{
.name = f.name,
.required = false,
@@ -430,11 +431,6 @@ fn buildHints(arena: std.mem.Allocator, required: []const []const u8, fields: []
return out;
}
fn containsName(names: []const []const u8, target: []const u8) bool {
for (names) |n| if (std.mem.eql(u8, n, target)) return true;
return false;
}
fn fieldTypeOf(value: std.json.Value) FieldType {
if (value != .object) return .other;
const ty = value.object.get("type") orelse return .other;

View File

@@ -311,6 +311,13 @@ pub fn isAllWhitespace(text: []const u8) bool {
} else true;
}
/// True when `needle` is byte-equal to one of the strings in `haystack`.
pub fn isOneOf(needle: []const u8, haystack: []const []const u8) bool {
return for (haystack) |s| {
if (std.mem.eql(u8, s, needle)) break true;
} else false;
}
/// Largest prefix of `bytes` whose length is at most `max_bytes` and
/// ends on a UTF-8 codepoint boundary. Invalid sequences count as one
/// byte each so the function never loops.