From f2fce0eec92cca97f988a043fd176bcc50c60584 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Tue, 6 Oct 2026 11:13:33 -0700 Subject: [PATCH] Keep loop bar text clear of its buttons at narrow widths At a 960px window with the sidebar rail and loop detail panel visible, the loop bar's metadata line (backend, elapsed, pass, usage) was drawn at a fixed offset into a fixed 220px rect and ran underneath Stop loop and Show in graph. Extract the bar geometry into LoopBarLayout.zig and use it as the single source for painting, mouse hit-testing, and UIA bounds. Every text run is now clamped to the space left of the leftmost button, ellipsized when squeezed, and dropped when narrower than 24px. Wide-window geometry is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/Tests/WindowsShell.Tests.ps1 | 6 + graphcode-windows/src/App.zig | 18 ++- graphcode-windows/src/LoopBarLayout.zig | 156 ++++++++++++++++++++ graphcode-windows/src/TerminalSurface.zig | 55 ++++--- graphcode-windows/src/TerminalWorkspace.zig | 1 + investigation/ui-parity-matrix.md | 2 +- 6 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 graphcode-windows/src/LoopBarLayout.zig diff --git a/Tools/windows/Tests/WindowsShell.Tests.ps1 b/Tools/windows/Tests/WindowsShell.Tests.ps1 index aedc101b..34aa2cba 100644 --- a/Tools/windows/Tests/WindowsShell.Tests.ps1 +++ b/Tools/windows/Tests/WindowsShell.Tests.ps1 @@ -878,6 +878,12 @@ Invoke-Native "Zmx session identity executable tests" { & $zig test src\ZmxSession.zig } finally { Pop-Location } } +Invoke-Native "Loop bar layout executable tests" { + Push-Location $shellRoot + try { + & $zig test src\LoopBarLayout.zig + } finally { Pop-Location } +} Invoke-Native "Terminal VT preparation and memory tests" { $depotRoot = Split-Path (Split-Path $repoRoot -Parent) -Parent $winghosttyRoot = [Environment]::GetEnvironmentVariable("GRAPHCODE_WINGHOSTTY_ROOT") diff --git a/graphcode-windows/src/App.zig b/graphcode-windows/src/App.zig index 36c68a42..8f6b59da 100644 --- a/graphcode-windows/src/App.zig +++ b/graphcode-windows/src/App.zig @@ -27,6 +27,7 @@ const GraphModel = @import("GraphModel.zig"); const InputRouter = @import("InputRouter.zig"); const MainWindow = @import("MainWindow.zig"); const TerminalWorkspace = @import("TerminalWorkspace.zig"); +const LoopBarLayout = @import("LoopBarLayout.zig"); const Clipboard = @import("Clipboard.zig"); const Tokens = @import("DesignTokens.zig"); const Dpi = @import("Dpi.zig"); @@ -957,6 +958,10 @@ fn isResolvedLoopState(state: []const u8) bool { std.mem.eql(u8, state, "stopped"); } +fn loopBarRect(rect: LoopBarLayout.Rect) c.RECT { + return .{ .left = rect.left, .top = rect.top, .right = rect.right, .bottom = rect.bottom }; +} + fn workspaceGraph(model: *const GraphModel.Model) ?*const GraphModel.GraphSummary { if (model.currentGraph()) |graph| if (graph.nodes.items.len != 0) return graph; if (model.selected_project_path) |path| { @@ -6113,14 +6118,19 @@ pub const App = struct { const workspace_left = if (self.workspace_controls.rail_visible) Tokens.sidebar_width else 0; const workspace_right = client.right - (if (self.workspace_controls.panel_visible) Tokens.loop_detail_width else 0); const selected_index = self.model.selectedIndex() orelse 0; + const loop_bar = TerminalWorkspace.loopBarLayout( + workspace_left, + workspace_right, + selected_index >= graph.nodes.items.len or isResolvedLoopState(graph.nodes.items[selected_index].state), + ); if (!self.workspace_is_quick_chat) { self.appendAccessibilityElement(&elements, &owned_identities, "workspace-toolbar", graph.project.path, header.title, 4, .{ .logical = header_layout.identity }, false, false) catch return; elements.items[elements.items.len - 1].invokable = false; } - self.appendAccessibilityElement(&elements, &owned_identities, "workspace-loop-bar", if (selected_index < graph.nodes.items.len) graph.nodes.items[selected_index].id else "none", "Selected loop workspace", 4, .{ .logical = .{ .left = workspace_left, .top = Tokens.header_height, .right = workspace_right, .bottom = Tokens.header_height + Tokens.loop_bar_height } }, false, false) catch return; - self.appendAccessibilityElement(&elements, &owned_identities, "workspace-show-graph", "show-graph", "Show in Graph", 4, .{ .logical = .{ .left = workspace_right - 104, .top = Tokens.header_height + 10, .right = workspace_right - 12, .bottom = Tokens.header_height + 36 } }, false, false) catch return; - if (selected_index < graph.nodes.items.len and !isResolvedLoopState(graph.nodes.items[selected_index].state)) { - self.appendAccessibilityElement(&elements, &owned_identities, "workspace-stop", graph.nodes.items[selected_index].id, "Stop loop", 4, .{ .logical = .{ .left = workspace_right - 196, .top = Tokens.header_height + 10, .right = workspace_right - 112, .bottom = Tokens.header_height + 36 } }, false, false) catch return; + self.appendAccessibilityElement(&elements, &owned_identities, "workspace-loop-bar", if (selected_index < graph.nodes.items.len) graph.nodes.items[selected_index].id else "none", "Selected loop workspace", 4, .{ .logical = loopBarRect(loop_bar.bar) }, false, false) catch return; + self.appendAccessibilityElement(&elements, &owned_identities, "workspace-show-graph", "show-graph", "Show in Graph", 4, .{ .logical = loopBarRect(loop_bar.show_graph) }, false, false) catch return; + if (loop_bar.stop) |stop| { + self.appendAccessibilityElement(&elements, &owned_identities, "workspace-stop", graph.nodes.items[selected_index].id, "Stop loop", 4, .{ .logical = loopBarRect(stop) }, false, false) catch return; } const panel_toggle = if (self.workspace_controls.panel_visible) GraphCanvas.loopDetailCollapseBounds(client.right) diff --git a/graphcode-windows/src/LoopBarLayout.zig b/graphcode-windows/src/LoopBarLayout.zig new file mode 100644 index 00000000..4fdcb758 --- /dev/null +++ b/graphcode-windows/src/LoopBarLayout.zig @@ -0,0 +1,156 @@ +const std = @import("std"); +const Tokens = @import("DesignTokens.zig"); + +// Logical-pixel geometry of the 46px workspace loop bar. Painting, mouse hit-testing, +// and UIA bounds all read these rects so the drawn buttons and their actionable areas +// cannot drift apart. + +pub const Rect = struct { + left: i32, + top: i32, + right: i32, + bottom: i32, + + pub fn width(self: Rect) i32 { + return self.right - self.left; + } + + pub fn contains(self: Rect, x: i32, y: i32) bool { + return x >= self.left and x < self.right and y >= self.top and y < self.bottom; + } + + pub fn intersects(self: Rect, other: Rect) bool { + return self.left < other.right and other.left < self.right and self.top < other.bottom and other.top < self.bottom; + } +}; + +pub const Action = enum { stop, show_graph }; + +// Every text run in the bar is drawn into a fixed 220px-wide rect when it has room. +pub const natural_text_width: i32 = 220; +// Clearance kept between the last text pixel and the leftmost button. +pub const button_gap: i32 = 8; +// Narrower than this a run would show little more than an ellipsis, so it is dropped. +pub const min_text_width: i32 = 24; + +pub const Layout = struct { + bar: Rect, + stripe: ?Rect, + title: ?Rect, + state: ?Rect, + activity: ?Rect, + detail: ?Rect, + stop: ?Rect, + show_graph: Rect, + + // Buttons accept clicks across the bar's full height, using their drawn columns. + pub fn actionAt(self: Layout, x: i32, y: i32) ?Action { + if (!self.bar.contains(x, y)) return null; + if (x >= self.show_graph.left and x < self.show_graph.right) return .show_graph; + if (self.stop) |stop| if (x >= stop.left and x < stop.right) return .stop; + return null; + } +}; + +fn textRect(x: i32, y: i32, size: i32, limit: i32) ?Rect { + const right = @min(x + natural_text_width, limit); + if (right - x < min_text_width) return null; + return .{ .left = x, .top = y, .right = right, .bottom = y + size + 8 }; +} + +pub fn compute(left: i32, top: i32, right: i32, resolved: bool) Layout { + const show_graph = Rect{ .left = right - 104, .top = top + 10, .right = right - 12, .bottom = top + 36 }; + const stop: ?Rect = if (resolved) null else .{ .left = right - 196, .top = top + 10, .right = right - 112, .bottom = top + 36 }; + const limit = (if (stop) |value| value.left else show_graph.left) - button_gap; + const stripe = Rect{ .left = left + 14, .top = top + 11, .right = left + 18, .bottom = top + 35 }; + return .{ + .bar = .{ .left = left, .top = top, .right = right, .bottom = top + Tokens.loop_bar_height }, + .stripe = if (stripe.right <= limit) stripe else null, + .title = textRect(left + 27, top + 5, 13, limit), + .state = textRect(left + 190, top + 8, 10, limit), + .activity = textRect(left + 27, top + 24, 10, limit), + .detail = textRect(left + 260, top + 10, 9, limit), + .stop = stop, + .show_graph = show_graph, + }; +} + +fn expectClearOfButtons(layout: Layout) !void { + const runs = [_]?Rect{ layout.stripe, layout.title, layout.state, layout.activity, layout.detail }; + for (runs) |maybe_run| { + const run = maybe_run orelse continue; + try std.testing.expect(run.width() > 0); + try std.testing.expect(run.left >= layout.bar.left); + try std.testing.expect(!run.intersects(layout.show_graph)); + if (layout.stop) |stop| try std.testing.expect(!run.intersects(stop)); + } +} + +test "loop bar metadata stays clear of Stop and Show in graph at 960px with rail and detail panel" { + // beta10 Dev Box qualification: 960px shell, 220px sidebar rail, 272px loop detail panel. + const left = Tokens.sidebar_width; + const right = 960 - Tokens.loop_detail_width; + const layout = compute(left, Tokens.header_height, right, false); + try expectClearOfButtons(layout); + // Only 4px remain between the metadata origin and Stop, so the run is dropped. + try std.testing.expect(layout.detail == null); + try std.testing.expect(layout.title != null and layout.state != null and layout.activity != null); +} + +test "loop bar ellipsizes metadata into the space left of Stop at 1056px with rail and detail panel" { + const left = Tokens.sidebar_width; + const right = 1056 - Tokens.loop_detail_width; + const layout = compute(left, Tokens.header_height, right, false); + try expectClearOfButtons(layout); + const detail = layout.detail.?; + try std.testing.expectEqual(left + 260, detail.left); + try std.testing.expectEqual(layout.stop.?.left - button_gap, detail.right); + try std.testing.expect(detail.width() < natural_text_width); +} + +test "loop bar keeps the wide 1280px geometry unchanged" { + const left = Tokens.sidebar_width; + const right = 1280 - Tokens.loop_detail_width; + const top = Tokens.header_height; + const layout = compute(left, top, right, false); + try expectClearOfButtons(layout); + try std.testing.expectEqual(Rect{ .left = left + 14, .top = top + 11, .right = left + 18, .bottom = top + 35 }, layout.stripe.?); + try std.testing.expectEqual(Rect{ .left = left + 27, .top = top + 5, .right = left + 247, .bottom = top + 26 }, layout.title.?); + try std.testing.expectEqual(Rect{ .left = left + 190, .top = top + 8, .right = left + 410, .bottom = top + 26 }, layout.state.?); + try std.testing.expectEqual(Rect{ .left = left + 27, .top = top + 24, .right = left + 247, .bottom = top + 42 }, layout.activity.?); + try std.testing.expectEqual(Rect{ .left = left + 260, .top = top + 10, .right = left + 480, .bottom = top + 27 }, layout.detail.?); + try std.testing.expectEqual(Rect{ .left = right - 196, .top = top + 10, .right = right - 112, .bottom = top + 36 }, layout.stop.?); + try std.testing.expectEqual(Rect{ .left = right - 104, .top = top + 10, .right = right - 12, .bottom = top + 36 }, layout.show_graph); +} + +test "loop bar drops text that cannot fit beside the buttons on an extremely narrow bar" { + const left = Tokens.sidebar_width; + const right = left + 240; + const layout = compute(left, Tokens.header_height, right, false); + try expectClearOfButtons(layout); + try std.testing.expect(layout.title == null and layout.state == null and layout.activity == null and layout.detail == null); + try std.testing.expectEqual(right - 12, layout.show_graph.right); + const sliver = compute(left, Tokens.header_height, left + 100, false); + try expectClearOfButtons(sliver); + try std.testing.expect(sliver.stripe == null); +} + +test "loop bar text reclaims the Stop slot once the loop is resolved" { + const left = Tokens.sidebar_width; + const right = 960 - Tokens.loop_detail_width; + const resolved = compute(left, Tokens.header_height, right, true); + try std.testing.expect(resolved.stop == null); + try expectClearOfButtons(resolved); + try std.testing.expectEqual(resolved.show_graph.left - button_gap, resolved.detail.?.right); +} + +test "loop bar actions hit exactly the button rects" { + const layout = compute(220, 34, 1200, false); + try std.testing.expectEqual(Action.stop, layout.actionAt(1010, 50).?); + try std.testing.expectEqual(Action.show_graph, layout.actionAt(1120, 50).?); + try std.testing.expectEqual(Action.show_graph, layout.actionAt(1120, 35).?); + try std.testing.expect(layout.actionAt(1120, 80) == null); + try std.testing.expect(layout.actionAt(1120, 90) == null); + try std.testing.expect(layout.actionAt(layout.stop.?.right, 50) == null); + try std.testing.expect(compute(220, 34, 1200, true).actionAt(1010, 50) == null); +} diff --git a/graphcode-windows/src/TerminalSurface.zig b/graphcode-windows/src/TerminalSurface.zig index 4f8dfa98..5a2e54c8 100644 --- a/graphcode-windows/src/TerminalSurface.zig +++ b/graphcode-windows/src/TerminalSurface.zig @@ -2,6 +2,7 @@ const std = @import("std"); const c = @import("Win32.zig").c; const WorkspaceLayout = @import("WorkspaceLayout.zig"); const Tokens = @import("DesignTokens.zig"); +const LoopBarLayout = @import("LoopBarLayout.zig"); const AppFont = @import("AppFont.zig"); const GdiGradient = @import("GdiGradient.zig"); const Dpi = @import("Dpi.zig"); @@ -159,14 +160,14 @@ const max_surfaces: usize = 32; pub const ChromeAction = enum { new_tab, split_right, split_down }; pub const TabAction = enum { select, close }; -pub const LoopBarAction = enum { stop, show_graph }; +pub const LoopBarAction = LoopBarLayout.Action; + +pub fn loopBarLayout(left: i32, right: i32, resolved: bool) LoopBarLayout.Layout { + return LoopBarLayout.compute(left, Tokens.header_height, right, resolved); +} pub fn loopBarActionAt(left: i32, top: i32, right: i32, x: i32, y: i32, resolved: bool) ?LoopBarAction { - if (y < top or y >= top + Tokens.loop_bar_height) return null; - if (x >= right - 104 and x < right - 12) return .show_graph; - if (!resolved and x >= right - 196 and x < right - 112) return .stop; - _ = left; - return null; + return LoopBarLayout.compute(left, top, right, resolved).actionAt(x, y); } fn chromeActionForBounds(origin_x: i32, origin_y: i32, width: i32, x: i32, y: i32) ?ChromeAction { @@ -1008,18 +1009,16 @@ pub const Workspace = struct { resolved: bool, ) void { const top = Tokens.header_height; + const layout = loopBarLayout(left, right, resolved); // Theme.loopBar: lit like the tab strip, one step lighter. GdiGradient.fillVertical(hdc, .{ .left = left, .top = top, .right = right, .bottom = top + Tokens.loop_bar_height }, Tokens.loop_bar_top, Tokens.loop_bar_bottom); - fillRect(hdc, .{ - .left = left + 14, - .top = top + 11, - .right = left + 18, - .bottom = top + 35, - }, loopTypeAccent(loop_type)); - drawUtf8(hdc, title, left + 27, top + 5, 13, 0x00F2F2F7); - drawUtf8(hdc, state, left + 190, top + 8, 10, stateAccent(state)); + if (layout.stripe) |stripe| { + fillRect(hdc, .{ .left = stripe.left, .top = stripe.top, .right = stripe.right, .bottom = stripe.bottom }, loopTypeAccent(loop_type)); + } + if (layout.title) |bounds| drawUtf8Bounded(hdc, title, bounds, 13, 0x00F2F2F7); + if (layout.state) |bounds| drawUtf8Bounded(hdc, state, bounds, 10, stateAccent(state)); const live_line = if (activity.len != 0) activity else project_name; - drawUtf8(hdc, live_line, left + 27, top + 24, 10, 0x008E8E93); + if (layout.activity) |bounds| drawUtf8Bounded(hdc, live_line, bounds, 10, 0x008E8E93); var usage: [32]u8 = undefined; const usage_text = if (token_usage) |value| std.fmt.bufPrint(&usage, "{d} tokens", .{value}) catch "usage n/a" else "usage n/a"; var detail: [256]u8 = undefined; @@ -1029,12 +1028,12 @@ pub const Workspace = struct { metric_passes, usage_text, }) catch "workspace metadata unavailable"; - drawUtf8(hdc, detail_text, left + 260, top + 10, 9, 0x008E8E93); - if (!resolved) { - fillRect(hdc, .{ .left = right - 196, .top = top + 10, .right = right - 112, .bottom = top + 36 }, 0x00303035); - drawUtf8(hdc, "Stop loop", right - 184, top + 17, 10, 0x00D8D8DC); + if (layout.detail) |bounds| drawUtf8Bounded(hdc, detail_text, bounds, 9, 0x008E8E93); + if (layout.stop) |stop| { + fillRect(hdc, .{ .left = stop.left, .top = stop.top, .right = stop.right, .bottom = stop.bottom }, 0x00303035); + drawUtf8(hdc, "Stop loop", stop.left + 12, top + 17, 10, 0x00D8D8DC); } - drawUtf8(hdc, "Show in graph", right - 100, top + 17, 10, 0x008E8E93); + drawUtf8(hdc, "Show in graph", layout.show_graph.left + 4, top + 17, 10, 0x008E8E93); // Theme.tabBarShadowLine, blended flat over the loop bar's own bottom stop -- // the edge where the strip's gloss meets the terminal below it. fillRect(hdc, .{ .left = left, .top = top + Tokens.loop_bar_height - 1, .right = right, .bottom = top + Tokens.loop_bar_height }, Tokens.tab_bar_shadow_line); @@ -4045,6 +4044,22 @@ fn drawUtf8(hdc: c.HDC, text: []const u8, x: i32, y: i32, size: i32, color: u32) _ = c.SelectObject(hdc, old_font); } +// Clipped to `bounds`; a run squeezed narrower than its natural width ends in an +// ellipsis instead of being cut mid-glyph. +fn drawUtf8Bounded(hdc: c.HDC, text: []const u8, bounds: LoopBarLayout.Rect, size: i32, color: u32) void { + const wide = std.unicode.utf8ToUtf16LeAlloc(std.heap.page_allocator, text) catch return; + defer std.heap.page_allocator.free(wide); + if (wide.len == 0) return; + const old_font = AppFont.select(hdc, size, false); + _ = c.SetTextColor(hdc, color); + _ = c.SetBkMode(hdc, c.TRANSPARENT); + var rect = c.RECT{ .left = bounds.left, .top = bounds.top, .right = bounds.right, .bottom = bounds.bottom }; + const ellipsis: c_int = if (bounds.width() < LoopBarLayout.natural_text_width) c.DT_END_ELLIPSIS else 0; + const format: c.UINT = @intCast(c.DT_LEFT | c.DT_SINGLELINE | ellipsis); + _ = c.DrawTextW(hdc, wide.ptr, @intCast(wide.len), &rect, format); + _ = c.SelectObject(hdc, old_font); +} + fn tabLabel(tab: WorkspaceLayout.Tab, index: usize) []const u8 { if (tab.panes.items.len > 1) return "split"; if (index == 0) return "agent"; diff --git a/graphcode-windows/src/TerminalWorkspace.zig b/graphcode-windows/src/TerminalWorkspace.zig index 69111f28..9a0c68ea 100644 --- a/graphcode-windows/src/TerminalWorkspace.zig +++ b/graphcode-windows/src/TerminalWorkspace.zig @@ -3,5 +3,6 @@ const TerminalSurface = @import("TerminalSurface.zig"); pub const Workspace = TerminalSurface.Workspace; pub const LoopBarAction = TerminalSurface.LoopBarAction; pub const loopBarActionAt = TerminalSurface.loopBarActionAt; +pub const loopBarLayout = TerminalSurface.loopBarLayout; pub const tabBounds = TerminalSurface.tabBounds; pub const chromeControlBounds = TerminalSurface.chromeControlBounds; diff --git a/investigation/ui-parity-matrix.md b/investigation/ui-parity-matrix.md index b90b745d..c9e7c07a 100644 --- a/investigation/ui-parity-matrix.md +++ b/investigation/ui-parity-matrix.md @@ -180,7 +180,7 @@ native keyboard/accelerator/window proof; the workspace row remains Partial. | Terminal VT state and rendering | Incremental VT parsing, complete Unicode text, styles, cursor, resize and scrollback with matching visible output | Explicit startup opt-in `GRAPHCODE_EXPERIMENTAL_TERMINAL_VT=1` uses the existing pinned public scalar libghostty-vt API; default ASCII behavior is unchanged. Real-library memory tests cover chunk boundaries, grapheme arrays/UTF-16 offsets, wide occupancy, colors, alternate screen, viewport/reflow, owned snapshots, allocation failures, and bounded pane-owned replies through the production input queue. The same publication seam fails with the legacy parser and passes with the opt-in state. Strict projection rejects unsupported clusters/wide/decorated cells with explicit unconfirmed-render status while preserving authoritative accessible text; the host remains a one-codepoint 5x7 renderer, not a full Unicode/glyph renderer. The earlier "production is still 120x40" claim was stale: `TerminalSurface.zig` already derives each pane's grid from live pane bounds and the measured cell metrics, and resizes the backend surface when the topology syncs. The remaining defect found there was degenerate geometry — a minimized or zero-sized pane clamped the grid to 1x1 and the old size was lost on restore. `paneBounds` now guards zero-sized rects and `syncPaneGrid` takes an injected resize callback so the grid-state transition is testable without the native provider, with behavioral minimize-then-restore tests asserting the pre-minimize grid is preserved. That is unit evidence over the pure state transition; live PTY resize negotiation against a real backend is still not demonstrated. Wheel/selection, visible cursor, native TextPattern/glyph parity, and OSC 7 PWD retention remain residuals. Memory tests and a build do not establish native UI parity or SIMD performance. | Partial | | Workspace detail screen | Selected loop replaces canvas detail while sidebar remains | Selecting a sidebar or overview loop now replaces the canvas detail with the full terminal workspace while retaining the sidebar; the workspace UIA tree now exposes a destination-specific toolbar, loop bar, tab controls, and Show in Graph action. The `windows-shell` CI job builds the real Swift daemon, Zig shell, and pinned zmx/Winghostty providers and runs `Tools\windows\uia-live-gate.ps1` against the live workspace: the gate independently asserts the toolbar identity child, Show in Graph child, all three split controls, and at least one tab child are present under a real, non-gated `Workspace.init()` (commit `a31813b`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793, passing) | Validated | | Folder toolbar identity | Project name and local/remote identity | Workspace chrome now paints an explicit Workspace title, project name, and Local folder/Remote repository identity over the native header, with a matching stable UIA toolbar child. The live gate's `workspace-toolbar-*` assertion (named `"UIA project"`) now runs against the real shell build and passes on the `windows-shell` CI job (run https://github.com/scgopi/GraphCode/actions/runs/35415967793) | Validated | -| Loop bar | Type stripe, title/state pill, live goal, pass trend, elapsed/usage, Stop, Show in graph | The native 46px workspace band shows loop-type stripe, title, state, current activity, backend, elapsed label from `createdAt`, metric-history pass count, token usage when reported, Stop for unresolved loops, and Show in graph. Focused hit-testing/UIA unit coverage plus the live `windows-shell` CI run (workspace toolbar/loop-bar UIA assertions passing at run https://github.com/scgopi/GraphCode/actions/runs/35415967793) now validate this end to end | Validated | +| Loop bar | Type stripe, title/state pill, live goal, pass trend, elapsed/usage, Stop, Show in graph | The native 46px workspace band shows loop-type stripe, title, state, current activity, backend, elapsed label from `createdAt`, metric-history pass count, token usage when reported, Stop for unresolved loops, and Show in graph. Focused hit-testing/UIA unit coverage plus the live `windows-shell` CI run (workspace toolbar/loop-bar UIA assertions passing at run https://github.com/scgopi/GraphCode/actions/runs/35415967793) validated presence end to end. The beta10 Dev Box qualification (100% DPI) then observed the metadata text drawn under Stop/Show in graph at a 960px window with the sidebar rail and loop detail panel visible. Painting, mouse hit-testing, and UIA bounds now share `LoopBarLayout.zig`, which clamps every text run to the space left of the leftmost button, ellipsizes squeezed runs, and drops runs narrower than 24px; at 960px with rail and panel that drops the metadata line, where macOS truncates instead. Only focused `LoopBarLayout.zig` unit tests cover this; no live observation of the corrected layout has been made | Partial | | Tab pills | Named tabs, selection, state indicator, shortcuts, per-tab close | The native tab strip paints agent/shell/split labels, live state indicators, Ctrl+1-style shortcut hints, and per-tab close affordances. Close routing removes only the selected tab topology and refuses the final tab. The live gate's `workspace-tab-*` assertion now runs against the real shell build and passes on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35415967793) | Validated | | Split controls | Visible Split Right, Split Down, New Tab buttons | The terminal tab bar renders distinct New Tab, Split Right, and Split Down controls with shared geometry helpers used by painting and hit testing, plus UIA children and focused gap-boundary regression coverage. The live gate's split-control assertion (`workspace-(new-tab\|split-right\|split-down)-*`, all three present) now runs against the real shell build and passes on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35415967793) | Validated | | Pane headers | agent/shell identity, backend/shell detail, focused state | Product-owned pane headers distinguish agent and shell panes, label the zmx session detail, add truthful backend: agent/backend: shell detail, and retain the explicit focused-pane accent. Focused rendering unit coverage plus the live `windows-shell` CI run (real workspace/terminal panes, run https://github.com/scgopi/GraphCode/actions/runs/35415967793) provide the side-by-side live evidence that was previously blocked | Validated |