Files
mim/src/context.zig
T
2026-06-21 05:00:21 +02:00

118 lines
5.8 KiB
Zig

const std = @import("std");
const session_mod = @import("session.zig");
// Compact local context formatting for same-user tools/agents.
// req: session/002, governance/002, governance/003, testing/001, testing/002
test {
_ = sessionContextAlloc;
}
pub const Limits = struct {
max_bytes_preview: usize = 48,
max_panel_summary: usize = 96,
max_task: usize = 96,
};
pub fn localContextAlloc(allocator: std.mem.Allocator, cwd: []const u8, task: ?[]const u8, intent: ?[]const u8, limits: Limits) ![]u8 {
var out = std.ArrayList(u8).empty;
errdefer out.deinit(allocator);
try appendLine(allocator, &out, "context:v1");
try appendField(allocator, &out, "cwd", cwd, limits.max_panel_summary);
if (task) |value| try appendField(allocator, &out, "task", value, limits.max_task);
if (intent) |value| try appendField(allocator, &out, "intent", value, limits.max_task);
return out.toOwnedSlice(allocator);
}
pub fn sessionContextAlloc(allocator: std.mem.Allocator, session: *const session_mod.Session, task: ?[]const u8, intent: ?[]const u8, limits: Limits) ![]u8 {
const snap = try session.snapshot();
const panel_path = try session.panelPathAlloc(allocator);
defer allocator.free(panel_path);
const panel_summary = try session.panelSummaryAlloc(allocator);
defer allocator.free(panel_summary);
var out = std.ArrayList(u8).empty;
errdefer out.deinit(allocator);
try appendLine(allocator, &out, "context:v1");
try appendNumber(allocator, &out, "cursor_byte", snap.cursor_byte);
try appendNumber(allocator, &out, "cursor_cell", snap.cursor_cell);
try appendNumber(allocator, &out, "bytes_len", snap.bytes.len);
try appendNumber(allocator, &out, "panel_depth", snap.panel_depth);
try appendField(allocator, &out, "active_panel", if (snap.active_panel_title) |title| title else "-", limits.max_panel_summary);
try appendField(allocator, &out, "panel_path", panel_path, limits.max_panel_summary);
try appendField(allocator, &out, "panel_summary", panel_summary, limits.max_panel_summary);
try appendField(allocator, &out, "buffer_preview", snap.bytes[0..@min(snap.bytes.len, limits.max_bytes_preview)], limits.max_bytes_preview);
if (task) |value| try appendField(allocator, &out, "task", value, limits.max_task);
if (intent) |value| try appendField(allocator, &out, "intent", value, limits.max_task);
return out.toOwnedSlice(allocator);
}
fn appendNumber(allocator: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, value: usize) !void {
const line = try std.fmt.allocPrint(allocator, "{s}={d}", .{ key, value });
defer allocator.free(line);
try appendLine(allocator, out, line);
}
fn appendField(allocator: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, value: []const u8, max_len: usize) !void {
const clean = try sanitizeAlloc(allocator, value, max_len);
defer allocator.free(clean);
const line = try std.fmt.allocPrint(allocator, "{s}={s}", .{ key, clean });
defer allocator.free(line);
try appendLine(allocator, out, line);
}
fn appendLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void {
try out.appendSlice(allocator, line);
try out.append(allocator, '\n');
}
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);
}
test "regular: session context includes compact editor and panel state" {
var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit();
try session.openFixture("pub const x = 1;\n");
try session.openListPanel("diagnostics", &.{ "lsp:diagnostics:count_1", "lsp:diag:error:1:5:file:bad" });
const text = try sessionContextAlloc(std.testing.allocator, &session, "fix bug", "inspect", .{ .max_bytes_preview = 8, .max_panel_summary = 80, .max_task = 16 });
defer std.testing.allocator.free(text);
try std.testing.expect(std.mem.indexOf(u8, text, "context:v1\n") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "active_panel=diagnostics") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "panel_summary=") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "buffer_preview=pub") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "task=fix_bug") != null);
}
test "regular: local context is line oriented and sanitized" {
const text = try localContextAlloc(std.testing.allocator, "/tmp/mim repo", "ship v1", null, .{});
defer std.testing.allocator.free(text);
try std.testing.expect(std.mem.indexOf(u8, text, "cwd=/tmp/mim_repo") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "task=ship_v1") != null);
}
test "adversarial: context output is bounded" {
const long = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const text = try localContextAlloc(std.testing.allocator, long, long, long, .{ .max_panel_summary = 8, .max_task = 8 });
defer std.testing.allocator.free(text);
try std.testing.expect(std.mem.indexOf(u8, text, "cwd=aaaaaaaa...") != null);
try std.testing.expect(std.mem.indexOf(u8, text, "task=aaaaaaaa...") != null);
}