From f14a1306cd226b3c99b802d4b5fbf0ab97fffd62 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 31 Jan 2026 11:07:26 -0800 Subject: [PATCH] renderer: semantic prompt overlay --- src/inspector/widgets/renderer.zig | 84 +++++++++--- src/renderer/Overlay.zig | 204 +++++++++++++++++++++++++++-- 2 files changed, 263 insertions(+), 25 deletions(-) diff --git a/src/inspector/widgets/renderer.zig b/src/inspector/widgets/renderer.zig index 3c6492dfe..1003b02ce 100644 --- a/src/inspector/widgets/renderer.zig +++ b/src/inspector/widgets/renderer.zig @@ -48,24 +48,76 @@ pub const Info = struct { ) void { if (!open) return; - cimgui.c.ImGui_SeparatorText("Overlays"); + cimgui.c.ImGui_SetNextItemOpen(true, cimgui.c.ImGuiCond_Once); + if (!cimgui.c.ImGui_CollapsingHeader("Overlays", cimgui.c.ImGuiTreeNodeFlags_None)) return; - // Hyperlinks - { - var hyperlinks: bool = self.features.contains(.highlight_hyperlinks); - _ = cimgui.c.ImGui_Checkbox("Overlay Hyperlinks", &hyperlinks); - cimgui.c.ImGui_SameLine(); - widgets.helpMarker("When enabled, highlights OSC8 hyperlinks."); + cimgui.c.ImGui_SeparatorText("Hyperlinks"); + self.overlayHyperlinks(alloc); + cimgui.c.ImGui_SeparatorText("Semantic Prompts"); + self.overlaySemanticPrompts(alloc); + } - if (!hyperlinks) { - _ = self.features.swapRemove(.highlight_hyperlinks); - } else { - self.features.put( - alloc, - .highlight_hyperlinks, - .highlight_hyperlinks, - ) catch log.warn("error enabling hyperlink overlay feature", .{}); - } + fn overlayHyperlinks(self: *Info, alloc: Allocator) void { + var hyperlinks: bool = self.features.contains(.highlight_hyperlinks); + _ = cimgui.c.ImGui_Checkbox("Overlay Hyperlinks", &hyperlinks); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("When enabled, highlights OSC8 hyperlinks."); + + if (!hyperlinks) { + _ = self.features.swapRemove(.highlight_hyperlinks); + } else { + self.features.put( + alloc, + .highlight_hyperlinks, + .highlight_hyperlinks, + ) catch log.warn("error enabling hyperlink overlay feature", .{}); } } + + fn overlaySemanticPrompts(self: *Info, alloc: Allocator) void { + var semantic_prompts: bool = self.features.contains(.semantic_prompts); + _ = cimgui.c.ImGui_Checkbox("Overlay Semantic Prompts", &semantic_prompts); + cimgui.c.ImGui_SameLine(); + widgets.helpMarker("When enabled, highlights OSC 133 semantic prompts."); + + // Handle the checkbox results + if (!semantic_prompts) { + _ = self.features.swapRemove(.semantic_prompts); + } else { + self.features.put( + alloc, + .semantic_prompts, + .semantic_prompts, + ) catch log.warn("error enabling semantic prompt overlay feature", .{}); + } + + // Help + cimgui.c.ImGui_Indent(); + defer cimgui.c.ImGui_Unindent(); + + cimgui.c.ImGui_TextDisabled("Colors:"); + + const prompt_rgb = renderer.Overlay.Color.semantic_prompt.rgb(); + const input_rgb = renderer.Overlay.Color.semantic_input.rgb(); + const prompt_col: cimgui.c.ImVec4 = .{ + .x = @as(f32, @floatFromInt(prompt_rgb.r)) / 255.0, + .y = @as(f32, @floatFromInt(prompt_rgb.g)) / 255.0, + .z = @as(f32, @floatFromInt(prompt_rgb.b)) / 255.0, + .w = 1.0, + }; + const input_col: cimgui.c.ImVec4 = .{ + .x = @as(f32, @floatFromInt(input_rgb.r)) / 255.0, + .y = @as(f32, @floatFromInt(input_rgb.g)) / 255.0, + .z = @as(f32, @floatFromInt(input_rgb.b)) / 255.0, + .w = 1.0, + }; + + _ = cimgui.c.ImGui_ColorButton("##prompt_color", prompt_col, cimgui.c.ImGuiColorEditFlags_NoTooltip); + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("Prompt"); + + _ = cimgui.c.ImGui_ColorButton("##input_color", input_col, cimgui.c.ImGuiColorEditFlags_NoTooltip); + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("Input"); + } }; diff --git a/src/renderer/Overlay.zig b/src/renderer/Overlay.zig index 7eb94acb5..62eb004ba 100644 --- a/src/renderer/Overlay.zig +++ b/src/renderer/Overlay.zig @@ -21,6 +21,44 @@ const Size = size.Size; const CellSize = size.CellSize; const Image = @import("image.zig").Image; +const log = std.log.scoped(.renderer_overlay); + +/// The colors we use for overlays. +pub const Color = enum { + hyperlink, // light blue + semantic_prompt, // orange/gold + semantic_input, // cyan + + pub fn rgb(self: Color) z2d.pixel.RGB { + return switch (self) { + .hyperlink => .{ .r = 180, .g = 180, .b = 255 }, + .semantic_prompt => .{ .r = 255, .g = 200, .b = 64 }, + .semantic_input => .{ .r = 64, .g = 200, .b = 255 }, + }; + } + + /// The fill color for rectangles. + pub fn rectFill(self: Color) z2d.Pixel { + return self.alphaPixel(96); + } + + /// The border color for rectangles. + pub fn rectBorder(self: Color) z2d.Pixel { + return self.alphaPixel(200); + } + + /// The raw RGB as a pixel. + pub fn pixel(self: Color) z2d.Pixel { + return self.rgb().asPixel(); + } + + fn alphaPixel(self: Color, alpha: u8) z2d.Pixel { + var rgba: z2d.pixel.RGBA = .fromPixel(self.pixel()); + rgba.a = alpha; + return rgba.multiply().asPixel(); + } +}; + /// The surface we're drawing our overlay to. surface: z2d.Surface, @@ -30,6 +68,7 @@ cell_size: CellSize, /// The set of available features and their configuration. pub const Feature = union(enum) { highlight_hyperlinks, + semantic_prompts, }; pub const InitError = Allocator.Error || error{ @@ -100,6 +139,10 @@ pub fn applyFeatures( alloc, state, ), + .semantic_prompts => self.highlightSemanticPrompts( + alloc, + state, + ), }; } @@ -113,13 +156,8 @@ fn highlightHyperlinks( alloc: Allocator, state: *const terminal.RenderState, ) void { - const border_fill_rgb: z2d.pixel.RGB = .{ .r = 180, .g = 180, .b = 255 }; - const border_color = border_fill_rgb.asPixel(); - const fill_color: z2d.Pixel = px: { - var rgba: z2d.pixel.RGBA = .fromPixel(border_color); - rgba.a = 128; - break :px rgba.multiply().asPixel(); - }; + const border_color = Color.hyperlink.rectBorder(); + const fill_color = Color.hyperlink.rectFill(); const row_slice = state.row_data.slice(); const row_raw = row_slice.items(.raw); @@ -145,7 +183,7 @@ fn highlightHyperlinks( while (x < raw_cells.len and raw_cells[x].hyperlink) x += 1; const end_x = x; - self.highlightRect( + self.highlightGridRect( alloc, start_x, y, @@ -160,9 +198,105 @@ fn highlightHyperlinks( } } +fn highlightSemanticPrompts( + self: *Overlay, + alloc: Allocator, + state: *const terminal.RenderState, +) void { + const row_slice = state.row_data.slice(); + const row_raw = row_slice.items(.raw); + const row_cells = row_slice.items(.cells); + + // Highlight the row-level semantic prompt bars. The prompts are easy + // because they're part of the row metadata. + { + const prompt_border = Color.semantic_prompt.rectBorder(); + const prompt_fill = Color.semantic_prompt.rectFill(); + + var y: usize = 0; + while (y < row_raw.len) { + // If its not a semantic prompt row, skip it. + if (row_raw[y].semantic_prompt == .none) { + y += 1; + continue; + } + + // Find the full length of the semantic prompt row by connecting + // all continuations. + const start_y = y; + y += 1; + while (y < row_raw.len and + row_raw[y].semantic_prompt == .prompt_continuation) + { + y += 1; + } + const end_y = y; // Exclusive + + const bar_width = @min(@as(usize, 5), self.cell_size.width); + self.highlightPixelRect( + alloc, + 0, + start_y, + bar_width, + end_y - start_y, + prompt_border, + prompt_fill, + ) catch |err| { + log.warn("Error drawing semantic prompt bar: {}", .{err}); + }; + } + } + + // Highlight contiguous semantic cells within rows. + for (row_cells, 0..) |cells, y| { + const cells_slice = cells.slice(); + const raw_cells = cells_slice.items(.raw); + + var x: usize = 0; + while (x < raw_cells.len) { + const cell = raw_cells[x]; + const content = cell.semantic_content; + const start_x = x; + + // We skip output because its just the rest of the non-prompt + // parts and it makes the overlay too noisy. + if (cell.semantic_content == .output) { + x += 1; + continue; + } + + // Find the end of this content. + x += 1; + while (x < raw_cells.len) { + const next = raw_cells[x]; + if (next.semantic_content != content) break; + x += 1; + } + + const color: Color = switch (content) { + .prompt => .semantic_prompt, + .input => .semantic_input, + .output => unreachable, + }; + + self.highlightGridRect( + alloc, + start_x, + y, + x - start_x, + 1, + color.rectBorder(), + color.rectFill(), + ) catch |err| { + log.warn("Error drawing semantic content highlight: {}", .{err}); + }; + } + } +} + /// Creates a rectangle for highlighting a grid region. x/y/width/height /// are all in grid cells. -fn highlightRect( +fn highlightGridRect( self: *Overlay, alloc: Allocator, x: usize, @@ -227,3 +361,55 @@ fn highlightRect( ctx.setSourceToPixel(border_color); try ctx.stroke(); } + +/// Creates a rectangle for highlighting a region. x/y are grid cells and +/// width/height are pixels. +fn highlightPixelRect( + self: *Overlay, + alloc: Allocator, + x: usize, + y: usize, + width_px: usize, + height: usize, + border_color: z2d.Pixel, + fill_color: z2d.Pixel, +) !void { + const px_width = std.math.cast(i32, width_px) orelse return error.Overflow; + const px_height = std.math.cast(i32, try std.math.mul( + usize, + height, + self.cell_size.height, + )) orelse return error.Overflow; + + const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul( + usize, + x, + self.cell_size.width, + )) orelse return error.Overflow); + const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul( + usize, + y, + self.cell_size.height, + )) orelse return error.Overflow); + const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width)); + const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height)); + + var ctx: z2d.Context = .init(alloc, &self.surface); + defer ctx.deinit(); + + ctx.setAntiAliasingMode(.none); + ctx.setHairline(true); + + try ctx.moveTo(start_x, start_y); + try ctx.lineTo(end_x, start_y); + try ctx.lineTo(end_x, end_y); + try ctx.lineTo(start_x, end_y); + try ctx.closePath(); + + ctx.setSourceToPixel(fill_color); + try ctx.fill(); + + ctx.setLineWidth(1); + ctx.setSourceToPixel(border_color); + try ctx.stroke(); +}