Add foreground Pi bridge panel
This commit is contained in:
@@ -7,6 +7,7 @@ const leader = @import("leader.zig");
|
||||
const lsp = @import("lsp.zig");
|
||||
const mobile_acceptance = @import("mobile_acceptance.zig");
|
||||
const panel = @import("panel.zig");
|
||||
const pi_bridge = @import("pi_bridge.zig");
|
||||
const protocol = @import("protocol.zig");
|
||||
const replay = @import("replay.zig");
|
||||
const repo = @import("repo.zig");
|
||||
@@ -191,6 +192,7 @@ test {
|
||||
_ = lsp;
|
||||
_ = mobile_acceptance;
|
||||
_ = panel;
|
||||
_ = pi_bridge;
|
||||
_ = protocol;
|
||||
_ = replay;
|
||||
_ = repo;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Foreground Pi/local assistant bridge over compact context.
|
||||
// req: session/002, session/004, governance/002, governance/003, testing/001, testing/002
|
||||
|
||||
test {
|
||||
_ = rowsAlloc;
|
||||
}
|
||||
|
||||
pub const Error = error{
|
||||
InvalidCommand,
|
||||
InvalidContext,
|
||||
};
|
||||
|
||||
pub fn rowsAlloc(allocator: std.mem.Allocator, io: std.Io, argv: []const []const u8, context: []const u8) ![][]const u8 {
|
||||
try validateArgv(argv);
|
||||
try validateContext(context);
|
||||
const full_argv = try allocator.alloc([]const u8, argv.len + 1);
|
||||
defer allocator.free(full_argv);
|
||||
@memcpy(full_argv[0..argv.len], argv);
|
||||
full_argv[argv.len] = context;
|
||||
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
for (rows.items) |row| allocator.free(row);
|
||||
rows.deinit(allocator);
|
||||
}
|
||||
const preview = try commandPreviewAlloc(allocator, argv);
|
||||
defer allocator.free(preview);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:spawned:{s}", .{preview}));
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:context:bytes_{d}", .{context.len}));
|
||||
|
||||
const result = std.process.run(allocator, io, .{
|
||||
.argv = full_argv,
|
||||
.stdout_limit = .limited(128 * 1024),
|
||||
.stderr_limit = .limited(64 * 1024),
|
||||
}) catch |err| {
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "pi:status:spawn_error_{s}", .{@errorName(err)}));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
};
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
try appendTermRow(allocator, &rows, result.term);
|
||||
try appendOutputRows(allocator, &rows, "stdout", result.stdout);
|
||||
try appendOutputRows(allocator, &rows, "stderr", result.stderr);
|
||||
if (rows.items.len == 3) try rows.append(allocator, try allocator.dupe(u8, "pi:output_empty"));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn validateArgv(argv: []const []const u8) !void {
|
||||
if (argv.len == 0) return Error.InvalidCommand;
|
||||
for (argv) |arg| {
|
||||
if (arg.len == 0 or !std.unicode.utf8ValidateSlice(arg)) return Error.InvalidCommand;
|
||||
for (arg) |byte| if (byte == 0 or byte == '\n' or byte == '\r') return Error.InvalidCommand;
|
||||
}
|
||||
}
|
||||
|
||||
fn validateContext(context: []const u8) !void {
|
||||
if (context.len == 0 or !std.unicode.utf8ValidateSlice(context)) return Error.InvalidContext;
|
||||
for (context) |byte| if (byte == 0) return Error.InvalidContext;
|
||||
}
|
||||
|
||||
fn appendTermRow(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), term: std.process.Child.Term) !void {
|
||||
const row = switch (term) {
|
||||
.exited => |code| try std.fmt.allocPrint(allocator, "pi:status:exit_{d}", .{code}),
|
||||
.signal => |sig| try std.fmt.allocPrint(allocator, "pi:status:signal_{d}", .{@intFromEnum(sig)}),
|
||||
.stopped => |sig| try std.fmt.allocPrint(allocator, "pi:status:stopped_{d}", .{@intFromEnum(sig)}),
|
||||
.unknown => |code| try std.fmt.allocPrint(allocator, "pi:status:unknown_{d}", .{code}),
|
||||
};
|
||||
try rows.append(allocator, row);
|
||||
}
|
||||
|
||||
fn appendOutputRows(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), prefix: []const u8, output: []const u8) !void {
|
||||
var lines = std.mem.splitScalar(u8, output, '\n');
|
||||
while (lines.next()) |raw_line| {
|
||||
const trimmed = std.mem.trim(u8, raw_line, "\r");
|
||||
if (trimmed.len == 0) continue;
|
||||
const sanitized = try sanitizeAlloc(allocator, trimmed, 120);
|
||||
defer allocator.free(sanitized);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:{s}", .{ prefix, sanitized }));
|
||||
}
|
||||
}
|
||||
|
||||
fn commandPreviewAlloc(allocator: std.mem.Allocator, argv: []const []const u8) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
for (argv, 0..) |arg, index| {
|
||||
if (index != 0) try out.append(allocator, '_');
|
||||
const clean = try sanitizeAlloc(allocator, arg, 40);
|
||||
defer allocator.free(clean);
|
||||
try out.appendSlice(allocator, clean);
|
||||
if (out.items.len >= 120) break;
|
||||
}
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn sanitizeAlloc(allocator: std.mem.Allocator, value: []const u8, max_len: usize) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
var i: usize = 0;
|
||||
while (i < value.len and out.items.len < max_len) {
|
||||
const len = std.unicode.utf8ByteSequenceLength(value[i]) catch 1;
|
||||
const end = @min(value.len, i + len);
|
||||
const slice = value[i..end];
|
||||
if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == '|')) {
|
||||
try out.append(allocator, '_');
|
||||
} else {
|
||||
try out.appendSlice(allocator, slice);
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
if (i < value.len) try out.appendSlice(allocator, "...");
|
||||
if (out.items.len == 0) try out.append(allocator, '_');
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn freeRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||
for (rows) |row| allocator.free(row);
|
||||
allocator.free(rows);
|
||||
}
|
||||
|
||||
test "regular: fake pi command receives context and emits output rows" {
|
||||
const context = "context:v1\ntask=fix_bug\n";
|
||||
const rows = try rowsAlloc(std.testing.allocator, std.testing.io, &.{ "sh", "-c", "case \"$1\" in *context:v1*) echo pi_ok;; *) echo missing; exit 2;; esac", "fake-pi" }, context);
|
||||
defer freeRows(std.testing.allocator, rows);
|
||||
try std.testing.expectEqualStrings("pi:status:exit_0", rows[2]);
|
||||
try std.testing.expectEqualStrings("stdout:pi_ok", rows[3]);
|
||||
}
|
||||
|
||||
test "adversarial: missing pi command and invalid input are explicit" {
|
||||
try std.testing.expectError(Error.InvalidCommand, rowsAlloc(std.testing.allocator, std.testing.io, &.{}, "context:v1\n"));
|
||||
try std.testing.expectError(Error.InvalidContext, rowsAlloc(std.testing.allocator, std.testing.io, &.{"true"}, ""));
|
||||
const rows = try rowsAlloc(std.testing.allocator, std.testing.io, &.{"definitely-not-a-mim-pi"}, "context:v1\n");
|
||||
defer freeRows(std.testing.allocator, rows);
|
||||
try std.testing.expect(std.mem.startsWith(u8, rows[2], "pi:status:spawn_error_"));
|
||||
}
|
||||
+64
@@ -1,8 +1,10 @@
|
||||
const std = @import("std");
|
||||
const context_mod = @import("context.zig");
|
||||
const input = @import("input.zig");
|
||||
const job_mod = @import("job.zig");
|
||||
const leader_mod = @import("leader.zig");
|
||||
const lsp_mod = @import("lsp.zig");
|
||||
const pi_bridge = @import("pi_bridge.zig");
|
||||
const protocol = @import("protocol.zig");
|
||||
const replay = @import("replay.zig");
|
||||
const repo_mod = @import("repo.zig");
|
||||
@@ -164,6 +166,7 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "lsp_signature ")) return self.openLspSignature(line[14..]);
|
||||
if (std.mem.eql(u8, line, "lsp_param_next")) return self.moveLspParameter(.next);
|
||||
if (std.mem.eql(u8, line, "lsp_param_previous")) return self.moveLspParameter(.previous);
|
||||
if (std.mem.startsWith(u8, line, "pi_run ")) return self.openPiRun(line[7..]);
|
||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||
@@ -537,6 +540,23 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openPiRun(self: *Client, command_line: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
defer argv.deinit(self.allocator);
|
||||
var parts = std.mem.splitScalar(u8, std.mem.trim(u8, command_line, " "), ' ');
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
try argv.append(self.allocator, part);
|
||||
}
|
||||
const context_text = try context_mod.sessionContextAlloc(self.allocator, &self.session, "pi_run", "bridge", .{});
|
||||
defer self.allocator.free(context_text);
|
||||
const rows = pi_bridge.rowsAlloc(self.allocator, io, argv.items, context_text) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("pi-output", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn jumpCurrentBufferToLspLocation(self: *Client, location: lsp_mod.DiagnosticLocation) !void {
|
||||
const snap = try self.session.snapshot();
|
||||
const bytes = try self.allocator.dupe(u8, snap.bytes);
|
||||
@@ -1954,3 +1974,47 @@ test "adversarial: malformed hover signature and non-call parameter movement are
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "lsp:param:outside_call") != null);
|
||||
}
|
||||
|
||||
fn makeTuiPiFixture(allocator: std.mem.Allocator) !struct { tmp: std.testing.TmpDir, script: []u8 } {
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
errdefer tmp.cleanup();
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "fake-pi.sh", .data = "case \"$1\" in *context:v1*) echo pi_ok;; *) echo missing; exit 2;; esac\n" });
|
||||
const script = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/fake-pi.sh", .{&tmp.sub_path});
|
||||
errdefer allocator.free(script);
|
||||
return .{ .tmp = tmp, .script = script };
|
||||
}
|
||||
|
||||
test "regular: pi bridge sends context to fake pi and renders output panel" {
|
||||
var fixture = try makeTuiPiFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.script);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 80, .height = 7 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open working buffer");
|
||||
const command = try std.fmt.allocPrint(std.testing.allocator, "pi_run sh {s}", .{fixture.script});
|
||||
defer std.testing.allocator.free(command);
|
||||
try client.handleTraceLine(command);
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "pi-output") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "pi:context:bytes_") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "pi:status:exit_0") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "stdout:pi_ok") != null);
|
||||
}
|
||||
|
||||
test "adversarial: missing pi command is visible and invalid command preserves buffer" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 80, .height = 6 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open safe");
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("pi_run "));
|
||||
try client.handleTraceLine("pi_run definitely-not-a-mim-pi");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "pi:status:spawn_error_") != null);
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user