diff --git a/bridge/main.zig b/bridge/main.zig index 19d8836..6d076e6 100644 --- a/bridge/main.zig +++ b/bridge/main.zig @@ -320,7 +320,7 @@ fn takeLine(r: *Io.Reader, arena: std.mem.Allocator) ![]u8 { fn connectionPath(arena: std.mem.Allocator, env: *const std.process.Environ.Map, profile: ?[]const u8) ![]const u8 { const tail = [_][]const u8{ "plugins", "agent", "connection.zon" }; if (profile) |p| return std.fs.path.join(arena, &.{ p, tail[0], tail[1], tail[2] }); - const os = @import("builtin").os.tag; + const os = @import("builtin").target.os.tag; const base: []const u8 = switch (os) { .macos => try std.fs.path.join(arena, &.{ env.get("HOME") orelse return error.NoHome, "Library", "Application Support" }), .windows => env.get("LOCALAPPDATA") orelse return error.NoLocalAppData, @@ -365,7 +365,8 @@ pub const Bridge = struct { const Connection = struct { socket: []const u8, token: []const u8 }; const source = Io.Dir.cwd().readFileAllocOptions(self.io, self.connection_path, arena, .limited(64 * 1024), .of(u8), 0) catch return error.FizzyNotRunning; - const c = std.zon.parse.fromSliceAlloc(Connection, arena, source, null, .{ .ignore_unknown_fields = true }) catch + var diag: std.zon.parse.Diagnostics = undefined; + const c = std.zon.parse.fromSlice(Connection, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return error.BadConnectionFile; const addr = try Io.net.UnixAddress.init(c.socket); const stream = addr.connect(self.io) catch return error.FizzyNotRunning; @@ -421,16 +422,18 @@ pub const Bridge = struct { if (!self.probe(arena)) { if (!self.reconnect(arena)) return null; } - const source = arena.dupeZ(u8, self.hello orelse return null) catch return null; + const source = arena.dupeSentinel(u8, self.hello orelse return null, 0) catch return null; const Hello = struct { executable: []const u8 = "" }; - const h = std.zon.parse.fromSliceAlloc(Hello, arena, source, null, .{ .ignore_unknown_fields = true }) catch return null; + var diag: std.zon.parse.Diagnostics = undefined; + const h = std.zon.parse.fromSlice(Hello, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return null; return if (h.executable.len > 0) h.executable else null; } /// The token `connection.zon` holds now: a new one is a new fizzy. pub fn connectionToken(self: *Bridge, arena: std.mem.Allocator) ?[]const u8 { const source = Io.Dir.cwd().readFileAllocOptions(self.io, self.connection_path, arena, .limited(64 * 1024), .of(u8), 0) catch return null; - const c = std.zon.parse.fromSliceAlloc(struct { token: []const u8 }, arena, source, null, .{ .ignore_unknown_fields = true }) catch return null; + var diag: std.zon.parse.Diagnostics = undefined; + const c = std.zon.parse.fromSlice(struct { token: []const u8 }, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return null; return c.token; } @@ -449,8 +452,9 @@ pub const Bridge = struct { try w.writeByte('\n'); try w.flush(); const line = try takeLine(&link.reader.interface, arena); - const source = try arena.dupeZ(u8, std.mem.trimEnd(u8, line, "\r")); - const r = try std.zon.parse.fromSliceAlloc(Wire.Response, arena, source, null, .{ .ignore_unknown_fields = true }); + const source = try arena.dupeSentinel(u8, std.mem.trimEnd(u8, line, "\r"), 0); + var diag: std.zon.parse.Diagnostics = undefined; + const r = try std.zon.parse.fromSlice(Wire.Response, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }); if (r.err) |e| return .{ .err = e }; return .{ .ok = r.ok orelse "" }; } @@ -590,7 +594,7 @@ pub const Bridge = struct { const b64 = try arena.alloc(u8, std.base64.standard.Encoder.calcSize(png.len)); _ = std.base64.standard.Encoder.encode(b64, png); const note = try std.fmt.allocPrint(arena, "{s}\n{d}x{d} (from {d}x{d}); fizzy {d} ms, here {d} ms, {d} KiB", .{ - path, size.w, size.h, cw, ch, + path, size.w, size.h, cw, ch, (t1 - t0) / std.time.ns_per_ms, (t2 - t1) / std.time.ns_per_ms, png.len / 1024, }); return reply(out, id, .{ .result = .{ @@ -616,7 +620,8 @@ pub const Bridge = struct { rect: struct { x: f64 = 0, y: f64 = 0, w: f64 = 0, h: f64 = 0 } = .{}, window: ?u32 = null, }; - const nodes = try std.zon.parse.fromSliceAlloc([]const Node, arena, try arena.dupeZ(u8, text), null, .{ .ignore_unknown_fields = true }); + var diag: std.zon.parse.Diagnostics = undefined; + const nodes = try std.zon.parse.fromSlice([]const Node, .{ .gpa = arena, .arena = arena, .source = try arena.dupeSentinel(u8, text, 0), .diagnostics = &diag, .ignore_unknown_fields = true }); const node = for (nodes) |n| { if (std.mem.eql(u8, n.tag, tag)) break n; } else return error.NoSuchTag; @@ -710,8 +715,9 @@ pub const Bridge = struct { .ok => |s| s, .err => return &.{}, }; - const source = arena.dupeZ(u8, list) catch return &.{}; - const commands = std.zon.parse.fromSliceAlloc([]const Listed, arena, source, null, .{ .ignore_unknown_fields = true }) catch return &.{}; + const source = arena.dupeSentinel(u8, list, 0) catch return &.{}; + var diag: std.zon.parse.Diagnostics = undefined; + const commands = std.zon.parse.fromSlice([]const Listed, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return &.{}; for (commands) |c| { if (!eql(c.id, id)) continue; var keys: std.ArrayListUnmanaged([]const u8) = .empty; @@ -790,9 +796,9 @@ fn reply(out: *Io.Writer, id: json.Value, body: anytype) !void { const B = @TypeOf(body); try out.writeAll("{\"jsonrpc\":\"2.0\",\"id\":"); try json.Stringify.value(id, .{}, out); - inline for (@typeInfo(B).@"struct".fields) |f| { - try out.writeAll(",\"" ++ f.name ++ "\":"); - try json.Stringify.value(@field(body, f.name), .{}, out); + inline for (@typeInfo(B).@"struct".field_names, @typeInfo(B).@"struct".field_types, @typeInfo(B).@"struct".field_attrs) |f_name, _, _| { + try out.writeAll(",\"" ++ f_name ++ "\":"); + try json.Stringify.value(@field(body, f_name), .{}, out); } try out.writeAll("}\n"); } @@ -871,7 +877,8 @@ test "JSON arguments read as ZON" { // And parse back as the plugin's types would. const Step = union(enum) { click: struct { tag: []const u8 }, type: []const u8, key: []const u8 }; const Args = struct { steps: []const Step, snapshot: bool = true }; - const parsed = try std.zon.parse.fromSliceAlloc(Args, a, try a.dupeZ(u8, w.written()), null, .{}); + var diag: std.zon.parse.Diagnostics = undefined; + const parsed = try std.zon.parse.fromSlice(Args, .{ .gpa = a, .arena = a, .source = try a.dupeSentinel(u8, w.written(), 0), .diagnostics = &diag }); try std.testing.expectEqualStrings("hi \"there\"\n", parsed.steps[1].type); try std.testing.expect(!parsed.snapshot); } diff --git a/build.zig b/build.zig index 1675350..1a5fc7c 100644 --- a/build.zig +++ b/build.zig @@ -18,7 +18,7 @@ pub fn build(b: *std.Build) void { // std-only: it relays between an MCP client on stdio and the plugin's socket, and makes the // pictures (scaling and encoding a frame fizzy hands over raw), so it is always built // optimized: a tool, not code anyone steps through, and the encode is 10x slower in Debug. - const bridge_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseFast else optimize; + const bridge_optimize: std.builtin.OptimizeMode = if (optimize == .debug) .fast else optimize; // The compiler's diagnostics, read the same way by the plugin's build loop and the bridge's. const diagnostics_module = b.createModule(.{ .target = target, @@ -38,15 +38,7 @@ pub fn build(b: *std.Build) void { // Beside the plugin's library too, `/agent/fizzy-mcp`, where the chat plugin looks // for it to hand an agent (ACP `session/new`'s `mcpServers`). Not on the web, which has no // plugins directory. - if (target.result.cpu.arch != .wasm32) { - const beside = b.allocator.create(BesidePlugin) catch @panic("OOM"); - beside.* = .{ - .step = std.Build.Step.init(.{ .id = .custom, .name = "install fizzy-mcp beside the plugin", .owner = b, .makeFn = BesidePlugin.make }), - .exe = bridge, - }; - beside.step.dependOn(&bridge.step); - b.getInstallStep().dependOn(&beside.step); - } + if (target.result.cpu.arch != .wasm32) fizzy.plugin.installBeside(b, bridge, .{ .plugin = "agent" }); const tests = b.addTest(.{ .root_module = b.createModule(.{ .target = target, @@ -63,47 +55,3 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addRunArtifact(tests).step); test_step.dependOn(&b.addRunArtifact(diagnostics).step); } - -/// Copies `fizzy-mcp` into `/agent/`, the directory `fizzy.plugin.install` puts the -/// library in: `$FIZZY_PROFILE/plugins` when that is set, else fizzy's own plugins directory. -/// Skipped, with a warning, where neither resolves (a bare CI runner). -const BesidePlugin = struct { - step: std.Build.Step, - exe: *std.Build.Step.Compile, - - fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void { - const self: *BesidePlugin = @fieldParentPtr("step", step); - const b = step.owner; - const io = b.graph.io; - const plugins = pluginsDir(b) orelse { - std.log.warn("agent: no plugins directory here; fizzy-mcp stays in zig-out/bin", .{}); - return; - }; - const dir = try std.fs.path.join(b.allocator, &.{ plugins, "agent" }); - std.Io.Dir.cwd().createDirPath(io, dir) catch {}; - const name = if (b.graph.host.result.os.tag == .windows) "fizzy-mcp.exe" else "fizzy-mcp"; - const dest = try std.fs.path.join(b.allocator, &.{ dir, name }); - const src = self.exe.getEmittedBin().getPath2(b, step); - const data = try std.Io.Dir.cwd().readFileAlloc(io, src, b.allocator, .limited(64 * 1024 * 1024)); - const tmp = try std.fmt.allocPrint(b.allocator, "{s}.part", .{dest}); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = tmp, .data = data, .flags = .{ .permissions = .fromMode(0o755) } }); - try std.Io.Dir.renameAbsolute(tmp, dest, io); - std.log.info("agent: installed fizzy-mcp → {s}", .{dest}); - } - - /// As `fizzy.plugin.install` resolves it (`sdk/plugin_sdk.zig`'s `fizzyPluginsDir`). - fn pluginsDir(b: *std.Build) ?[]const u8 { - const env = &b.graph.environ_map; - if (env.get("FIZZY_PROFILE")) |profile| if (profile.len > 0) - return std.fs.path.resolve(b.allocator, &.{ b.build_root.path orelse ".", profile, "plugins" }) catch null; - const join = std.fs.path.join; - return switch (b.graph.host.result.os.tag) { - .macos => join(b.allocator, &.{ env.get("HOME") orelse return null, "Library", "Application Support", "fizzy", "plugins" }) catch null, - .windows => join(b.allocator, &.{ env.get("LOCALAPPDATA") orelse return null, "fizzy", "plugins" }) catch null, - else => if (env.get("XDG_CONFIG_HOME")) |x| - join(b.allocator, &.{ x, "fizzy", "plugins" }) catch null - else - join(b.allocator, &.{ env.get("HOME") orelse return null, ".config", "fizzy", "plugins" }) catch null, - }; - } -}; diff --git a/build.zig.zon b/build.zig.zon index 51e7896..c894f3a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .agent, .version = "0.1.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", "plugin.zig", "plugin.zig.zon", "src", "bridge", "README.md" }, .fingerprint = 0x268b9c9dc0289c64, .dependencies = .{ @@ -10,8 +10,8 @@ // clean clone, and the release asset (never the tag's git archive) is the SDK // package alone. To build against a local fizzy instead: // `zig build --fork=/sdk`. - .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.28/fizzy-sdk-v0.2.28.tar.gz", - .hash = "fizzy-0.2.28-SGBwY_PuJQCcliz1OqCE9VlJoS204IsAExXIg75ejXhx", + .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.30/fizzy-sdk-v0.2.30.tar.gz", + .hash = "fizzy-0.2.30-SGBwY1QnJwDznUMVnSLATxXgH2e8_Vz7eMwLj9UQ0sVU", }, }, } diff --git a/plugin.zig b/plugin.zig index e7cef7f..8ce3271 100644 --- a/plugin.zig +++ b/plugin.zig @@ -59,7 +59,7 @@ fn initPlugin(_: *anyopaque) anyerror!void { const socket_path = if (beside.len < 100) beside else blk: { gpa.free(beside); // Windows has no `/tmp`, and its process ids are not `pid_t`s. - if (comptime builtin.os.tag == .windows) { + if (comptime builtin.target.os.tag == .windows) { const temp = if (std.c.getenv("TEMP")) |t| std.mem.span(t) else return error.NoTempDir; break :blk try std.fmt.allocPrint(gpa, "{s}\\fizzy-agent-{d}.sock", .{ temp, std.os.windows.GetCurrentProcessId() }); } @@ -83,7 +83,7 @@ fn initPlugin(_: *anyopaque) anyerror!void { fn writeConnection(dir: []const u8, socket_path: []const u8, token: []const u8) !void { const path = try std.fmt.allocPrint(host.allocator, "{s}/connection.zon", .{dir}); defer host.allocator.free(path); - const permissions: std.Io.File.Permissions = if (comptime builtin.os.tag == .windows) .default_file else .fromMode(0o600); + const permissions: std.Io.File.Permissions = if (comptime builtin.target.os.tag == .windows) .default_file else .fromMode(0o600); const file = try std.Io.Dir.cwd().createFile(dvui.io, path, .{ .permissions = permissions }); defer file.close(dvui.io); var buf: [1024]u8 = undefined; diff --git a/plugin.zig.zon b/plugin.zig.zon index f3bdfc5..69103be 100644 --- a/plugin.zig.zon +++ b/plugin.zig.zon @@ -1,7 +1,7 @@ .{ .id = "agent", .name = "Agent", - .version = "0.1.0", - .min_sdk_version = "0.2.28", + .version = "0.1.1", + .min_sdk_version = "0.2.30", .description = "Lets an AI agent drive fizzy as a person does: its commands as tools, the screen as text, input through live tapes, the profiler on request. Pair it with fizzy-mcp.", } diff --git a/src/Build.zig b/src/Build.zig index 259c55a..a5b15ce 100644 --- a/src/Build.zig +++ b/src/Build.zig @@ -61,19 +61,20 @@ pub fn start(gpa: std.mem.Allocator, host: *sdk.Host, plugins: *Plugins, conn: u var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); - const args = std.zon.parse.fromSliceAlloc(Args, a, source, null, .{ .ignore_unknown_fields = true }) catch return error.BadArguments; + var diag: std.zon.parse.Diagnostics = undefined; + const args = std.zon.parse.fromSlice(Args, .{ .gpa = a, .arena = a, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return error.BadArguments; if (!std.fs.path.isAbsolute(args.path)) return error.PathNotAbsolute; // Which plugin: its identity file, as the build helper reads it. const zon_path = try std.fs.path.join(a, &.{ args.path, "plugin.zig.zon" }); const zon = std.Io.Dir.cwd().readFileAllocOptions(dvui.io, zon_path, a, .limited(64 * 1024), .of(u8), 0) catch return error.NoPluginZon; const Identity = struct { id: []const u8 }; - const id = (std.zon.parse.fromSliceAlloc(Identity, a, zon, null, .{ .ignore_unknown_fields = true }) catch return error.NoPluginZon).id; + const id = (std.zon.parse.fromSlice(Identity, .{ .gpa = a, .arena = a, .source = zon, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return error.NoPluginZon).id; if (std.mem.eql(u8, id, "agent")) return error.CannotBuildItself; const plugins_dir = host.plugins_dir orelse return error.NoPluginsDir; const profile = std.fs.path.dirname(plugins_dir) orelse plugins_dir; - const ext = switch (builtin.os.tag) { + const ext = switch (builtin.target.os.tag) { .macos => "dylib", .windows => "dll", else => "so", @@ -153,7 +154,7 @@ fn build(self: *Build) void { } fn spawn(gpa: std.mem.Allocator, argv: []const []const u8, cwd: []const u8) !std.process.Child { - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { return core.darwin_spawn.spawn(gpa, .{ .argv = argv, .cwd = cwd, .stdin = .discard, .stdout = .discard, .stderr = .pipe }, core.shell_env.path(gpa, dvui.io)); } return std.process.spawn(dvui.io, .{ .argv = argv, .cwd = .{ .path = cwd }, .stdin = .ignore, .stdout = .ignore, .stderr = .pipe, .create_no_window = true }); @@ -250,7 +251,7 @@ fn checkOrFinish(self: *Build, host: *sdk.Host, auto: ?*Automation) ?[]const u8 var src: std.Io.Writer.Allocating = .init(self.arena.allocator()); // `Run.Step` nests (a `compare` holds steps), so the serializer is given a depth to stop at. std.zon.stringify.serializeMaxDepth(Run.Args{ .steps = self.args.check }, .{ .emit_default_optional_fields = false }, &src.writer, 32) catch return self.fail("could not write the check"); - const zon = self.arena.allocator().dupeZ(u8, src.written()) catch return self.fail("out of memory"); + const zon = self.arena.allocator().dupeSentinel(u8, src.written(), 0) catch return self.fail("out of memory"); const r = Run.init(self.gpa, self.conn, zon) catch return self.fail("the check is not a script ui.run takes"); self.phase = .{ .checking = r }; const report = r.advance(host, auto.?) orelse return null; diff --git a/src/Run.zig b/src/Run.zig index f7c79b7..58fb08c 100644 --- a/src/Run.zig +++ b/src/Run.zig @@ -137,7 +137,8 @@ pub const Args = struct { pub fn init(gpa: std.mem.Allocator, conn: u32, source: [:0]const u8) error{ BadScript, OutOfMemory }!*Run { var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); - const args = std.zon.parse.fromSliceAlloc(Args, arena.allocator(), source, null, .{ .ignore_unknown_fields = true }) catch + var diag: std.zon.parse.Diagnostics = undefined; + const args = std.zon.parse.fromSlice(Args, .{ .gpa = arena.allocator(), .arena = arena.allocator(), .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return error.BadScript; const self = try gpa.create(Run); self.* = .{ .arena = arena, .conn = conn, .steps = args.steps, .timeout_ms = args.timeout_ms, .report = .init(gpa) }; diff --git a/src/Server.zig b/src/Server.zig index 0c53522..8e67f38 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -18,7 +18,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Io = std.Io; -const has_wake_pipe = builtin.os.tag != .windows; +const has_wake_pipe = builtin.target.os.tag != .windows; gpa: std.mem.Allocator, io: Io, @@ -241,8 +241,9 @@ fn serve(self: *Server, stream: Io.net.Stream, conn_id: u32) void { defer arena.deinit(); const a = arena.allocator(); const line = takeLine(&reader.interface, a) catch return; - const source = a.dupeZ(u8, std.mem.trimEnd(u8, line, "\r")) catch return; - const wire = std.zon.parse.fromSliceAlloc(Wire.Request, a, source, null, .{ .ignore_unknown_fields = true }) catch { + const source = a.dupeSentinel(u8, std.mem.trimEnd(u8, line, "\r"), 0) catch return; + var diag: std.zon.parse.Diagnostics = undefined; + const wire = std.zon.parse.fromSlice(Wire.Request, .{ .gpa = a, .arena = a, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch { respond(&writer.interface, .{ .id = 0, .err = "not a request: one ZON value per line, .{ .id, .tool, .args }" }) catch return; continue; }; diff --git a/src/measure.zig b/src/measure.zig index 02e87cb..f693695 100644 --- a/src/measure.zig +++ b/src/measure.zig @@ -11,7 +11,7 @@ const core = @import("core"); /// The timer that wakes fizzy when a measurement ends. One is enough: one measurement waits at a /// time per frame, and the latest deadline wins. -const timer_id: dvui.Id = @enumFromInt(0x6167_6e74_6d65_6173); // "agntmeas" +const timer_id: dvui.Id = @fromBackingInt(@intCast(0x6167_6e74_6d65_6173)); // "agntmeas" /// Ask for a frame at `until` (boot-clock nanoseconds), however idle fizzy is meanwhile. Called each /// frame while waiting, which also keeps the timer alive (dvui drops one nobody asks after). diff --git a/src/tools.zig b/src/tools.zig index 711f2ab..949a0ac 100644 --- a/src/tools.zig +++ b/src/tools.zig @@ -178,7 +178,7 @@ pub fn agentCall(host: *sdk.Host, conn: u32, id: []const u8, args: []const u8, a "{s} acts on the active document, {s}, which is not one you are working on: it would take back the person's steps. Take back your own by clearing what you drew and drawing it again.", .{ id, active }, ) catch "undo acts on the active document, which is not yours" }; - const doc_path: ?[]const u8 = if (target(host, id, arena.dupeZ(u8, args) catch "", arena)) |doc| blk: { + const doc_path: ?[]const u8 = if (target(host, id, arena.dupeSentinel(u8, args, 0) catch "", arena)) |doc| blk: { const h = session(host, conn, doc, "An agent") catch break :blk null; break :blk arena.dupe(u8, h.path) catch null; } else null; @@ -284,7 +284,7 @@ fn handle(host: *sdk.Host, server: *Server, req: *Server.Request) void { var arena_state: std.heap.ArenaAllocator = .init(host.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); - const source = arena.dupeZ(u8, req.args) catch return req.answer(server, false, "out of memory"); + const source = arena.dupeSentinel(u8, req.args, 0) catch return req.answer(server, false, "out of memory"); const tool = req.tool; if (eql(tool, "hello")) { @@ -350,7 +350,7 @@ fn handle(host: *sdk.Host, server: *Server, req: *Server.Request) void { } if (eql(tool, "tape.play")) { return switch (auto.play(req.args)) { - .started => |t| req.answer(server, true, std.fmt.allocPrint(arena, ".{{ .ticket = {d} }}", .{@intFromEnum(t)}) catch "?"), + .started => |t| req.answer(server, true, std.fmt.allocPrint(arena, ".{{ .ticket = {d} }}", .{@backingInt(t)}) catch "?"), .busy => req.answer(server, false, "something else is driving fizzy: a tape or a demo"), .invalid => |why| req.answer(server, false, why), }; @@ -358,7 +358,7 @@ fn handle(host: *sdk.Host, server: *Server, req: *Server.Request) void { if (eql(tool, "tape.outcome")) { const a = parse(struct { ticket: u32 }, arena, source) orelse return req.answer(server, false, "tape.outcome takes .{ .ticket }"); var buf: [64]u8 = undefined; - return req.answer(server, true, outcomeText(&buf, auto.outcome(@enumFromInt(a.ticket)))); + return req.answer(server, true, outcomeText(&buf, auto.outcome(@fromBackingInt(@intCast(a.ticket))))); } if (eql(tool, "profile.report")) { // `drive`: ask for every frame, to measure what drawing costs; else only the frames fizzy @@ -503,7 +503,7 @@ fn act(host: *sdk.Host, server: *Server, req: *Server.Request, arena: std.mem.Al // `Run.Step` nests (a `compare` holds steps): the serializer is given a depth to stop at. std.zon.stringify.serializeMaxDepth(Run.Args{ .steps = steps.items, .timeout_ms = a.timeout_ms }, .{ .emit_default_optional_fields = false }, &out.writer, 32) catch return req.answer(server, false, "out of memory"); - const zon = arena.dupeZ(u8, out.written()) catch return req.answer(server, false, "out of memory"); + const zon = arena.dupeSentinel(u8, out.written(), 0) catch return req.answer(server, false, "out of memory"); const auto = automation(host) orelse return req.answer(server, false, "this app offers no automation service"); run(host, server, req, auto, zon); } @@ -573,8 +573,9 @@ fn listCommands(host: *sdk.Host, server: *Server, req: *Server.Request, arena: s } fn parse(comptime T: type, arena: std.mem.Allocator, source: [:0]const u8) ?T { - if (source.len == 0) return std.zon.parse.fromSliceAlloc(T, arena, ".{}", null, .{}) catch null; - return std.zon.parse.fromSliceAlloc(T, arena, source, null, .{ .ignore_unknown_fields = true }) catch null; + var diag: std.zon.parse.Diagnostics = undefined; + if (source.len == 0) return std.zon.parse.fromSlice(T, .{ .gpa = arena, .arena = arena, .source = ".{}", .diagnostics = &diag }) catch null; + return std.zon.parse.fromSlice(T, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true }) catch null; } fn eql(a: []const u8, b: []const u8) bool {