Show recoverable panel and editor feedback

This commit is contained in:
slhx agent
2026-06-21 02:50:19 +02:00
parent c0a4d1c9bb
commit e540e64d93
+79 -7
View File
@@ -65,6 +65,7 @@ pub const Client = struct {
leader: leader_mod.Leader,
viewport: Viewport,
saved_bytes: ?[]u8 = null,
message: ?[]const u8 = null,
quit: bool = false,
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
@@ -96,6 +97,10 @@ pub const Client = struct {
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);
if (std.mem.startsWith(u8, line, "message ")) {
self.message = line[8..];
return;
}
if (std.mem.eql(u8, line, "list_down")) return self.applyProtocol("command list_down");
if (std.mem.eql(u8, line, "list_up")) return self.applyProtocol("command list_up");
if (std.mem.eql(u8, line, "list_select")) return self.applyProtocol("command list_select");
@@ -149,7 +154,9 @@ pub const Client = struct {
}
const leader_status = self.leader.status();
const status = if (leader_status.len != 0)
const status = if (self.message) |message|
try std.fmt.allocPrint(allocator, "{s}", .{message})
else if (leader_status.len != 0)
try std.fmt.allocPrint(
allocator,
"{s}{s}{s}",
@@ -206,11 +213,14 @@ pub const Client = struct {
try out.append(allocator, '\n');
}
const status = try std.fmt.allocPrint(
allocator,
"panel {d}/{d} x close",
.{ snap.active_panel_index.? + 1, snap.panel_depth },
);
const status = if (self.message) |message|
try std.fmt.allocPrint(allocator, "{s}", .{message})
else
try std.fmt.allocPrint(
allocator,
"panel {d}/{d} x close",
.{ snap.active_panel_index.? + 1, snap.panel_depth },
);
defer allocator.free(status);
try appendVisibleCells(allocator, &out, status, self.viewport.width);
return out.toOwnedSlice(allocator);
@@ -293,7 +303,19 @@ pub const Client = struct {
fn applyProtocol(self: *Client, line: []const u8) !void {
const response = try protocol.handleLine(self.allocator, &self.session, line);
defer self.allocator.free(response);
if (std.mem.startsWith(u8, response, "err ")) return Error.ProtocolRejected;
if (std.mem.startsWith(u8, response, "err ")) {
self.message = protocolErrorMessage(response);
return Error.ProtocolRejected;
}
self.message = null;
}
fn protocolErrorMessage(response: []const u8) []const u8 {
if (std.mem.indexOf(u8, response, "invalid panel title") != null) return "error: invalid panel title";
if (std.mem.indexOf(u8, response, "no panel open") != null) return "error: no panel open";
if (std.mem.indexOf(u8, response, "invalid list") != null) return "error: invalid list";
if (std.mem.indexOf(u8, response, "active panel is not list") != null) return "error: active panel is not list";
return "error: command failed";
}
fn resize(self: *Client, payload: []const u8) !void {
@@ -310,6 +332,7 @@ pub const Client = struct {
const copy = try self.allocator.dupe(u8, snap.bytes);
if (self.saved_bytes) |old| self.allocator.free(old);
self.saved_bytes = copy;
self.message = "saved";
}
};
@@ -665,3 +688,52 @@ test "adversarial: list no-match state and invalid actions recover" {
try client.handleTraceLine("list_cancel");
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("list_down"));
}
test "regular: save status appears near editor status line" {
var client = try Client.init(std.testing.allocator, .{ .width = 20, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("key space");
try client.handleTraceLine("key s");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "saved") != null);
try assertLinesFit(frame, 20);
}
test "regular: empty generic panel shows local empty state" {
var client = try Client.init(std.testing.allocator, .{ .width = 22, .height = 4 });
defer client.deinit();
try client.handleTraceLine("panel_open help");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "help: no content yet") != null);
try assertLinesFit(frame, 22);
}
test "adversarial: protocol errors render recoverable local message" {
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("panel_open bad title"));
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "error: invalid panel") != null);
try assertLinesFit(frame, 24);
}
test "adversarial: long feedback messages are clipped to viewport width" {
var client = try Client.init(std.testing.allocator, .{ .width = 16, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("message this-message-is-way-too-long-for-the-phone-width");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try assertLinesFit(frame, 16);
try std.testing.expect(std.mem.indexOf(u8, frame, "this-message-is-") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "phone-width") == null);
}