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); }