From 621b575eb62baa29bc683088968382a16ae5ceeb Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Sun, 27 Sep 2026 13:19:18 -0700 Subject: [PATCH 1/2] test(windows): verify native sketch promotion submenu Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- graphcode-windows/src/GraphContextMenu.zig | 1399 +++++++++++--------- investigation/ui-parity-matrix.md | 426 +++--- 2 files changed, 951 insertions(+), 874 deletions(-) diff --git a/graphcode-windows/src/GraphContextMenu.zig b/graphcode-windows/src/GraphContextMenu.zig index 81cddb19..ba3f5d85 100644 --- a/graphcode-windows/src/GraphContextMenu.zig +++ b/graphcode-windows/src/GraphContextMenu.zig @@ -1,661 +1,738 @@ -const c = @import("Win32.zig").c; -const Wire = @import("Wire.zig"); -const SketchPromotion = @import("SketchPromotion.zig"); - -pub const NodeTarget = struct { - project_path: []const u8, - id: []const u8, - composite: bool = false, - can_arm: bool = false, - unwired: bool = false, - follows_template: bool = false, - resolved: bool = false, - can_create_child: bool = true, - sketch: bool = false, - promotion_context: ?*const SketchPromotion.Context = null, -}; - -pub const BackgroundTarget = struct { - project_path: []const u8, - local_filesystem: bool, - can_create_edge: bool, -}; - -pub const EdgeTarget = struct { - project_path: []const u8, - id: []const u8, -}; - -pub const QuickChatTarget = struct { - id: []const u8, -}; - -pub const ProjectTarget = struct { - path: []const u8, - remote: bool, -}; - -pub const Target = union(enum) { - background: BackgroundTarget, - quick_chats, - project: ProjectTarget, - node: NodeTarget, - edge: EdgeTarget, - quick_chat: QuickChatTarget, -}; - -pub const Action = enum { - none, - edit_node, - promote_goal, - promote_turn, - promote_timed, - rename_node, - stop_node, - delete_node, - open_terminal, - new_child_node, - message_node, - memo_node, - open_composite, - pilot_composite, - arm_composite, - save_node_template, - detach_template, - wire_node, - mark_entry, - edit_edge, - delete_edge, - create_edge, - open_quick_chat, - rename_quick_chat, - delete_quick_chat, - open_project, - new_project_loop, - inspect_project_worktrees, - project_settings, - reveal_project, - remote_project_info, - close_project, - remove_project, - move_project, - trash_project, - delete_project_loops, - new_quick_chat, -}; - -pub const Callback = *const fn (?*anyopaque, Action, Target) void; - -pub fn requiresConfirmation(action: Action) bool { - return action == .delete_node or action == .delete_edge or action == .delete_quick_chat or - action == .remove_project or action == .trash_project or action == .delete_project_loops; -} - -pub fn shouldApply(action: Action, confirmed: bool) bool { - return !requiresConfirmation(action) or confirmed; -} - -pub fn canEditEdge(edge_id: []const u8) bool { - return edge_id.len != 0; -} - -const ids = struct { - const edit_node = 5100; - const rename_node = 5101; - const stop_node = 5102; - const delete_node = 5103; - const open_terminal = 5104; - const message_node = 5105; - const memo_node = 5106; - const open_composite = 5113; - const pilot_composite = 5107; - const arm_composite = 5108; - const save_node_template = 5114; - const detach_template = 5115; - const new_child_node = 5119; - const promote_goal = 5116; - const promote_turn = 5117; - const promote_timed = 5118; - const wire_node = 5109; - const mark_entry = 5112; - const edit_edge = 5110; - const delete_edge = 5111; - const create_edge = 5120; - const open_quick_chat = 5130; - const rename_quick_chat = 5131; - const delete_quick_chat = 5132; - const open_project = 5140; - const new_project_loop = 5141; - const inspect_project_worktrees = 5142; - const project_settings = 5143; - const reveal_project = 5144; - const remote_project_info = 5145; - const close_project = 5146; - const remove_project = 5147; - const move_project = 5149; - const trash_project = 5151; - const delete_project_loops = 5148; - const new_quick_chat = 5150; -}; - -pub const move_project_menu_text = "Move Project... (unavailable: daemon support required)"; - -pub const MoveProjectMenuItem = struct { - id: usize = ids.move_project, - text: []const u8 = move_project_menu_text, - enabled: bool, -}; - -/// Builds the real "Move Project..." popup item used by `show()` for the -/// project context menu. Exposed so tests can exercise the exact same -/// data the live Win32 menu is constructed from, rather than only a -/// separate accessibility contract model. -pub fn moveProjectMenuItem() MoveProjectMenuItem { - return .{ .enabled = Wire.supportsProjectRelocation() }; -} - -pub const NodeMenuItem = struct { - id: usize = 0, - text: []const u8 = "", - enabled: bool = true, -}; - -pub fn newChildNodeMenuItem(node: NodeTarget) ?NodeMenuItem { - if (node.resolved) return null; - return .{ .id = ids.new_child_node, .text = "New Child Node...", .enabled = node.can_create_child }; -} - -pub const NodeMenuPlan = struct { - items: [15]NodeMenuItem = undefined, - len: usize = 0, - - fn add(self: *NodeMenuPlan, item: NodeMenuItem) void { - self.items[self.len] = item; - self.len += 1; - } -}; - -pub fn nodeMenuPlan(node: NodeTarget) NodeMenuPlan { - var plan = NodeMenuPlan{}; - plan.add(.{ .id = ids.open_terminal, .text = "Open Terminal" }); - if (newChildNodeMenuItem(node)) |item| plan.add(item); - if (node.unwired) { - plan.add(.{ .id = ids.wire_node, .text = "Wire it up" }); - plan.add(.{ .id = ids.mark_entry, .text = "Mark as entry" }); - plan.add(.{}); - } - if (node.composite) { - plan.add(.{ .id = ids.open_composite, .text = "Open Group" }); - plan.add(.{ .id = ids.pilot_composite, .text = "Pilot Once" }); - plan.add(.{ .id = ids.arm_composite, .text = "Arm Schedule", .enabled = node.can_arm }); - plan.add(.{}); - } - plan.add(.{ .id = ids.edit_node, .text = "Edit Details..." }); - plan.add(.{ .id = ids.save_node_template, .text = "Save as Template..." }); - if (node.follows_template) plan.add(.{ .id = ids.detach_template, .text = "Detach from Template" }); - plan.add(.{ .id = ids.rename_node, .text = "Rename...\tF2" }); - if (!node.resolved) plan.add(.{ .id = ids.stop_node, .text = "Stop\tCtrl+S" }); - plan.add(.{ .id = ids.delete_node, .text = "Delete Loop...\tDelete" }); - return plan; -} - -pub const PromotionItem = struct { id: usize, text: []const u8, action: Action }; -const promotion_items = [_]PromotionItem{ - .{ .id = ids.promote_goal, .text = "Goal - asks for a done check", .action = .promote_goal }, - .{ .id = ids.promote_turn, .text = "Turn - asks where to pause", .action = .promote_turn }, - .{ .id = ids.promote_timed, .text = "Timed - asks for a cadence", .action = .promote_timed }, -}; - -pub fn promotionItems(node: NodeTarget) []const PromotionItem { - return if (node.sketch) &promotion_items else &.{}; -} - -pub fn promotionTarget(action: Action) ?SketchPromotion.Target { - return switch (action) { - .promote_goal => .goal, - .promote_turn => .turn, - .promote_timed => .timed, - else => null, - }; -} - -pub fn promotionEnabled(node: NodeTarget) bool { - return node.sketch and node.promotion_context != null; -} - -pub fn show( - parent: c.HWND, - target: Target, - x: i32, - y: i32, - context: ?*anyopaque, - callback: Callback, -) void { - const menu = buildMenu(target) orelse return; - defer _ = c.DestroyMenu(menu); - const command = c.TrackPopupMenu( - menu, - c.TPM_RETURNCMD | c.TPM_NONOTIFY | c.TPM_RIGHTBUTTON, - x, - y, - 0, - parent, - null, - ); - const action = actionForCommand(command); - if (action != .none) callback(context, action, target); -} - -fn buildMenu(target: Target) c.HMENU { - const menu = c.CreatePopupMenu() orelse return null; - switch (target) { - .background => |background| { - if (!std.mem.eql(u8, background.project_path, "graphcode://global")) { - appendEnabled(menu, ids.inspect_project_worktrees, "Worktrees...", background.local_filesystem); - appendEnabled(menu, ids.project_settings, "Project Settings...", background.local_filesystem); - appendEnabled(menu, ids.reveal_project, "Show in Explorer", background.local_filesystem); - separator(menu); - } - appendEnabled(menu, ids.create_edge, "Create Edge", background.can_create_edge); - }, - .quick_chats => append(menu, ids.new_quick_chat, "New Chat"), - .project => |project| { - append(menu, ids.open_project, "Open Project"); - append(menu, ids.new_project_loop, "New Loop...\tCtrl+N"); - separator(menu); - append(menu, ids.inspect_project_worktrees, "Worktrees..."); - append(menu, ids.project_settings, "Project Settings..."); - if (project.remote) - append(menu, ids.remote_project_info, "Remote Connection Info") - else - append(menu, ids.reveal_project, "Show in Explorer"); - separator(menu); - append(menu, ids.close_project, "Close Project"); - if (!project.remote) { - const move_item = moveProjectMenuItem(); - appendEnabled(menu, move_item.id, move_item.text, move_item.enabled); - append(menu, ids.trash_project, "Move to Recycle Bin..."); - } - append(menu, ids.remove_project, "Remove from GraphCode..."); - append(menu, ids.delete_project_loops, "Delete All Loops..."); - }, - .node => |node| { - const plan = nodeMenuPlan(node); - for (plan.items[0..plan.len]) |item| { - if (item.id == 0) separator(menu) else appendEnabled(menu, item.id, item.text, item.enabled); - if (item.id == ids.edit_node) { - const items = promotionItems(node); - if (items.len != 0) { - const submenu = c.CreatePopupMenu() orelse { - _ = c.DestroyMenu(menu); - return null; - }; - for (items) |promotion_item| appendEnabled(submenu, promotion_item.id, promotion_item.text, promotionEnabled(node)); - if (c.AppendMenuW(menu, c.MF_POPUP | c.MF_STRING, @intFromPtr(submenu), std.unicode.utf8ToUtf16LeStringLiteral("Promote to...").ptr) == 0) { - _ = c.DestroyMenu(submenu); - _ = c.DestroyMenu(menu); - return null; - } - } - } - } - }, - .edge => { - append(menu, ids.edit_edge, "Edit Edge..."); - append(menu, ids.delete_edge, "Delete Edge"); - }, - .quick_chat => { - append(menu, ids.open_quick_chat, "Open Chat"); - append(menu, ids.rename_quick_chat, "Rename...\tCtrl+Shift+Q"); - append(menu, ids.delete_quick_chat, "Delete Chat...\tCtrl+Shift+Delete"); - }, - } - return menu; -} - -pub fn confirm(parent: c.HWND, title: []const u8, message: []const u8) bool { - const title_wide = toWide(title) orelse return false; - defer std.heap.c_allocator.free(title_wide); - const message_wide = toWide(message) orelse return false; - defer std.heap.c_allocator.free(message_wide); - return c.MessageBoxW(parent, message_wide.ptr, title_wide.ptr, c.MB_ICONWARNING | c.MB_YESNO | c.MB_DEFBUTTON2) == c.IDYES; -} - -fn actionForCommand(command: c_int) Action { - return switch (command) { - ids.rename_node => .rename_node, - ids.stop_node => .stop_node, - ids.delete_node => .delete_node, - ids.open_terminal => .open_terminal, - ids.new_child_node => .new_child_node, - ids.edit_node => .edit_node, - ids.promote_goal => .promote_goal, - ids.promote_turn => .promote_turn, - ids.promote_timed => .promote_timed, - ids.open_composite => .open_composite, - ids.pilot_composite => .pilot_composite, - ids.arm_composite => .arm_composite, - ids.save_node_template => .save_node_template, - ids.detach_template => .detach_template, - ids.wire_node => .wire_node, - ids.mark_entry => .mark_entry, - ids.edit_edge => .edit_edge, - ids.delete_edge => .delete_edge, - ids.create_edge => .create_edge, - ids.open_quick_chat => .open_quick_chat, - ids.rename_quick_chat => .rename_quick_chat, - ids.delete_quick_chat => .delete_quick_chat, - ids.open_project => .open_project, - ids.new_project_loop => .new_project_loop, - ids.inspect_project_worktrees => .inspect_project_worktrees, - ids.project_settings => .project_settings, - ids.reveal_project => .reveal_project, - ids.remote_project_info => .remote_project_info, - ids.close_project => .close_project, - ids.remove_project => .remove_project, - ids.move_project => .move_project, - ids.trash_project => .trash_project, - ids.delete_project_loops => .delete_project_loops, - ids.new_quick_chat => .new_quick_chat, - else => .none, - }; -} - -fn append(menu: c.HMENU, id: usize, text: []const u8) void { - appendEnabled(menu, id, text, true); -} - -fn appendEnabled(menu: c.HMENU, id: usize, text: []const u8, enabled: bool) void { - const wide = toWide(text) orelse return; - defer std.heap.c_allocator.free(wide); - var flags: c.UINT = @intCast(c.MF_STRING); - if (!enabled) flags |= @intCast(c.MF_GRAYED); - _ = c.AppendMenuW(menu, flags, id, wide.ptr); -} - -fn separator(menu: c.HMENU) void { - _ = c.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); -} - -fn toWide(text: []const u8) ?[]u16 { - const raw = std.unicode.utf8ToUtf16LeAlloc(std.heap.c_allocator, text) catch return null; - const result = std.heap.c_allocator.alloc(u16, raw.len + 1) catch { - std.heap.c_allocator.free(raw); - return null; - }; - @memcpy(result[0..raw.len], raw); - result[raw.len] = 0; - std.heap.c_allocator.free(raw); - return result; -} - -const std = @import("std"); - -test "custody child menu plan is unresolved-only and uses its reserved command" { - const target = NodeTarget{ .project_path = "B", .id = "11111111-1111-4111-8111-111111111111" }; - const item = newChildNodeMenuItem(target).?; - try std.testing.expectEqual(@as(usize, 5119), item.id); - try std.testing.expectEqualStrings("New Child Node...", item.text); - try std.testing.expect(item.enabled); - try std.testing.expectEqual(Action.new_child_node, actionForCommand(@intCast(item.id))); - var resolved = target; - resolved.resolved = true; - try std.testing.expect(newChildNodeMenuItem(resolved) == null); - try std.testing.expectEqual(Action.none, actionForCommand(0)); -} - -test "custody child node menu plan preserves existing items when creation is unavailable" { - const expected_unresolved = [_]usize{ 5104, 5119, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5102, 5103 }; - const expected_resolved = [_]usize{ 5104, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5103 }; - for ([_]bool{ false, true }) |resolved| { - const plan = nodeMenuPlan(.{ - .project_path = "B", - .id = "parent", - .composite = true, - .unwired = true, - .follows_template = true, - .resolved = resolved, - .can_create_child = false, - }); - const expected: []const usize = if (resolved) &expected_resolved else &expected_unresolved; - try std.testing.expectEqual(expected.len, plan.len); - for (plan.items[0..plan.len], expected) |item, id| { - try std.testing.expectEqual(id, item.id); - try std.testing.expectEqual(id != 5119 and id != 5108, item.enabled); - } - try std.testing.expectEqualStrings("Open Terminal", plan.items[0].text); - try std.testing.expectEqualStrings("Delete Loop...\tDelete", plan.items[plan.len - 1].text); - } -} - -test "sketch promotion real menu plan exposes only three eligible target actions" { - var node = NodeTarget{ .project_path = "B", .id = "id" }; - try std.testing.expectEqual(@as(usize, 0), promotionItems(node).len); - node.sketch = true; - try std.testing.expectEqual(@as(usize, 3), promotionItems(node).len); - try std.testing.expect(!promotionEnabled(node)); - const context = SketchPromotion.Context{}; - node.promotion_context = &context; - try std.testing.expect(promotionEnabled(node)); - for (promotionItems(node), [_]SketchPromotion.Target{ .goal, .turn, .timed }) |item, target| { - try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); - try std.testing.expectEqual(target, promotionTarget(item.action).?); - } - for (std.enums.values(Action)) |action| { - if (action != .promote_goal and action != .promote_turn and action != .promote_timed) - try std.testing.expect(promotionTarget(action) == null); - } -} - -test "background menu exposes supported folder actions and gates edge creation" { - const menu = buildMenu(.{ .background = .{ - .project_path = "C:\\fixture", - .local_filesystem = true, - .can_create_edge = false, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.inspect_project_worktrees, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.project_settings, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.reveal_project, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(menu, ids.create_edge, c.MF_BYCOMMAND)); -} - -test "resolved node menu hides Stop but retains rename and edit details" { - const menu = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-a", - .resolved = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.stop_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.rename_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.message_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.memo_node, c.MF_BYCOMMAND)); -} - -test "node shortcut captions keep Open Terminal without a standalone Enter binding" { - const InputRouter = @import("InputRouter.zig"); - try std.testing.expectEqual(InputRouter.Action.none, InputRouter.keyAction(c.VK_RETURN, false, false)); - try std.testing.expectEqual(InputRouter.HeaderKey.none, InputRouter.headerKey(c.VK_RETURN, false, false, false, false)); - try std.testing.expectEqual(InputRouter.HeaderKey.activate, InputRouter.headerKey(c.VK_RETURN, false, false, false, true)); - try std.testing.expectEqual(Action.open_terminal, actionForCommand(ids.open_terminal)); - - const targets = [_]NodeTarget{ - .{ .project_path = "C:\\fixture", .id = "ordinary" }, - .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, - .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, - .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, - }; - for (targets) |target| { - const menu = buildMenu(.{ .node = target }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, ids.open_terminal), c.GetMenuItemID(menu, 0)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.open_terminal, c.MF_BYCOMMAND)); - var caption: [128]u16 = undefined; - const length = c.GetMenuStringW(menu, ids.open_terminal, &caption, caption.len, c.MF_BYCOMMAND); - try std.testing.expect(length > 0 and length < caption.len - 1); - try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Open Terminal"), caption[0..@intCast(length)]); - } -} - -test "node shortcut captions keep Edit Details without the rename Ctrl E hint" { - const InputRouter = @import("InputRouter.zig"); - try std.testing.expectEqual(InputRouter.Action.edit_node, InputRouter.keyAction('E', true, false)); - try std.testing.expectEqual(InputRouter.Action.rename_selected, InputRouter.keyAction(c.VK_F2, false, false)); - try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); - try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); - - const targets = [_]struct { node: NodeTarget, edit_position: c_int }{ - .{ .node = .{ .project_path = "C:\\fixture", .id = "ordinary" }, .edit_position = 2 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, .edit_position = 1 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, .edit_position = 6 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, .edit_position = 5 }, - }; - for (targets) |target| { - const menu = buildMenu(.{ .node = target.node }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, ids.edit_node), c.GetMenuItemID(menu, target.edit_position)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); - var caption: [128]u16 = undefined; - const length = c.GetMenuStringW(menu, ids.edit_node, &caption, caption.len, c.MF_BYCOMMAND); - try std.testing.expect(length > 0 and length < caption.len - 1); - try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Edit Details..."), caption[0..@intCast(length)]); - } -} - -test "background menu disables unavailable folder actions and omits them for global scope" { - const remote = buildMenu(.{ .background = .{ - .project_path = "ssh://builder/fixture", - .local_filesystem = false, - .can_create_edge = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(remote); - for ([_]c.UINT{ ids.inspect_project_worktrees, ids.project_settings, ids.reveal_project }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(remote, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(remote, ids.create_edge, c.MF_BYCOMMAND)); - - const global = buildMenu(.{ .background = .{ - .project_path = "graphcode://global", - .local_filesystem = false, - .can_create_edge = false, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(global); - try std.testing.expectEqual(@as(c_int, 1), c.GetMenuItemCount(global)); - try std.testing.expectEqual(@as(c.UINT, ids.create_edge), c.GetMenuItemID(global, 0)); -} - -test "native node menu variants expose only eligible actions" { - for ([_]bool{ false, true }) |can_arm| { - const menu = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-b", - .composite = true, - .can_arm = can_arm, - .follows_template = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, if (can_arm) c.MF_ENABLED else c.MF_GRAYED), c.GetMenuState(menu, ids.arm_composite, c.MF_BYCOMMAND)); - for ([_]c.UINT{ ids.open_composite, ids.pilot_composite, ids.stop_node, ids.detach_template }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.wire_node, c.MF_BYCOMMAND)); - } - const unwired = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-c", - .unwired = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(unwired); - for ([_]c.UINT{ ids.wire_node, ids.mark_entry, ids.stop_node }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(unwired, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(unwired, ids.arm_composite, c.MF_BYCOMMAND)); -} - -test "edge menu preserves edit and delete command ordering" { - const menu = buildMenu(.{ .edge = .{ .project_path = "C:\\fixture", .id = "edge-a" } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c_int, 2), c.GetMenuItemCount(menu)); - try std.testing.expectEqual(@as(c.UINT, ids.edit_edge), c.GetMenuItemID(menu, 0)); - try std.testing.expectEqual(@as(c.UINT, ids.delete_edge), c.GetMenuItemID(menu, 1)); -} - -test "context actions remain stable when graph IDs are reordered" { - try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); - try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); - try std.testing.expectEqual(Action.delete_edge, actionForCommand(ids.delete_edge)); - try std.testing.expectEqual(Action.none, actionForCommand(0)); - try std.testing.expectEqual(Action.pilot_composite, actionForCommand(ids.pilot_composite)); - try std.testing.expectEqual(Action.open_composite, actionForCommand(ids.open_composite)); - try std.testing.expectEqual(Action.arm_composite, actionForCommand(ids.arm_composite)); - try std.testing.expectEqual(Action.wire_node, actionForCommand(ids.wire_node)); - try std.testing.expectEqual(Action.mark_entry, actionForCommand(ids.mark_entry)); - try std.testing.expectEqual(Action.save_node_template, actionForCommand(ids.save_node_template)); - try std.testing.expectEqual(Action.detach_template, actionForCommand(ids.detach_template)); -} - -test "destructive context actions cannot bypass a cancelled confirmation" { - try std.testing.expect(!shouldApply(.delete_node, false)); - try std.testing.expect(!shouldApply(.delete_edge, false)); - try std.testing.expect(!shouldApply(.delete_quick_chat, false)); - try std.testing.expect(!shouldApply(.remove_project, false)); - try std.testing.expect(!shouldApply(.trash_project, false)); - try std.testing.expect(!shouldApply(.delete_project_loops, false)); - try std.testing.expect(shouldApply(.rename_node, false)); -} - -test "quick chat context targets preserve stable identity" { - const target = QuickChatTarget{ .id = "chat-a" }; - try std.testing.expectEqualStrings("chat-a", target.id); - try std.testing.expectEqual(Action.open_quick_chat, actionForCommand(ids.open_quick_chat)); - try std.testing.expectEqual(Action.rename_quick_chat, actionForCommand(ids.rename_quick_chat)); - try std.testing.expectEqual(Action.delete_quick_chat, actionForCommand(ids.delete_quick_chat)); -} - -test "edge editing requires a stable edge identifier" { - try std.testing.expect(!canEditEdge("")); - try std.testing.expect(canEditEdge("edge-1")); -} - -test "context targets carry stable copied identity rather than collection indices" { - const node = NodeTarget{ .project_path = "C:\\work\\graph", .id = "node-a" }; - const edge = EdgeTarget{ .project_path = "C:\\work\\graph", .id = "edge-a" }; - try std.testing.expectEqualStrings("node-a", node.id); - try std.testing.expectEqualStrings("edge-a", edge.id); - try std.testing.expectEqualStrings("C:\\work\\graph", edge.project_path); -} - -test "project context commands expose ingress management and safe destructive actions" { - const target = ProjectTarget{ .path = "C:\\work\\graph", .remote = false }; - try std.testing.expectEqualStrings("C:\\work\\graph", target.path); - try std.testing.expectEqual(Action.open_project, actionForCommand(ids.open_project)); - try std.testing.expectEqual(Action.project_settings, actionForCommand(ids.project_settings)); - try std.testing.expectEqual(Action.remove_project, actionForCommand(ids.remove_project)); - try std.testing.expectEqual(Action.move_project, actionForCommand(ids.move_project)); - try std.testing.expectEqual(Action.trash_project, actionForCommand(ids.trash_project)); - try std.testing.expectEqual(Action.delete_project_loops, actionForCommand(ids.delete_project_loops)); -} - -test "project relocation is visibly unavailable rather than an Explorer alias" { - try std.testing.expect(!Wire.supportsProjectRelocation()); - try std.testing.expect(std.mem.indexOf( - u8, - Wire.project_relocation_unavailable_reason, - "authoritative moveProject command", - ) != null); -} - -test "the real Move Project menu item is disabled with its explicit reason inline" { - // This exercises moveProjectMenuItem() directly: the same function - // show() calls to append the actual Win32 popup entry, not a - // separate accessibility-only model. It fails the moment the item's - // command id, label, or enabled state drift from what the live - // context menu renders. - const item = moveProjectMenuItem(); - try std.testing.expectEqual(@as(usize, ids.move_project), item.id); - try std.testing.expectEqualStrings( - "Move Project... (unavailable: daemon support required)", - item.text, - ); - try std.testing.expect(!item.enabled); - try std.testing.expectEqual(Action.move_project, actionForCommand(@intCast(item.id))); -} +const c = @import("Win32.zig").c; +const Wire = @import("Wire.zig"); +const SketchPromotion = @import("SketchPromotion.zig"); + +pub const NodeTarget = struct { + project_path: []const u8, + id: []const u8, + composite: bool = false, + can_arm: bool = false, + unwired: bool = false, + follows_template: bool = false, + resolved: bool = false, + can_create_child: bool = true, + sketch: bool = false, + promotion_context: ?*const SketchPromotion.Context = null, +}; + +pub const BackgroundTarget = struct { + project_path: []const u8, + local_filesystem: bool, + can_create_edge: bool, +}; + +pub const EdgeTarget = struct { + project_path: []const u8, + id: []const u8, +}; + +pub const QuickChatTarget = struct { + id: []const u8, +}; + +pub const ProjectTarget = struct { + path: []const u8, + remote: bool, +}; + +pub const Target = union(enum) { + background: BackgroundTarget, + quick_chats, + project: ProjectTarget, + node: NodeTarget, + edge: EdgeTarget, + quick_chat: QuickChatTarget, +}; + +pub const Action = enum { + none, + edit_node, + promote_goal, + promote_turn, + promote_timed, + rename_node, + stop_node, + delete_node, + open_terminal, + new_child_node, + message_node, + memo_node, + open_composite, + pilot_composite, + arm_composite, + save_node_template, + detach_template, + wire_node, + mark_entry, + edit_edge, + delete_edge, + create_edge, + open_quick_chat, + rename_quick_chat, + delete_quick_chat, + open_project, + new_project_loop, + inspect_project_worktrees, + project_settings, + reveal_project, + remote_project_info, + close_project, + remove_project, + move_project, + trash_project, + delete_project_loops, + new_quick_chat, +}; + +pub const Callback = *const fn (?*anyopaque, Action, Target) void; + +pub fn requiresConfirmation(action: Action) bool { + return action == .delete_node or action == .delete_edge or action == .delete_quick_chat or + action == .remove_project or action == .trash_project or action == .delete_project_loops; +} + +pub fn shouldApply(action: Action, confirmed: bool) bool { + return !requiresConfirmation(action) or confirmed; +} + +pub fn canEditEdge(edge_id: []const u8) bool { + return edge_id.len != 0; +} + +const ids = struct { + const edit_node = 5100; + const rename_node = 5101; + const stop_node = 5102; + const delete_node = 5103; + const open_terminal = 5104; + const message_node = 5105; + const memo_node = 5106; + const open_composite = 5113; + const pilot_composite = 5107; + const arm_composite = 5108; + const save_node_template = 5114; + const detach_template = 5115; + const new_child_node = 5119; + const promote_goal = 5116; + const promote_turn = 5117; + const promote_timed = 5118; + const wire_node = 5109; + const mark_entry = 5112; + const edit_edge = 5110; + const delete_edge = 5111; + const create_edge = 5120; + const open_quick_chat = 5130; + const rename_quick_chat = 5131; + const delete_quick_chat = 5132; + const open_project = 5140; + const new_project_loop = 5141; + const inspect_project_worktrees = 5142; + const project_settings = 5143; + const reveal_project = 5144; + const remote_project_info = 5145; + const close_project = 5146; + const remove_project = 5147; + const move_project = 5149; + const trash_project = 5151; + const delete_project_loops = 5148; + const new_quick_chat = 5150; +}; + +pub const move_project_menu_text = "Move Project... (unavailable: daemon support required)"; + +pub const MoveProjectMenuItem = struct { + id: usize = ids.move_project, + text: []const u8 = move_project_menu_text, + enabled: bool, +}; + +/// Builds the real "Move Project..." popup item used by `show()` for the +/// project context menu. Exposed so tests can exercise the exact same +/// data the live Win32 menu is constructed from, rather than only a +/// separate accessibility contract model. +pub fn moveProjectMenuItem() MoveProjectMenuItem { + return .{ .enabled = Wire.supportsProjectRelocation() }; +} + +pub const NodeMenuItem = struct { + id: usize = 0, + text: []const u8 = "", + enabled: bool = true, +}; + +pub fn newChildNodeMenuItem(node: NodeTarget) ?NodeMenuItem { + if (node.resolved) return null; + return .{ .id = ids.new_child_node, .text = "New Child Node...", .enabled = node.can_create_child }; +} + +pub const NodeMenuPlan = struct { + items: [15]NodeMenuItem = undefined, + len: usize = 0, + + fn add(self: *NodeMenuPlan, item: NodeMenuItem) void { + self.items[self.len] = item; + self.len += 1; + } +}; + +pub fn nodeMenuPlan(node: NodeTarget) NodeMenuPlan { + var plan = NodeMenuPlan{}; + plan.add(.{ .id = ids.open_terminal, .text = "Open Terminal" }); + if (newChildNodeMenuItem(node)) |item| plan.add(item); + if (node.unwired) { + plan.add(.{ .id = ids.wire_node, .text = "Wire it up" }); + plan.add(.{ .id = ids.mark_entry, .text = "Mark as entry" }); + plan.add(.{}); + } + if (node.composite) { + plan.add(.{ .id = ids.open_composite, .text = "Open Group" }); + plan.add(.{ .id = ids.pilot_composite, .text = "Pilot Once" }); + plan.add(.{ .id = ids.arm_composite, .text = "Arm Schedule", .enabled = node.can_arm }); + plan.add(.{}); + } + plan.add(.{ .id = ids.edit_node, .text = "Edit Details..." }); + plan.add(.{ .id = ids.save_node_template, .text = "Save as Template..." }); + if (node.follows_template) plan.add(.{ .id = ids.detach_template, .text = "Detach from Template" }); + plan.add(.{ .id = ids.rename_node, .text = "Rename...\tF2" }); + if (!node.resolved) plan.add(.{ .id = ids.stop_node, .text = "Stop\tCtrl+S" }); + plan.add(.{ .id = ids.delete_node, .text = "Delete Loop...\tDelete" }); + return plan; +} + +pub const PromotionItem = struct { id: usize, text: []const u8, action: Action }; +const promotion_items = [_]PromotionItem{ + .{ .id = ids.promote_goal, .text = "Goal - asks for a done check", .action = .promote_goal }, + .{ .id = ids.promote_turn, .text = "Turn - asks where to pause", .action = .promote_turn }, + .{ .id = ids.promote_timed, .text = "Timed - asks for a cadence", .action = .promote_timed }, +}; + +pub fn promotionItems(node: NodeTarget) []const PromotionItem { + return if (node.sketch) &promotion_items else &.{}; +} + +pub fn promotionTarget(action: Action) ?SketchPromotion.Target { + return switch (action) { + .promote_goal => .goal, + .promote_turn => .turn, + .promote_timed => .timed, + else => null, + }; +} + +pub fn promotionEnabled(node: NodeTarget) bool { + return node.sketch and node.promotion_context != null; +} + +pub fn show( + parent: c.HWND, + target: Target, + x: i32, + y: i32, + context: ?*anyopaque, + callback: Callback, +) void { + const menu = buildMenu(target) orelse return; + defer _ = c.DestroyMenu(menu); + const command = c.TrackPopupMenu( + menu, + c.TPM_RETURNCMD | c.TPM_NONOTIFY | c.TPM_RIGHTBUTTON, + x, + y, + 0, + parent, + null, + ); + const action = actionForCommand(command); + if (action != .none) callback(context, action, target); +} + +fn buildMenu(target: Target) c.HMENU { + const menu = c.CreatePopupMenu() orelse return null; + switch (target) { + .background => |background| { + if (!std.mem.eql(u8, background.project_path, "graphcode://global")) { + appendEnabled(menu, ids.inspect_project_worktrees, "Worktrees...", background.local_filesystem); + appendEnabled(menu, ids.project_settings, "Project Settings...", background.local_filesystem); + appendEnabled(menu, ids.reveal_project, "Show in Explorer", background.local_filesystem); + separator(menu); + } + appendEnabled(menu, ids.create_edge, "Create Edge", background.can_create_edge); + }, + .quick_chats => append(menu, ids.new_quick_chat, "New Chat"), + .project => |project| { + append(menu, ids.open_project, "Open Project"); + append(menu, ids.new_project_loop, "New Loop...\tCtrl+N"); + separator(menu); + append(menu, ids.inspect_project_worktrees, "Worktrees..."); + append(menu, ids.project_settings, "Project Settings..."); + if (project.remote) + append(menu, ids.remote_project_info, "Remote Connection Info") + else + append(menu, ids.reveal_project, "Show in Explorer"); + separator(menu); + append(menu, ids.close_project, "Close Project"); + if (!project.remote) { + const move_item = moveProjectMenuItem(); + appendEnabled(menu, move_item.id, move_item.text, move_item.enabled); + append(menu, ids.trash_project, "Move to Recycle Bin..."); + } + append(menu, ids.remove_project, "Remove from GraphCode..."); + append(menu, ids.delete_project_loops, "Delete All Loops..."); + }, + .node => |node| { + const plan = nodeMenuPlan(node); + for (plan.items[0..plan.len]) |item| { + if (item.id == 0) separator(menu) else appendEnabled(menu, item.id, item.text, item.enabled); + if (item.id == ids.edit_node) { + const items = promotionItems(node); + if (items.len != 0) { + const submenu = c.CreatePopupMenu() orelse { + _ = c.DestroyMenu(menu); + return null; + }; + for (items) |promotion_item| appendEnabled(submenu, promotion_item.id, promotion_item.text, promotionEnabled(node)); + if (c.AppendMenuW(menu, c.MF_POPUP | c.MF_STRING, @intFromPtr(submenu), std.unicode.utf8ToUtf16LeStringLiteral("Promote to...").ptr) == 0) { + _ = c.DestroyMenu(submenu); + _ = c.DestroyMenu(menu); + return null; + } + } + } + } + }, + .edge => { + append(menu, ids.edit_edge, "Edit Edge..."); + append(menu, ids.delete_edge, "Delete Edge"); + }, + .quick_chat => { + append(menu, ids.open_quick_chat, "Open Chat"); + append(menu, ids.rename_quick_chat, "Rename...\tCtrl+Shift+Q"); + append(menu, ids.delete_quick_chat, "Delete Chat...\tCtrl+Shift+Delete"); + }, + } + return menu; +} + +pub fn confirm(parent: c.HWND, title: []const u8, message: []const u8) bool { + const title_wide = toWide(title) orelse return false; + defer std.heap.c_allocator.free(title_wide); + const message_wide = toWide(message) orelse return false; + defer std.heap.c_allocator.free(message_wide); + return c.MessageBoxW(parent, message_wide.ptr, title_wide.ptr, c.MB_ICONWARNING | c.MB_YESNO | c.MB_DEFBUTTON2) == c.IDYES; +} + +fn actionForCommand(command: c_int) Action { + return switch (command) { + ids.rename_node => .rename_node, + ids.stop_node => .stop_node, + ids.delete_node => .delete_node, + ids.open_terminal => .open_terminal, + ids.new_child_node => .new_child_node, + ids.edit_node => .edit_node, + ids.promote_goal => .promote_goal, + ids.promote_turn => .promote_turn, + ids.promote_timed => .promote_timed, + ids.open_composite => .open_composite, + ids.pilot_composite => .pilot_composite, + ids.arm_composite => .arm_composite, + ids.save_node_template => .save_node_template, + ids.detach_template => .detach_template, + ids.wire_node => .wire_node, + ids.mark_entry => .mark_entry, + ids.edit_edge => .edit_edge, + ids.delete_edge => .delete_edge, + ids.create_edge => .create_edge, + ids.open_quick_chat => .open_quick_chat, + ids.rename_quick_chat => .rename_quick_chat, + ids.delete_quick_chat => .delete_quick_chat, + ids.open_project => .open_project, + ids.new_project_loop => .new_project_loop, + ids.inspect_project_worktrees => .inspect_project_worktrees, + ids.project_settings => .project_settings, + ids.reveal_project => .reveal_project, + ids.remote_project_info => .remote_project_info, + ids.close_project => .close_project, + ids.remove_project => .remove_project, + ids.move_project => .move_project, + ids.trash_project => .trash_project, + ids.delete_project_loops => .delete_project_loops, + ids.new_quick_chat => .new_quick_chat, + else => .none, + }; +} + +fn append(menu: c.HMENU, id: usize, text: []const u8) void { + appendEnabled(menu, id, text, true); +} + +fn appendEnabled(menu: c.HMENU, id: usize, text: []const u8, enabled: bool) void { + const wide = toWide(text) orelse return; + defer std.heap.c_allocator.free(wide); + var flags: c.UINT = @intCast(c.MF_STRING); + if (!enabled) flags |= @intCast(c.MF_GRAYED); + _ = c.AppendMenuW(menu, flags, id, wide.ptr); +} + +fn separator(menu: c.HMENU) void { + _ = c.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); +} + +fn toWide(text: []const u8) ?[]u16 { + const raw = std.unicode.utf8ToUtf16LeAlloc(std.heap.c_allocator, text) catch return null; + const result = std.heap.c_allocator.alloc(u16, raw.len + 1) catch { + std.heap.c_allocator.free(raw); + return null; + }; + @memcpy(result[0..raw.len], raw); + result[raw.len] = 0; + std.heap.c_allocator.free(raw); + return result; +} + +const std = @import("std"); + +test "custody child menu plan is unresolved-only and uses its reserved command" { + const target = NodeTarget{ .project_path = "B", .id = "11111111-1111-4111-8111-111111111111" }; + const item = newChildNodeMenuItem(target).?; + try std.testing.expectEqual(@as(usize, 5119), item.id); + try std.testing.expectEqualStrings("New Child Node...", item.text); + try std.testing.expect(item.enabled); + try std.testing.expectEqual(Action.new_child_node, actionForCommand(@intCast(item.id))); + var resolved = target; + resolved.resolved = true; + try std.testing.expect(newChildNodeMenuItem(resolved) == null); + try std.testing.expectEqual(Action.none, actionForCommand(0)); +} + +test "custody child node menu plan preserves existing items when creation is unavailable" { + const expected_unresolved = [_]usize{ 5104, 5119, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5102, 5103 }; + const expected_resolved = [_]usize{ 5104, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5103 }; + for ([_]bool{ false, true }) |resolved| { + const plan = nodeMenuPlan(.{ + .project_path = "B", + .id = "parent", + .composite = true, + .unwired = true, + .follows_template = true, + .resolved = resolved, + .can_create_child = false, + }); + const expected: []const usize = if (resolved) &expected_resolved else &expected_unresolved; + try std.testing.expectEqual(expected.len, plan.len); + for (plan.items[0..plan.len], expected) |item, id| { + try std.testing.expectEqual(id, item.id); + try std.testing.expectEqual(id != 5119 and id != 5108, item.enabled); + } + try std.testing.expectEqualStrings("Open Terminal", plan.items[0].text); + try std.testing.expectEqualStrings("Delete Loop...\tDelete", plan.items[plan.len - 1].text); + } +} + +test "sketch promotion real menu plan exposes only three eligible target actions" { + var node = NodeTarget{ .project_path = "B", .id = "id" }; + try std.testing.expectEqual(@as(usize, 0), promotionItems(node).len); + node.sketch = true; + try std.testing.expectEqual(@as(usize, 3), promotionItems(node).len); + try std.testing.expect(!promotionEnabled(node)); + const context = SketchPromotion.Context{}; + node.promotion_context = &context; + try std.testing.expect(promotionEnabled(node)); + for (promotionItems(node), [_]SketchPromotion.Target{ .goal, .turn, .timed }) |item, target| { + try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); + try std.testing.expectEqual(target, promotionTarget(item.action).?); + } + for (std.enums.values(Action)) |action| { + if (action != .promote_goal and action != .promote_turn and action != .promote_timed) + try std.testing.expect(promotionTarget(action) == null); + } +} + +test "sketch promotion production popup installs native Goal Turn Timed submenu on hidden HWND" { + const hwnd = c.CreateWindowExW( + 0, + std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), + std.unicode.utf8ToUtf16LeStringLiteral("Promotion menu test"), + c.WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + null, + null, + c.GetModuleHandleW(null), + null, + ) orelse return error.WindowCreationFailed; + defer _ = c.DestroyWindow(hwnd); + + const context = SketchPromotion.Context{}; + const cases = [_]struct { sketch: bool, context: ?*const SketchPromotion.Context, enabled: bool }{ + .{ .sketch = false, .context = null, .enabled = false }, + .{ .sketch = true, .context = null, .enabled = false }, + .{ .sketch = true, .context = &context, .enabled = true }, + }; + for (cases) |case| { + const popup = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "sketch-a", + .sketch = case.sketch, + .promotion_context = case.context, + } }) orelse return error.MenuCreationFailed; + const bar = c.CreateMenu() orelse { + _ = c.DestroyMenu(popup); + return error.MenuCreationFailed; + }; + defer { + _ = c.SetMenu(hwnd, null); + _ = c.DestroyMenu(bar); + } + if (c.AppendMenuW(bar, c.MF_POPUP | c.MF_STRING, @intFromPtr(popup), std.unicode.utf8ToUtf16LeStringLiteral("Node").ptr) == 0) { + _ = c.DestroyMenu(popup); + return error.MenuInstallFailed; + } + if (c.SetMenu(hwnd, bar) == 0) return error.MenuInstallFailed; + const menu = c.GetSubMenu(c.GetMenu(hwnd), 0); + try std.testing.expect(menu != null); + var edit_position: c_int = 0; + while (edit_position < c.GetMenuItemCount(menu) and c.GetMenuItemID(menu, edit_position) != ids.edit_node) : (edit_position += 1) {} + try std.testing.expect(edit_position < c.GetMenuItemCount(menu)); + const submenu = c.GetSubMenu(menu, edit_position + 1); + if (!case.sketch) { + try std.testing.expect(submenu == null); + for ([_]c.UINT{ 5116, 5117, 5118 }) |id| + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); + continue; + } + try std.testing.expect(submenu != null); + var title: [64]u16 = undefined; + const title_len = c.GetMenuStringW(menu, @intCast(edit_position + 1), &title, title.len, c.MF_BYPOSITION); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Promote to..."), title[0..@intCast(title_len)]); + try std.testing.expectEqual(@as(c_int, 3), c.GetMenuItemCount(submenu)); + for ([_]struct { id: c.UINT, label: []const u16, action: Action }{ + .{ .id = 5116, .label = std.unicode.utf8ToUtf16LeStringLiteral("Goal - asks for a done check"), .action = .promote_goal }, + .{ .id = 5117, .label = std.unicode.utf8ToUtf16LeStringLiteral("Turn - asks where to pause"), .action = .promote_turn }, + .{ .id = 5118, .label = std.unicode.utf8ToUtf16LeStringLiteral("Timed - asks for a cadence"), .action = .promote_timed }, + }, 0..) |item, index| { + try std.testing.expectEqual(item.id, c.GetMenuItemID(submenu, @intCast(index))); + const state = c.GetMenuState(submenu, item.id, c.MF_BYCOMMAND); + try std.testing.expect(state != std.math.maxInt(c.UINT)); + try std.testing.expectEqual(case.enabled, state & c.MF_GRAYED == 0); + var label: [64]u16 = undefined; + const length = c.GetMenuStringW(submenu, item.id, &label, label.len, c.MF_BYCOMMAND); + try std.testing.expectEqualSlices(u16, item.label, label[0..@intCast(length)]); + try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); + } + } +} + +test "background menu exposes supported folder actions and gates edge creation" { + const menu = buildMenu(.{ .background = .{ + .project_path = "C:\\fixture", + .local_filesystem = true, + .can_create_edge = false, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.inspect_project_worktrees, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.project_settings, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.reveal_project, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(menu, ids.create_edge, c.MF_BYCOMMAND)); +} + +test "resolved node menu hides Stop but retains rename and edit details" { + const menu = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-a", + .resolved = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.stop_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.rename_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.message_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.memo_node, c.MF_BYCOMMAND)); +} + +test "node shortcut captions keep Open Terminal without a standalone Enter binding" { + const InputRouter = @import("InputRouter.zig"); + try std.testing.expectEqual(InputRouter.Action.none, InputRouter.keyAction(c.VK_RETURN, false, false)); + try std.testing.expectEqual(InputRouter.HeaderKey.none, InputRouter.headerKey(c.VK_RETURN, false, false, false, false)); + try std.testing.expectEqual(InputRouter.HeaderKey.activate, InputRouter.headerKey(c.VK_RETURN, false, false, false, true)); + try std.testing.expectEqual(Action.open_terminal, actionForCommand(ids.open_terminal)); + + const targets = [_]NodeTarget{ + .{ .project_path = "C:\\fixture", .id = "ordinary" }, + .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, + .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, + .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, + }; + for (targets) |target| { + const menu = buildMenu(.{ .node = target }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, ids.open_terminal), c.GetMenuItemID(menu, 0)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.open_terminal, c.MF_BYCOMMAND)); + var caption: [128]u16 = undefined; + const length = c.GetMenuStringW(menu, ids.open_terminal, &caption, caption.len, c.MF_BYCOMMAND); + try std.testing.expect(length > 0 and length < caption.len - 1); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Open Terminal"), caption[0..@intCast(length)]); + } +} + +test "node shortcut captions keep Edit Details without the rename Ctrl E hint" { + const InputRouter = @import("InputRouter.zig"); + try std.testing.expectEqual(InputRouter.Action.edit_node, InputRouter.keyAction('E', true, false)); + try std.testing.expectEqual(InputRouter.Action.rename_selected, InputRouter.keyAction(c.VK_F2, false, false)); + try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); + try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); + + const targets = [_]struct { node: NodeTarget, edit_position: c_int }{ + .{ .node = .{ .project_path = "C:\\fixture", .id = "ordinary" }, .edit_position = 2 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, .edit_position = 1 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, .edit_position = 6 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, .edit_position = 5 }, + }; + for (targets) |target| { + const menu = buildMenu(.{ .node = target.node }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, ids.edit_node), c.GetMenuItemID(menu, target.edit_position)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); + var caption: [128]u16 = undefined; + const length = c.GetMenuStringW(menu, ids.edit_node, &caption, caption.len, c.MF_BYCOMMAND); + try std.testing.expect(length > 0 and length < caption.len - 1); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Edit Details..."), caption[0..@intCast(length)]); + } +} + +test "background menu disables unavailable folder actions and omits them for global scope" { + const remote = buildMenu(.{ .background = .{ + .project_path = "ssh://builder/fixture", + .local_filesystem = false, + .can_create_edge = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(remote); + for ([_]c.UINT{ ids.inspect_project_worktrees, ids.project_settings, ids.reveal_project }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(remote, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(remote, ids.create_edge, c.MF_BYCOMMAND)); + + const global = buildMenu(.{ .background = .{ + .project_path = "graphcode://global", + .local_filesystem = false, + .can_create_edge = false, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(global); + try std.testing.expectEqual(@as(c_int, 1), c.GetMenuItemCount(global)); + try std.testing.expectEqual(@as(c.UINT, ids.create_edge), c.GetMenuItemID(global, 0)); +} + +test "native node menu variants expose only eligible actions" { + for ([_]bool{ false, true }) |can_arm| { + const menu = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-b", + .composite = true, + .can_arm = can_arm, + .follows_template = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, if (can_arm) c.MF_ENABLED else c.MF_GRAYED), c.GetMenuState(menu, ids.arm_composite, c.MF_BYCOMMAND)); + for ([_]c.UINT{ ids.open_composite, ids.pilot_composite, ids.stop_node, ids.detach_template }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.wire_node, c.MF_BYCOMMAND)); + } + const unwired = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-c", + .unwired = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(unwired); + for ([_]c.UINT{ ids.wire_node, ids.mark_entry, ids.stop_node }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(unwired, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(unwired, ids.arm_composite, c.MF_BYCOMMAND)); +} + +test "edge menu preserves edit and delete command ordering" { + const menu = buildMenu(.{ .edge = .{ .project_path = "C:\\fixture", .id = "edge-a" } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c_int, 2), c.GetMenuItemCount(menu)); + try std.testing.expectEqual(@as(c.UINT, ids.edit_edge), c.GetMenuItemID(menu, 0)); + try std.testing.expectEqual(@as(c.UINT, ids.delete_edge), c.GetMenuItemID(menu, 1)); +} + +test "context actions remain stable when graph IDs are reordered" { + try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); + try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); + try std.testing.expectEqual(Action.delete_edge, actionForCommand(ids.delete_edge)); + try std.testing.expectEqual(Action.none, actionForCommand(0)); + try std.testing.expectEqual(Action.pilot_composite, actionForCommand(ids.pilot_composite)); + try std.testing.expectEqual(Action.open_composite, actionForCommand(ids.open_composite)); + try std.testing.expectEqual(Action.arm_composite, actionForCommand(ids.arm_composite)); + try std.testing.expectEqual(Action.wire_node, actionForCommand(ids.wire_node)); + try std.testing.expectEqual(Action.mark_entry, actionForCommand(ids.mark_entry)); + try std.testing.expectEqual(Action.save_node_template, actionForCommand(ids.save_node_template)); + try std.testing.expectEqual(Action.detach_template, actionForCommand(ids.detach_template)); +} + +test "destructive context actions cannot bypass a cancelled confirmation" { + try std.testing.expect(!shouldApply(.delete_node, false)); + try std.testing.expect(!shouldApply(.delete_edge, false)); + try std.testing.expect(!shouldApply(.delete_quick_chat, false)); + try std.testing.expect(!shouldApply(.remove_project, false)); + try std.testing.expect(!shouldApply(.trash_project, false)); + try std.testing.expect(!shouldApply(.delete_project_loops, false)); + try std.testing.expect(shouldApply(.rename_node, false)); +} + +test "quick chat context targets preserve stable identity" { + const target = QuickChatTarget{ .id = "chat-a" }; + try std.testing.expectEqualStrings("chat-a", target.id); + try std.testing.expectEqual(Action.open_quick_chat, actionForCommand(ids.open_quick_chat)); + try std.testing.expectEqual(Action.rename_quick_chat, actionForCommand(ids.rename_quick_chat)); + try std.testing.expectEqual(Action.delete_quick_chat, actionForCommand(ids.delete_quick_chat)); +} + +test "edge editing requires a stable edge identifier" { + try std.testing.expect(!canEditEdge("")); + try std.testing.expect(canEditEdge("edge-1")); +} + +test "context targets carry stable copied identity rather than collection indices" { + const node = NodeTarget{ .project_path = "C:\\work\\graph", .id = "node-a" }; + const edge = EdgeTarget{ .project_path = "C:\\work\\graph", .id = "edge-a" }; + try std.testing.expectEqualStrings("node-a", node.id); + try std.testing.expectEqualStrings("edge-a", edge.id); + try std.testing.expectEqualStrings("C:\\work\\graph", edge.project_path); +} + +test "project context commands expose ingress management and safe destructive actions" { + const target = ProjectTarget{ .path = "C:\\work\\graph", .remote = false }; + try std.testing.expectEqualStrings("C:\\work\\graph", target.path); + try std.testing.expectEqual(Action.open_project, actionForCommand(ids.open_project)); + try std.testing.expectEqual(Action.project_settings, actionForCommand(ids.project_settings)); + try std.testing.expectEqual(Action.remove_project, actionForCommand(ids.remove_project)); + try std.testing.expectEqual(Action.move_project, actionForCommand(ids.move_project)); + try std.testing.expectEqual(Action.trash_project, actionForCommand(ids.trash_project)); + try std.testing.expectEqual(Action.delete_project_loops, actionForCommand(ids.delete_project_loops)); +} + +test "project relocation is visibly unavailable rather than an Explorer alias" { + try std.testing.expect(!Wire.supportsProjectRelocation()); + try std.testing.expect(std.mem.indexOf( + u8, + Wire.project_relocation_unavailable_reason, + "authoritative moveProject command", + ) != null); +} + +test "the real Move Project menu item is disabled with its explicit reason inline" { + // This exercises moveProjectMenuItem() directly: the same function + // show() calls to append the actual Win32 popup entry, not a + // separate accessibility-only model. It fails the moment the item's + // command id, label, or enabled state drift from what the live + // context menu renders. + const item = moveProjectMenuItem(); + try std.testing.expectEqual(@as(usize, ids.move_project), item.id); + try std.testing.expectEqualStrings( + "Move Project... (unavailable: daemon support required)", + item.text, + ); + try std.testing.expect(!item.enabled); + try std.testing.expectEqual(Action.move_project, actionForCommand(@intCast(item.id))); +} diff --git a/investigation/ui-parity-matrix.md b/investigation/ui-parity-matrix.md index d9022d4f..6ed5f39a 100644 --- a/investigation/ui-parity-matrix.md +++ b/investigation/ui-parity-matrix.md @@ -1,213 +1,213 @@ -# Windows UI parity ledger - -This is a source-derived completion ledger, not a requirements sketch. A row is -`Validated` only when the Windows implementation exposes the same user-visible -information and actions as macOS and has runtime evidence. Platform-native chrome may -differ, but hiding a feature behind an undocumented shortcut or replacing a structured -screen with raw protocol fields is not parity. - -Statuses: - -- `Validated`: source mapping, automated coverage, and live walkthrough agree. -- `Partial`: some behavior exists, but visible controls, state, or interaction is absent - or materially different. -- `Missing`: no equivalent reachable Windows surface. -- `Blocked`: requires a deliberate platform decision or unavailable dependency. -- `Divergent`: Windows exposes a different product concept in the place where the macOS - surface belongs; it must be separated or redesigned before parity. - -## Application shell and navigation - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Main split view | Persistent sidebar; detail switches among welcome, global graph, project canvas, Quick Chats canvas, and loop workspace | Explicit destinations exist; the earlier stub walkthrough covered project → overview → workspace → Show in Graph with sidebar retention. The shared header now follows destination identity, distinguishes a Quick Chat workspace from a previously selected project, and keeps detail-panel toggling inside the loop workspace. Production-state and layout tests cover the destinations, narrow widths, and sidebar/panel independence. The current live attempt stopped at foreground acquisition before UIA root access: no new live UIA SetFocus, F6, Jump, pixels, or sidebar-effect proof was obtained, and provider-backed workspace/panel behavior remains unverified | Partial | -| Window toolbar | Needs-you chip, worktree notice, jump field, contextual loop-panel toggle | Render, hit-test, UIA exposure, and focus order share header visibility/layout. Needs-you opens the selected attention target; cached local worktree notices honor the owning project and count/size threshold. Jump opens the existing palette even with no projects. The detail toggle is limited to loop workspaces with supported edge/metric content; workspace/project identity is no longer overpainted. F6 entry/exit, scoped traversal/activation, focus rendering, and marshaled UIA button focus are implemented, while ordinary Tab/Shift+Tab/Ctrl+Tab contracts remain. The header paints inside the buffered logical pass; literal 96/144/192-DPI bounds and hidden native client tests cover logical layout/input and exactly-once physical UIA conversion without rescaling terminal tabs. Production-helper tests, a hidden native Tab-to-command test, and a hidden empty-palette create/cancel test pass. Live focus/event-deadlock, keyboard activation, rendered pixels, sidebar effects, and provider-backed panel proof are still absent because foreground acquisition failed. Cross-project worktree-notice discovery/aggregation, summary/board/mailroom panel content, and macOS visual parity remain separate residuals; private live tooling is not CI coverage | Partial | -| Jump palette | Search field, ranked cross-project results, type/state/project context, mouse and keyboard selection | Ctrl+P and Ctrl+J open a native modal palette with live exact-ID, exact-title, title-prefix, and substring ranking across projects. Results visibly include project, loop type, and state; Up/Down, Return, Escape, and mouse double-click are supported. The deterministic UIA gate verifies a visible search field, contextual cross-project results, and keyboard navigation changing the selected loop. | Validated | -| File/Loop/Terminal menus | Discoverable project, worktree, navigation, workspace, update, settings, and help commands with state-aware enablement | Startup menu replacement and UTF-16 corruption are fixed and the five readable runtime groups were probed. Reclaim/Reveal require a selected row (`worktreeRowSelected()` retains dedicated unit coverage); Save requires the Worktrees dialog open. Loop commands now gray out when they have no actionable target: Jump requires a loaded loop anywhere, Next/Previous require at least two loops or one unselected loop in the active graph, Create Edge requires two active-graph loops, and Stop requires a selected loop. These distinct thresholds follow their different production handlers and are intentional. Terminal Next/Previous Tab and Focus Next/Previous Pane require multiple tabs or panes in the selected tab. A hidden-window test calls `MainWindow.updateMenu` and verifies the real native HMENU `MF_GRAYED` bits in both unavailable and available states; this is unit/runtime Win32-menu evidence, not a live application walkthrough. The Worktrees-dialog UIA gate evidence remains outstanding: obtaining it requires launching the shell in a foreground desktop session, and no application executable is built in this worktree; provider builds/provisioning and foreground-gated UIA automation were not performed. Broader project-management parity remains incomplete | Partial | -| Workspace lifecycle | List/switch, create, rename, delete with fail-closed confirmation, and keyboard/UIA reachability | Windows discovers `Default` plus `.graphcode-*` siblings and exposes New/Rename/Delete and Ctrl+Alt paging. Manage now has an owned row model/native list, Default/creation-time/name-tie order, identity-deduplicated current-outside-home inclusion, full-path detail, current/default/open/uncertain-window refusals, and captured-identity Open/New/Rename handoff after modal teardown. An App-owned single joinable reader supplies bounded **saved top-level** graph-header counts, not live/descendant totals; current live content, missing/unreadable/invalid/duplicate/limited/unsupported-location data remain explicitly unavailable, not partial zero. Fixed local/non-reparse reads use existing project JSON and the documented mailroom-array exception, no daemon/backend or window activation. Done/Escape cancels; pending actions wait for reader cancellation/join while the modal pumps, and shutdown drains before allocator teardown (a stalled local disk can delay shutdown). Manager Delete is disabled pending recoverable deletion/teardown; existing menu deletion and ordinary menu ordering/Open/New semantics are unchanged. Next/Previous now reread the manager-ordered list, include validated current outside home, filter identified running windows, wrap both ways, recheck the target, and use a restore-only API with no launcher. Closed/disappeared targets never cold-open; any unidentified flag or lookup/restore failure refuses with status. Existing SID/session/class/published-metadata checks are preserved, not executable-path attestation. Menu capability counts known rows plus implicit current, without periodic window polling. Injected production-helper RED/GREEN, allocation-failure/owned-refresh, ordering/dedup/offset/race/refusal tests and pure menu-capability tests execute in the existing App/MainWindow roots; full native roots compile/link without execution and the ReleaseSafe app builds without launch. Manager coverage remains pure owned-data/explicit disposable fixtures, controlled memory-only joined threads, and filtered form/App data seams; no new shown manager/native-control/window-lookup/UIA/runtime proof is claimed. Existing lifecycle helper and never-shown native-control regressions cover accepted text lifetime, allocation cleanup, normalized naming/collisions, lexical identity, exact-window and one-launch routing, Default/current/open refusals, child-only support/daemon-pipe isolation, canonical plus legacy reservations, unidentified-window refusal, fail-closed Settings rebinding, and disposable confirmed mutation preservation. Lexical identity is not junction equivalence. Shown-dialog accessibility/keyboard/layout, full multi-instance/save-reload behavior, reliable live totals, real running-cycle keyboard/window proof, and recoverable deletion with session/daemon teardown remain residuals. Shared-host menu failures still have an unproven cause; no full lifecycle parity or host-only diagnosis is claimed | Partial | -| Help menu | GraphCode Basics and normal About entry | The live Help menu exposes GraphCode Basics, which reopens onboarding, and About GraphCode, which opens a native versioned product dialog. The populated UIA gate verifies the dialog identity, version text, and close behavior | Validated | -| Update command | Check for Updates, disabled while checking/installing | Reachable from the Help menu's native menu bar, immediately reports "Checking for updates..." status and disables the command the instant a check is invoked, and — after fixing a real bug where the background check's completion never refreshed the menu bit because the only refresh path was gated on unrelated daemon-connectivity state — now reliably re-enables the command once the check settles. A dedicated live UIA gate assertion (reading the real native `HMENU` bit via `GetMenuState`, not the fragile UIA tree) and a Win32-window-backed unit test both confirm the disable/re-enable cycle. When the settled result is an available update, the gate additionally waits for the real offer, dismisses it via Later, verifies the modal disappears, and requires the shell owner to be enabled again before continuing; failure/no-update outcomes are required not to present an offer. In-app installation is now implemented as a separate scope (see Install progress/Relaunch prompt, Partial) | Validated | -| Tray lifecycle | Restore and exit without foreground daemon window | `TrayLive.Tests.ps1` exercises the physical icon, Open, close-to-hide, single-instance restore, Explorer recovery, popup contents, and visible Exit activation | Validated | -| Connection failure presentation | Explicit visible failure without replacing normal navigation | A persistent inline canvas banner now reports daemon unavailability while leaving sidebar and destination navigation intact; ingress errors take precedence when present. The live UIA gate forces the disconnected state and verifies the dedicated banner text and bounds | Validated | - -Running-cycle follow-up evidence: the final lookup actually used for activation -now refuses mixed identified/unidentified results before activating that same -target; ordinary Open keeps its target-first policy. The real accelerator -descriptor and eligible pretranslation/top-level fallback now wire Ctrl+Alt -paging to the same cycle action before terminal-child dispatch. Pure injected -final-lookup, descriptor/modifier, and message-data tests cover these paths and -preserve Ctrl-only tabs/F6/F10 routing. The corrected native menu regression is -compiled, not locally executed. This is not an atomic global-window snapshot or -native keyboard/accelerator/window proof; the workspace row remains Partial. - -## First-run and empty states - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Four-page onboarding | Visual terminology tour, Skip/Back/Continue/Get Started, backend selection, reopen, persisted seen state | Custom rounded Win32 onboarding; all pages exercised and persistence verified | Validated | -| No-project Welcome detail | Graph icon, pitch, explanatory copy, Open Folder action, inline error | Windows matches the centered Graph identity, pitch, explanatory copy, and single Open Folder action. Persistent project-ingress failures now also render as a bounded, wrapped inline canvas alert without replacing navigation; focused geometry coverage and the populated UIA gate verify the alert text and live bounds | Validated | -| Empty global graph | “Nothing running yet”, explanatory copy, Open Folder action, New Loop action | The dedicated overview empty state exposes both bounded Open Folder and New Loop actions; New Loop targets the daemon's `graphcode://global` project. The live UIA gate switches to an empty model, verifies both visible native controls, invokes New Loop, and observes the node form | Validated | -| Empty project canvas | Project-specific empty message and New Loop action | The dedicated “No loops yet” project state exposes its visible New Loop action. The live UIA gate installs an empty local project, invokes that exact command, and observes the project-scoped node form | Validated | -| Empty Quick Chats canvas | Explanation of Quick Chats and New Chat action | Live walkthrough verified the dedicated explanation and New Chat action with corrected non-overlapping layout | Validated | - -## Sidebar - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Graph row | Pinned global graph row with graph glyph | The sidebar now keeps a dedicated Graph destination visible even with no open project, labels it with a graph identity glyph, and routes it through the existing global-overview hit target and UIA destination | Validated | -| Quick Chats group | Selectable header, hover New Chat, disclosure, child rows | The native header remains selectable, reveals a hover-only New Chat action and disclosure, and exposes stable selectable child rows with Rename/Delete context actions. Focused menu tests cover stable chat identity; the live UIA gate invokes New Chat, collapses and restores children, and verifies child runtime identity survives. | Validated | -| Local/remote sections | Group labels, independent collapse, folder/network glyphs | LOCAL and REMOTE retain local/folder and remote/network identity and now toggle independently as native section actions. Focused layout coverage validates mixed ordering, and the live UIA gate collapses LOCAL while proving the REMOTE row and its stable automation identity remain present before restoring LOCAL. | Validated | -| Project rows | Selection, folder type, hover New Loop, disclosure | Open project rows retain selection and local/remote glyphs, reveal hover-only New Loop and disclosure controls, and collapse/restore their own loop tree without changing row identity. The live UIA gate invokes the project-row New Loop action into the real native node form and exercises project collapse/expand through stable UIA actions. | Validated | -| Nested loop tree | Edge-derived hierarchy, persisted expansion, drag reorder of roots | Handoff edges derive a cycle-safe root/descendant tree; nested rows disclose and collapse by stable node ID, expanded IDs persist atomically in the GraphCode support directory, and root rows now reorder through live pointer drag backed by the existing transactional `root` records. Focused Sidebar/Wire coverage and the deterministic UIA gate verify observable reorder plus emission of the new `sidebarNodesReordered` daemon command for server-side persistence parity. | Validated | -| Loop row presentation | Type stripe, title, elapsed time, state indicator | Rows now show a loop-type stripe, title, compact state indicator, and a compact elapsed value derived from `createdAt`. `Sidebar.elapsedText` now has focused boundary coverage for every unit rollover (seconds/minutes/hours/days) and its invalid-input guards (`created_at<=0`, `now<=created_at`), reverified via `zig test` and the full `WindowsShell.Tests.ps1` suite. This is still text painted directly onto the sidebar's `HDC` with no UIA identity of its own (the same limitation `Sidebar.updateBannerAt` had before this change), so a live assertion that reads the *rendered pixels* of the elapsed column was not captured this session; only the formatting logic and the row's overall live-rendering-without-crashing are executable evidence today | Partial | -| Project context menu | Move, worktrees, settings, Explorer, remote info, close, remove, delete loops/project | Project rows expose the lifecycle actions plus the Windows Recycle Bin path for local folders. Move is deliberately **not** an Explorer `/select` alias: `GraphContextMenu.moveProjectMenuItem()` appends "Move Project... (unavailable: daemon support required)" with `MF_GRAYED` while `Wire.supportsProjectRelocation()` is false, and the stale command path surfaces that explicit reason instead of opening Explorer. The live UIA gate now drives the real `TrackPopupMenu` popup (`MainWindow.wm_uia_context_menu` -> the same `GraphContextMenu.show()` the mouse path calls) and asserts the live menu's ordered items, that Move is command 5149 with that exact text and a disabled state, that a remote project's menu omits Move/Recycle Bin/Explorer entirely, and that the popup dismisses without wedging the shell. Note the observation channel: a popup menu appears in the UIA tree only as an empty Pane with no `MenuItem` children, so item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, but not UIA-tree evidence. | Validated | -| Loop context menu | Open, composite actions, rename, stop, delete | Sidebar and canvas loop rows share stable-ID Open, Rename, Stop, and Delete actions. Composite cards expose Open Group, Pilot Once, and Arm Schedule; the drilled-in canvas addresses mutations through the parent composite. The live UIA gate now opens and reads all three `wm_uia_context_menu` loop variants: target 3 (a plain wired loop) asserts Open/Rename/Stop/Delete are present and that every composite-only and unwired-only command is absent; target 6 (a composite, not-yet-piloted loop) asserts Open Group/Pilot Once/Arm Schedule are present with Arm Schedule rendered `MF_GRAYED` (not piloted), and that unwired-only commands are absent; target 7 (an unwired loop node, added live via the sidebar-reorder fixture mutation) asserts Wire it up/Mark as entry are present and composite-only commands are absent. As with the project context menu, item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, not UIA-tree evidence | Validated | -| Recent projects | Reachable from Add Folder menu | Recent and currently-open projects are now exposed as distinct sidebar rows, with unopened recents remaining under the LOCAL/REMOTE sections while open workspaces use separate `open-project` identities. The deterministic UIA gate verifies the split presentation and section behavior. The live gate now also walks the Recent Folders submenu reachable from Add Folder end-to-end: it sends a real `WM_INITMENUPOPUP` (the message `App.zig` uses to refresh `recent_folders` from the live model before a popup shows, so the read reflects the fixture's recent projects rather than pre-fixture placeholder state), reads the submenu's live items in fixture order with their stable `recent_folder_command_base`-derived command IDs, and invokes the second entry through the real `WM_COMMAND` route, confirming the shell routes it without crashing | Validated | -| Add Folder menu | Open Folder, Clone, Add Remote, recents | File now groups Open Folder, Clone Repository, Add Remote Repository, and Add Codespace under Add Folder and adds a dedicated Recent Folders submenu with its own command range; that submenu is now located rather than positionally indexed, so a new ingress entry can no longer silently retarget the rebuild. Focused MainWindow coverage validates the native menu structure and recent-folder command wiring. The live UIA gate now reads the Add Folder submenu directly off the live `HMENU` (`GetMenu`/`GetSubMenu`, not `TrackPopupMenu`, since this is the persistent menu bar) and asserts all four action labels plus the Recent Folders submenu; see the Recent projects row above for the live Recent Folders walkthrough this shares | Validated | -| Sidebar update banner | Available version and click-to-install action | A persistent footer banner now shows the retained offered version and reopens the native update offer when clicked. The live UIA gate now drives this through the real click path rather than the `GRAPHCODE_UIA_SHOW_UPDATE` bypass: since `Sidebar.updateBannerRect` has no UIA identity of its own, the gate computes the banner's live pixel geometry from the shell's real client height (matching the same viewport-bottom formula the paint code uses) and posts a genuine synthetic `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at that point, then verifies the real `GraphCode Update Available` dialog opens with the offered `GraphCode 9.9.9-test` version text, dismisses it via the live Later command, and confirms the shell survives. **In-app install remains deliberately Blocked** — the offer still hands installation off to the verified release page, and this row's evidence does not claim otherwise | Validated | -| Sidebar error footer | Persistent, scoped project-ingress error | Folder, clone, remote, and daemon-open failures now persist in a dedicated red sidebar footer independently of transient status. Successful project ingress clears it, wrapped layout preserves long messages, and the deterministic UIA gate verifies the dedicated footer identity plus multi-line bounds below the update offer. | Validated | -| Needs-you section | Navigable list with reason/project and Stop action | Up to four entries now expose selection, explicit reason copy, stable UIA identities, click/UIA navigation, and a dedicated Stop action. Focused routing coverage plus the deterministic UIA gate verify Stop targets the populated entry's real project path and loop ID. | Validated | -| Activity strip | Optional bottom strip, summary, attention-only filter, horizontally scrolling actionable events | Activity events retain project/node identity and timestamps, render timestamped cards, expose stable UIA rows plus scroll controls, and now keep a real horizontal viewport with an attention-only filter. Focused Sidebar coverage and the deterministic UIA gate verify scroll-state changes, attention-only filtering, and card navigation into the selected loop workspace. | Validated | - -## Graph overview and project canvas - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Cross-project global graph | Every open folder as a lane on one canvas | Existing lanes stack vertically, as on macOS. Production geometry tests now use four Alpha loops and two Beta loops, literal accumulated lane/card bounds, both distinct Open/Worktrees targets, transformed hit testing, a recent-only exclusion, and an empty ordinary-folder lane. `App.applyOverviewLaneAction` is the existing lane dispatch extracted without changing its ordering or selection semantics; the real overview callback consumes it before the unchanged loop/pan paths. Never-shown native tests exercise both projects' Open actions and emitted project/card selection and UIA bounds at 96/144/192 DPI. Interleaved graph refresh retains the other project's selected loop and updates stable card identity/geometry. Real scoped Worktrees actions start with no inspection, inspect two independent disposable Git roots, and require exact inspection/dialog paths and emitted primary rows, non-reclaimable primary safety, and preserved sentinel bytes; pre-seeded rows or an Invoke return cannot satisfy them. Deliberate wrong-identity/action/geometry controls are rejected before dispatch, not claimed as disabled-production-dispatcher or historical bug evidence. These are production-helper/model/UIA-data results, not shown rendering, OS input, COM invocation, mounted terminal/focus, or macOS runtime proof. The shared live overview segment still covers one project's two cards; simultaneous two-project capture and loop navigation remain unproved. macOS topology/START furniture, richer lane captions/chips, global-lane filtering, and remote/all-project worktree binding behavior also remain outside this slice | Partial | -| Folder lanes/bands | Project caption, worktree chip, open/close and folder actions | Overview lanes render distinct Open and Worktrees actions beside the project caption; click routing selects the project or opens scoped worktree inspection. Focused geometry/input coverage passes, and the local Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` posts real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` messages at the lane's Open and Worktrees hit-test rects against the live executable and verifies Open routes to the project canvas (synchronized cards render at a new position) and Worktrees opens the scoped inspection view, both confirmed passing across many consecutive live runs. This also uncovered and fixed two real accessibility bugs along the way: `App.zig`'s `.overview` mouse-click switch arm and its `.cycle_attention` action handler were both missing the `syncAccessibility()` call that keeps the live UIA tree in sync with what is rendered, so a lane's Open/Worktrees click previously had no observable effect through the accessibility tree even though the underlying surface did change | Validated | -| Notebook grid | Grid pans and zooms with canvas | `GraphCanvas.drawGrid` derives its cell size and offset from the exact same `CanvasState.zoom`/`pan_x`/`pan_y` fields consumed by `overviewCardBounds`, `overviewLaneBounds`, and the loop-card geometry, so the same focused pan/zoom coverage (`GraphCanvas.zig`: "canvas hit testing follows pan and zoom", "overview and quick chat geometry applies pan and zoom consistently") indirectly proves the grid cannot desynchronize from the content it underlays. The Windows shell toolchain blocker is resolved, but the grid itself is a 1px `0x00161815` GDI line pattern with no UIA surface of its own, and this session did not add a live pixel-scan assertion (the connector-handle and attention-rail blocks already show this pattern is feasible) to directly confirm grid line spacing changes with zoom in the running executable. Left Partial rather than claim live evidence that was not actually captured | Partial | -| Pan and anchored zoom | Pan, pointer-centered wheel/pinch zoom | Mouse pan and pointer-centered wheel zoom remain intact and unchanged, with the same focused regression coverage as before (`GraphCanvas.zig`: "canvas zoom keeps the graph point beneath the cursor stable", "canvas wheel zoom scales high-resolution trackpad deltas"). Native touchscreen pinch-zoom is now implemented and routed through the main window: `MainWindow.zig` registers only `GID_ZOOM` via `SetGestureConfig`, leaving every other gesture class (`GID_PAN`/`GID_ROTATE`/`GID_TWOFINGERTAP`/`GID_PRESSANDTAP`) at its existing OS default rather than explicitly blocking gestures this app has no opinion on, with a real registration test against a genuine `HWND` plus two independent negative controls — a malformed native `SetGestureConfig` call and a genuinely invalid `HWND` passed straight through the production `registerCanvasGestureConfig` helper itself — proving the helper's own `GetLastError()` capture path actually fires, not just the raw Win32 API. `App.zig`'s `WM_GESTURE` case decodes `GID_ZOOM` via a pure `CanvasInput.classifyGesture` decision table (including a distinct outcome for a gesture delivered as a single combined begin+end message, so it can never reuse a stale prior gesture's baseline), requires the active surface to actually render the graph canvas (not just the wheel-region rectangle, which the terminal workspace surface shares), and applies `GraphCanvas.zig`'s `beginPinchZoom`/`continuePinchZoom`/`endPinchZoom` against a per-gesture identity hash of surface+project so a same-region destination change mid-gesture (surface switch, or project switch while still graph-capable) resets the baseline instead of silently continuing to scale the wrong canvas; a failed `GetClientRect` is guarded and treated as unhandled rather than classified against an undefined rect, and the gesture handle is closed before any call that could re-enter the message loop. This is source-mapped, automated routing/unit evidence, not live hardware-input evidence: this session held no live UIA capture slot this pass, so pinch is proven by code mapping and a full deterministic test suite (non-compounding/clamp/zero-distance/lifecycle/context-mismatch/routing matrix, verified with genuine temporary-regression RED/GREEN passes against the production helpers, not GREEN-only), not an actual touchscreen or Precision Touchpad device. Per Microsoft's documented default, Precision Touchpad pinch on a classic Win32 window is emulated as synthetic Ctrl+`WM_MOUSEWHEEL`, not delivered as `WM_GESTURE`, so this implementation targets true touchscreen digitizers specifically; Precision Touchpad pinch behavior is not separately implemented or verified here. Touch-driven pan (`GID_PAN`) remains a genuine unimplemented gap: this app forwards it unhandled rather than half-handling it, but does not explicitly block it either. Left Partial: touchscreen pinch has real source and automated-test coverage but no live device evidence, and touch pan is still unimplemented | Partial | -| Zoom controls | Zoom out, actual size, zoom in, fit with shortcuts/help | Visible bottom-right controls provide zoom out, percentage/actual size, zoom in, and fit. A visible shortcut/help line accompanies the controls; Ctrl+-, Ctrl+0, Ctrl+=, and Ctrl+9 remain represented in the View menu. The Windows shell toolchain blocker is resolved and `Tools\windows\uia-live-gate.ps1` now runs against the live executable: it locates the `zoom-out`, `actual-size`, `zoom-in`, and `fit-canvas` UIA fragments, requires non-empty bounds, resolves each `InvokePattern`, and then actually invokes zoom-in, actual-size, zoom-out, and fit-canvas in sequence against the running shell, all of which completed without error across many consecutive live runs | Validated | -| New Loop canvas button | Visible top-right add action | A live-validated top-right New Loop button is now present on non-empty project canvases and remains centered in the empty state | Validated | -| Composite breadcrumb | Current group, project back action, loop count | Open Group swaps the project canvas to the authoritative nested graph, renders its cards and edges through the normal interactive canvas, and exposes a clickable `Project > Group` breadcrumb with loop count that restores and reselects the parent. Nested graph selection survives daemon refreshes, and the populated live UIA gate invokes Open Group, verifies both nested cards, and invokes the bounded Back breadcrumb to restore the parent canvas | Validated | -| Canvas attention rail | Count/oldest context and Review action | The rail exposes a clickable Review target and uses `createdAt` from the daemon model when present to show a true `oldest ` label alongside the oldest attention item title. Focused hit testing passes, and the Windows shell toolchain blocker is now resolved: the rail has no dedicated UIA element of its own (it is a full-width band GDI hit-test region), so `Tools\windows\uia-live-gate.ps1` posts a real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at the rail's exact screen rect against the live executable and verifies the click drives `App.zig`'s `.review_attention` -> `selectNextAttention()` routing by observing the resulting `SelectionItemPattern` selection actually move from one card to the NEEDS YOU card, passing across many consecutive live runs | Validated | -| Node positioning | Persisted positions and direct card movement where supported | Project cards can be dragged directly, with movement transformed correctly at non-default zoom, shared geometry/hit testing updated during the drag, and capture-loss cancellation restoring the prior position. Offsets are keyed to stable node identity, remapped across daemon reorder, and atomically persisted under the configured GraphCode support directory. Focused reorder/reload regressions and a real physical drag capture validate the complete flow | Validated | -| Connector handles | Hover handles and drag-to-connect | The right-edge connector tracks hover, paints a visible handle and plus affordance, and preserves the drag-to-connect path. Focused rendering/input coverage passes, and the Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` synthesizes real `WM_MOUSEMOVE` hover messages at the source card's outgoing connector position and confirms the live `0x7ACDFF` hover handle pixel actually appears on screen (`Test-ScreenPixelNear`), then drives a full `WM_LBUTTONDOWN`/`WM_MOUSEMOVE`/`WM_LBUTTONUP` drag from that connector onto a second card and confirms the resulting native "Create or edit edge" dialog locks its From/To fields to the exact dragged source and dropped target loop IDs, all passing across many consecutive live runs | Validated | -| Loop card identity | Loop-type stripe, title, state pill, entry/cycle role | Project and overview cards now use loop-type-colored stripes while retaining lifecycle state text, START, UNWIRED, and attention labels. Focused color regression coverage passes; live evidence remains blocked | Partial | -| Loop card live detail | Goal/prompt/check line, progress, metric change, elapsed/backend/model/worktree metadata | Cards prioritize goal, trigger, or check detail, retain current activity, and add metric pass/change text, elapsed age, backend identity, token usage, model tier, and worktree/branch metadata from the same decoded daemon fields used by the workspace loop bar. Focused card metadata tests (`GraphCanvas.zig`: "loop card metadata includes backend elapsed and token usage when reported") pass, and the `windows-shell` CI job's live UIA gate exercises the populated card fixture end to end (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, passing, merged as PR #399) | Validated | -| Loop card attention | Reason-aware amber presentation and primary action | NEEDS YOU cards render a card-level reason-specific primary button: `Reply` for reported awaiting-input sessions and `Inspect` for other attention reasons, both routed through the normal loop-opening path. Focused action-label tests (`GraphCanvas.zig`: "attention cards expose reason-specific primary actions") pass, and the live UIA gate's `attention-action-*` assertion for the deterministic awaiting-input card passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | -| Unwired card recovery | Explanation, Wire it up, Mark as entry | Cards with no inbound or outbound edge now show an explicit UNWIRED warning and recovery explanation. Their native context menu exposes Wire it up, which enters the existing drag-to-connect flow, and Mark as entry, which changes the card to START for the session. Focused role/action tests plus live menu and post-action captures validate the flow | Validated | -| Worktree reclaim offer | Reclaim and Keep actions on resolved card | Safe resolved cards with a matching landed, clean, pushed worktree expose separate Reclaim and Keep targets. Reclaim revalidates safety and Keep suppresses the offer for the session. Canvas Reclaim/Keep descendants are now emitted through the UIA provider and invoke the same fail-closed paths. Focused geometry/safety coverage passes, and `Tools\windows\uia-live-gate.ps1` now asserts the live Reclaim/Keep descendants under the Graph fragment (name, non-empty bounds, InvokePattern, and correct RawView/ControlView sibling linkage) via the `windows-shell` CI job (PR #385, run 35422364203, passing) | Validated | -| Composite card actions | Open Group, Pilot Once, Arm Schedule | Canvas and sidebar composite menus expose all three actions. Open Group is live-validated; nested creates, edits, deletes, edge changes, pilot, and arm commands use the daemon's authoritative `subGraphCommand` envelope; and Arm Schedule is disabled unless the decoded pilot state is exactly `piloted` | Validated | -| Edge presentation | Kind style, fired state, cycle label | Windows retains typed optional cycle guards and derives fired state from authoritative `fireCount`, with missing/null legacy fallback. Raw snapshot JSON reaches the production label formatter for maximum/until/flat-pass and empty-guard summaries; signed/null/malformed inputs, Unicode/long text, owned copies, refresh/composite transitions, and allocation-failure cleanup have executable coverage. Kind styling, selected emphasis, fixed 148-by-20 label bounds, ellipsis, and collision placement are retained. This is exact-string and static-geometry evidence only: full visible wording, the macOS context-menu summary, rendered pixels, and live label evidence remain unverified | Partial | -| Edge creation sheet | Kind/condition/transform/cycle controls with conditional validation | The guided native form provides endpoint selectors, conditional fields, validation, keyboard traversal, scrolling, and recap. Menu and connector creation now share the full draft sender, retaining condition, payload transform, all optional cycle guards, and spawn target. Project/composite identity and endpoint IDs/titles are owned across the modal; stale model/selection/client subgraph addressing and missing or changed endpoint choices are explicitly rejected without retargeting. The update subscription can still observe another cached project or refresh independently: outgoing creation explicitly addresses the captured project. Registered pure production-path tests inspect the real client's outgoing queue, including cached-project selection, Unicode/quotes, callback-induced graph changes, cancellation, and allocation failures. The reduced connector sender fails the same accepted-draft fixture that the full sender passes. Earlier live evidence only opened the form and checked endpoints/recap before cancellation; this fix has no new live modal/keyboard, daemon acceptance, persistence, or cross-platform parity evidence | Partial | -| Custody child creation | New Child Node on unresolved parents, inherited editable backend, daemon-owned custody | Project-canvas/sidebar node menus consume the tested unresolved-only item plan (5119). Owned popup target/settings/child-only exact-project worktree snapshots feed the existing guarded node form and template/attachment continuation. Original popup context is checked before normal initial selection; final guards never reselect, reject project/composite/address drift or deleted/resolved/type/backend-changed parents, and allow rename/reorder/unresolved progress. Tests exercise the production initializer/transfer boundary and real data-only client queue, including cached B while observing A, root-versus-composite same IDs, UUID/null wire fields, cancellation and allocation failures. `createdBy` produces one create command: GraphStore owns the fired handoff/report-back memo and normal startup. No new protocol or parent/session mutation. Native display/action results, overview right-click, daemon acceptance/persistence, and inherited downstream-send failure handling remain unverified. The merged ordinary-creation path now retains the owned worktree snapshots whose evidence is recorded in Node creation sheet, while custody retains its pre-popup owned snapshot; template backend settings remain a separate residual | Partial | -| Edge editing | Preserve identity/configuration and refuse stale edits | Windows uses an owned modal snapshot and one checked `updateEdge`, retaining endpoints, current fireCount, transform/spawn settings and unchanged optional legacy guards. Root/direct-composite scope and current cache are rechecked without treating observation subscription as authorization. Existing serialized daemon commands, blocking, parent roll-up and root publication remain authoritative; deeper new edit wrappers are refused. Production-helper regressions exercise actual Wire JSON/unstarted queues, foreign scopes, refresh, cancellation and allocation cleanup. Edit-only live/submit capture reads complete Unicode text and blocks acceptance on per-field read/allocation errors; injected-reader and initializer tests show no controls. In-memory Swift store tests cover CAS, kind/blocking rules, root publication, preview refusal and serialized legacy-child writeback followed by an edit that retains runtime progress. This is not native editing, daemon acceptance/persistence or macOS runtime evidence; macOS sources expose creation-time configuration and delete, not an existing edit UI | Partial | -| Node creation sheet | Loop-type teaching tiles, conditional fields, backend/model/branch pickers, recap, validation reason | A guided native form provides loop-type/backend/model choices, type-specific fields, explanatory copy, accessible checkboxes, inline validation, keyboard traversal, scrolling, and a live-updating recap. The native Branch picker offers inspection choices plus “This folder”; existing tests cover its empty state and selected binding, and earlier gate evidence covers the recap, Branch picker, and distinct Attach control. Staging now reserves a uniquely named file exclusively: the production append/removal regression first demonstrated A/B/C → remove B → add D overwriting C's bytes, then passed with C's original path and bytes preserved. Offline in-file coverage also checks template-restored/legacy files, preexisting destination collisions, partial-write rollback, allocation failures without orphan copies, invalid inputs, exactly eight files and the ninth rejection, and the exact 10 MiB/+1-byte boundary. Existing attachment DTO/wire tests remain passing; these are no-window helper/filesystem results, not OS input or daemon persistence evidence. Creation choices now own an exact-project snapshot of inspected path/branch strings across cache/model replacement; baseline aliasing and foreign-cache regressions fail before the fix and pass afterward. Noninteractive production-projection tests cover snapshot lifetime, projection without a loaded graph, and allocation-failure cleanup. Separate pure initializer/builder-to-Wire tests verify exact non-default repository/id/path/branch fields, initial/current/template-restored selection, and “This folder” clearing with available choices; legacy empty-choice hidden binding remains unchanged. This does not establish actual native non-default selection, a Git-inspection-to-creation flow, or daemon save/reload. **Still partial:** actual inspected nondefault binding and project-scoped ownership across pumped modals remain unproved; New branch and remote/global branch visibility differ from macOS; authentic OS picker acceptance/cancellation, clipboard paste/drop, and native-input → request → daemon save/reload evidence remain outstanding. Per-file removed-file reclamation and legacy unguarded cleanup remain separate residuals. Creation now retains the original project/composite context and validates after modal teardown, before attachment transfer, across normal, template-load-error, empty-library, and template-loop submissions without switching selection or client scope. Bounded offline production-boundary RED/GREEN and owned temporary-file tests cover stale-scope refusal, loaded-project closure, same-path refresh/promotion, path-only starts, valid empty composites, template continuation, allocation unwinding, and checked cleanup failures. Guarded staging reserves its attachment-directory leaf exclusively; cancellation/refusal and zero-reference acceptance abandon only that owned leaf, while nonzero accepted attachment bytes survive. Zero-reference drafts preserve existing empty-ID/wire behavior. These are helper/model/unstarted-client-queue proofs, not live modal, OS picker, or daemon-persistence evidence; path-only external disappearance and downstream send failure remain limitations | Partial | -| Node update/rename | Dedicated rename prompt and safe typed updates | Rename retains its dedicated safe prompt. Edit Details uses `NativeForms.update` and `sendUpdateNodeForm`; its project/node identity is now owned across the modal message loop and re-resolved afterward, and changed strings are compared against the form's owned initial snapshot rather than potentially freed graph strings. Original numeric scalars and clear-versus-unchanged semantics are preserved. Production-helper tests cover source mutation, unchanged/changed typed fields, cancellation, clearing, and allocation failures. The lifetime test rejects the original borrowed baseline. Live attempts stopped at background-menu activation before reaching editor or rename actions, so app-level dispatch, cancellation, and returned model results remain unverified in this work | Partial | -| Delete confirmations | Named object, consequences, safe default | Loop deletion names the loop and explains graph-connection removal. Edge deletion now names both endpoint loops and the connection kind, explains that the loops remain, re-resolves the stable edge after confirmation, and defaults to cancellation | Validated | -| Canvas context menu | Folder actions on background; complete node/edge actions | Project backgrounds now expose existing Worktrees, Project Settings, and Explorer routes with captured project context; unavailable non-filesystem actions are disabled and global scope omits them. Create Edge remains present but disabled with fewer than two loops. Resolved node menus omit Stop, matching macOS; composite/template/unwired actions and the absence of Message/Memo are preserved. Tests inspect the same native HMENU builder used by `TrackPopupMenu`, including an exact disabled-item assertion that fails when Create Edge is removed. The existing gate already inspects plain/composite/unwired popup contents; its succeeded-node Stop assertion now requires absence. Focused live attempts read the real background popup but did not achieve an action/result, and a minimal native control failed foreground acquisition before opening its menu; the cause remains unproven. Live node/edge/background action results, New Child live proof, and import/export remain deferred; custody child creation and sketch promotion retain their separate Partial evidence rows | Partial | -| Sketch promotion | Promote a sketch to Goal, Turn, or Timed without replacing its identity or session | Canvas and sidebar context menus use one production submenu plan and focused native forms. Owned popup/form snapshots reject stale scope, selection, type, and deletion; initial selection of a different cached project remains supported independently of the observation subscription. Filtered pure tests exercise the actual App selection adapter, typed native-form builder, and unstarted DaemonClient outbound queue, including composite addressing and allocation failures. Three emitted fixtures decode as existing Swift `promoteNode` variants, and the full Windows application builds ReleaseSafe. This proves local command construction/queueing only: no native keyboard, HMENU/HWND, UIA, app launch, or real-daemon acceptance/persistence was exercised for this feature | Partial | - -## Quick Chats - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Quick Chats canvas | Band, cards, pan/zoom, add button, empty state | The real executable was exercised with two deterministic chats. The band, transformed cards, top-right New Chat action, bottom-right zoom controls, and 100% → 110% zoom transition were captured live. Context actions are wired, and the populated live UIA gate validates named, bounded, invokable Quick Chat cards plus the populated-canvas New Chat action | Validated | -| Quick Chat cards | Title, chat identity, optional backend badge, open/rename/delete menu | A populated live fixture verified title/default-chat identity/optional backend rendering and direct opening (`openQuickChat` was observed by the protocol stub). Cards now expose Open Chat, Rename, and Delete Chat context actions | Validated | -| Create chat | Visible New Chat controls | Empty and populated Quick Chats canvases expose the New Chat action in the macOS placements. The live UIA gate invokes the populated canvas action and the sidebar/header action; the existing empty-state walkthrough invokes the centered empty-state action | Validated | -| Rename chat | Single title prompt from row/card | Uses a dedicated single-title modal from the card/keyboard action, trims input, and rejects empty titles | Validated | -| Delete chat | Named confirmation explaining session/scrollback deletion | Uses a named warning that explains terminal-session and scrollback removal, defaults to cancellation, and only sends deletion after confirmation | Validated | -| Chat workspace | Opens a persistent terminal workspace | Opening a Quick Chat now exposes a bounded, selected `Quick Chat terminal workspace` UIA surface while the existing terminal panel remains persistent. The full native UIA walkthrough verifies the card invocation and workspace transition without duplicating loop-workspace implementation | Validated | - -## Loop terminal workspace - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| 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. Production is still 120x40 without PTY resize negotiation; 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 | -| 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 | -| Mounted background tabs | Switching preserves live terminal surfaces | Workspace implementation tests still cover the topology, and the live UIA gate now creates a second mounted tab, switches between the original and background tab, and asserts both tab automation identities survive the round trip without shell exit/reconnection | Partial | -| Right loop panel | Minimap, upstream/downstream, fired conditions, metric sparkline, branch/start/usage footer | The full workspace right rail includes the selected-loop map, upstream/downstream cards, fired-edge coloring, edge conditions, branch/worktree identity, metric/goal detail, model tier, a metric-history sparkline from decoded samples, start-time/usage/backend footer text, a collapse/expand control that no longer reserves rail width while hidden, and dedicated UIA children for sparkline/start/usage/toggle (`workspace-detail-sparkline-*`, start, usage, and toggle automation IDs). The live UIA gate asserts those children and toggles collapse/expand, and that gate passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | -| Show in Graph | Visible loop-bar and menu action | The live gate invokes the real `workspace-show-graph-*` loop-bar action through `InvokePattern`, requires the exact selected `UIA loop A` `canvas-card-*` identity and selected state with strict workspace-chrome absence, then returns through the exact supported `loop-row-*` identity and requires the same project toolbar, selected-loop bar, and Show in Graph identities. A focused isolated pair at `.graphcode-evidence/show-in-graph-20260924-151455` used source gate SHA-256 `AE4A2C71...`, focused harness `78E6821E...`, and shell `8E0AA9E7...`: GREEN invoked the action and completed the exact round trip (stdout `E94DCA9D...`); RED disabled only that invocation and failed the unchanged chrome-absence assertion (stderr `3198914D...`). This confirms the route without a product fix; graph cards remain intentionally non-invokable. The focused early return did not exercise later downstream-provider rebinding. A full integrated gate pass and live native Loop-menu invocation remain unverified. | Partial | - -**Live-gate infrastructure fix (this session):** the `windows-shell` CI job's `uia-live-gate.ps1` step was, until now, never actually exercising any of the workspace chrome above: `App.init()` unconditionally skipped `Workspace.init()` under `GRAPHCODE_UIA_GATE=1` regardless of whether a real `zmx` executable was supplied (a pre-existing guard predating this workstream), so every "Partial" row above had never been run against a real workspace at all. Fixed in `App.zig` to build the real workspace under the gate whenever `GRAPHCODE_ZMX` is present. That surfaced a second, genuine regression: the newly-real terminal surface competed for native Win32 keyboard focus with the rest of the UI after navigating away from the workspace (`App.openGlobalOverview()` and friends). Root-caused to `Workspace.poll()` (driven by the main window's 100ms `WM_TIMER`) unconditionally draining terminal output and calling `winghostty_surface_notify_accessibility_text()` regardless of workspace visibility, which kept re-asserting UI Automation focus on the terminal no matter what Win32-level focus fixes were made. Fixed by adding `Workspace.collapse()`/`Workspace.collapsed`, skipping `resize()`/`syncTopology()`'s pane refocus and terminal-output polling entirely while the workspace is hidden, plus a `WM_ACTIVATE` handler that reasserts the app's own focus policy after `DefWindowProc`'s default child-focus restoration on window reactivation. All of this is now covered by the passing `windows-shell` CI job (commits `a31813b`..`cba010f`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793). Note: the separate `windows-spikes`/`windows-hardening` jobs (`validate.ps1 -Task all`) run the identical gate script but under much heavier CI load and still intermittently hit this same assertion's 15-second retry window; this has been confirmed as pre-existing, cross-branch flakiness unrelated to this workstream (an unrelated sibling branch, `coneilen-microsoft-repository-settings-parity`, shows both a pass and an unrelated failure on the same job across consecutive runs), not a regression introduced here. - -## Repository ingress - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Open Folder | Native picker from Welcome and Add Folder menu | Welcome and File menu commands use the Windows folder-only File Open dialog with filesystem/path validation. The live UIA gate invokes the empty-state action, verifies the titled native picker, and cancels it safely | Validated | -| Clone Repository sheet | Repository, location picker, derived folder, branch, depth, progress, inline failure, cancel | Clone runs behind a progress-capable native operation sheet with live output, cancellation, terminal status, and shared dark painting. Windows-shell contracts pass. The UIA gate opens the real sheet, verifies URL/destination/branch/depth fields, submits it empty to verify the inline URL error, and cancels it | Validated | -| Add Remote Repository sheet | Server/user/port/path, explanation, validation progress, inline selectable error | SSH validation runs on a worker while a validation sheet remains open and Connect is unavailable until completion. It shares Clone’s dark themed native dialog class. Windows-shell contracts pass. The UIA gate opens the real “Add SSH Repository” sheet, verifies host/user/port/path fields, submits it empty to verify inline validation, and cancels it | Validated | -| Remote Connection info | Read-only selectable connection sheet | Remote project context menus expose a dedicated read-only connection-information dialog with the encoded remote project identity and management guidance. The live UIA gate opens the native sheet, verifies both pieces of content, and closes it | Validated | -| Add Codespace sheet | Authenticated codespace discovery, selection, workspace path, validation progress, empty/error/retry/cancel, opens the remote project | `Codespaces.zig` + `WindowsCodespaceDialog.zig` provide discovery, failure remediation, validation, submit gating, and `codespace://` project opening. Deterministic Codespaces/dialog/windows-shell coverage remains green. **Still partial:** the available token lacks `codespace` scope; no real discovery-success, selection, validated dial, or rendered-sheet UIA walkthrough was possible. Only the 403/remediation path was exercised against real `gh` | Partial | - -## Settings and worktrees - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Product Settings window | Backend, three permission pickers, model picker/auto toggle, activity, briefing, beta with explanatory copy | `WindowsProductSettings.zig` exposes native backend/model/Claude/Copilot/Codex selectors, routing/activity/briefing/beta controls, macOS-equivalent consequence copy, and Save/Cancel. Focused tests cover settings preservation and selector copy; `GRAPHCODE_UIA_GATE` mutation 15 opens the real window against an isolated settings file, verifies every required visible control/explanation, proves control-targeted Return saves while preserving unknown fields, and proves Escape cancels byte-for-byte | Validated | -| Infrastructure diagnostics | If retained, separate advanced surface | Daemon pipe/support-directory overrides are now explicitly labeled “Advanced Connection Settings...” while the normal Settings command opens product settings | Validated | -| Project Settings sheet | Resolve policy radio rows, safety explanation, size/count thresholds, immediate save | Valid threshold/radio edits persist immediately, while empty/partial threshold input preserves the last valid persisted value; Done remains dismiss-only. The shared dark native form is covered by focused and Windows-shell tests. The UIA gate uses the gate-only hook to open it and verifies policy, threshold-unit, and worktree content before cancelling | Validated | -| Worktree sweep sheet | Safe/look/in-use grouping, size summaries, default selections, reveal, inline destructive confirmation, recovery note | Sweep storage supports 256 rows, real directory sizes and aggregate totals, reveal, and a second destructive confirmation for dirty rows. Focused and Windows-shell coverage pass. The UIA gate opens the real sheet through the gate-only hook and verifies safe/look group rendering and Remove Selected. The fixture proves sheet rendering and interaction affordances, not Git worktree discovery | Validated | -| Worktree notice chip | Threshold-driven titlebar and lane notice | Explicit inspection installs compact value observations on each exact `GraphSummary` owner; replacing the full inspection, selecting another project, or reordering lanes does not transfer its counts. Overview chips share one presentation/geometry source for paint, hit testing, and path-keyed UIA data, reuse the existing Worktrees action, and show uninspected, incomplete-size, policy-unavailable, stale, and failed-refresh states without fabricated zero/freshness. Checked policy outcomes distinguish missing/legacy defaults from unreadable or malformed configuration. Failed policy writes also reconcile the captured owner's checked readback while preserving the original write error. The shared inclusive count-or-binary-GiB evaluator uses exact known bytes; partial measurements are approximate, and unknown policy or stale observations cannot claim a configured breach. Relevant binding/state/connection/mutation signals stale observations; nested comparisons reuse the existing decoder and scoped IDs under depth, work, and scratch-memory bounds, and comparison uncertainty remains explicitly stale. Title/position-only edits and supported reordering do not manufacture a refresh. Existing owner close/restore eviction removes observations. An exact-current-owner raw-inspection accessor keeps correct-owner rows visible below threshold or when stale/policy-unknown, while excluding foreign current-root rows without changing retained modal state. Pure production-helper/mapper tests cover boundaries, atomic replacement, ownership, nested changes/limits, injected partial-write reconciliation, error states, old action rectangles, path resolution, and literal 96/144/192-DPI UIA-data bounds; the sizing, rounding-label, raw-inspection, nested-binding, and failed-write regressions have RED/GREEN evidence. These are synthetic-data/helper results, not actual filesystem-failure, Git-inspection, native modal, keyboard/click, or COM/UIA walkthrough evidence. Windows still counts all inspection rows including primary/prunable and sums logical file bytes; macOS excludes the opened checkout and sums non-prunable allocated usage. **Still partial:** automatic discovery, global titlebar aggregation, remote/project-canvas band parity, and authentic multi-lane/boundary review-action proof remain outstanding | Partial | - -**Bounded worktree subprocess reliability (2026-09-26):** local job-bounded -`WorktreeGitProcess.Tests.ps1` calls the actual production helper. The baseline -with only `create_no_window` launch instrumentation reproduced upper/mixed-case -repository redirection, outside index/object writes, and an unread real-Git -stderr hang. Child-only environment scoping, concurrent bounded pipe collection, -and owned-child/result cleanup now cover those cases. Real selected/forced -removals and synthetic nonempty output from all three mutation paths exercise -allocation ownership; synthetic auth/config preservation does not use secrets. -The normal WindowsShell runner invokes this suite. No production wall-clock -deadline, OS wait/kill fault injection, live UI/provider walkthrough, or full -parity is claimed. The pre-existing direct `reclaim` command's self-removal -failure on the tested Windows Git remains unchanged; the synthetic direct-call -case proves ownership only. All existing Partial rows remain Partial. - -## Updates and dialogs - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Available update alert | Install, Release Notes, Later | The native offer now conditions Install on whether the real feed check resolved a Windows asset URL (`WindowsUpdates.zig`/`App.showAvailableUpdate`): when one exists Install is enabled and triggers the real download/verify/extract/upgrade pipeline; when none exists (the real, currently-live state — see Install progress) Install stays visibly disabled with an honest reason, replacing the old permanent "not implemented" label. `UpdateOfferDialog.zig`'s `buttonsFor(installable)` is unit-tested for both states, and a real, live `feed-check` run against the actual `scgopi/GraphCode` releases API (`Tools/windows/Tests/WindowsUpdateInstall.Live.Tests.ps1`) confirms the disabled path is genuine, not simulated, for the current release. What is **not** live-witnessed: clicking Install through a real enabled offer end-to-end in the running UI, because no Windows release asset currently exists to enable it against — that gap is honestly disclosed rather than faked | Partial | -| Install progress | In-window progress indicator | `WindowsUpdateInstall.zig` now implements the full download → SHA-256 checksum verify → extract → `GraphCode-Setup.ps1 -Command Upgrade` pipeline, reusing the existing packaging verification/rollback logic rather than a second copy, and reports phase/fraction progress through a `ProgressFn` callback. `UpdateInstallDialog.zig` renders that progress in a native window (download %, verifying, extracting, installing) plus a failure state; both are unit-tested (14 tests total across the two files, part of the 42-file/273-test hermetic `WindowsShell.Tests.ps1` suite). Real, non-simulated live evidence (`WindowsUpdateInstall.Live.Tests.ps1`, invoked directly — not part of the hermetic suite since it makes real network calls): a real HTTPS download of a real multi-megabyte GitHub release asset streams genuine progress (100+ real progress reports, 0→100%) and its SHA-256 is verified against the asset's real published digest before extraction is attempted; a deliberately wrong digest is rejected with `ChecksumMismatch` strictly before extraction, proving the checksum gate is not vacuous. Honestly out of scope and **not** provable right now: extracting and upgrading a real Windows ZIP asset end-to-end, because the last recorded and just-reconfirmed-live asset check found only macOS DMGs published — there is no real Windows asset to extract. Also unproven: whether `Move-InstallDirectory`'s rename succeeds while `graphcode-windows.exe` is the actual running, self-updating process (the existing `Packaging.RealLifecycle.Tests.ps1` proves the *opposite* guarantee — that a locked file blocks and rolls back — not this scenario) | Partial | -| Relaunch prompt | Relaunch Now/Later and session continuity explanation | `UpdateInstallDialog.zig` presents Relaunch Now / Later with session-continuity copy after a successful install, and `App.runInstall`/`relaunchAfterUpdate` wire the outcome: Relaunch Now respawns the executable (same `CreateProcessW` pattern as `launchWorkspace`) and then quits the current process; Later leaves the update staged and shows an honest status message. Pure logic (`relaunch_message`, outcome handling) is unit-tested; the real Win32 window/thread code compiles and runs but is not live-driven by UI automation in this PR. This row cannot move past `Partial` genuinely: there is no real Windows release asset to drive a real install to completion today, so the actual relaunch — and whether zmx-backed terminal sessions survive an Upgrade-triggered restart specifically, as opposed to the differently-scoped scenario `DaemonHandoff.Live.Tests.ps1` already covers — remains unproven live. Faking that would violate this fleet's evidence policy, so the row is left honestly `Partial` rather than asserted `Validated` | Partial | -| Install failure | Download in Browser/Cancel with reason | The Windows flow exposes Release Notes/Later and a disabled Install action explaining that in-app installation is not implemented, then hands off through the verified browser release page when notes are requested. This remains Partial until download/install failure handling exists | Partial | -| Loop rename | Title field, Return submits, explanatory text | The dedicated single-title modal explains where the title appears, prepopulates the current value, trims and validates submission, and re-resolves the stable loop ID after the modal. The populated UIA gate edits the native field and verifies Return submits and closes the dialog | Validated | -| Loop delete | Named loop and full consequence message | Names the loop, explains graph-connection removal, and defaults to cancellation | Validated | -| Chat rename/delete | Dedicated prompts | Dedicated single-title rename modal and named fail-closed deletion warning are wired from card actions and shortcuts | Validated | -| Project delete loops | Dedicated confirmation | Sidebar project menus expose Delete All Loops through one fail-closed implementation with graph and filesystem consequence copy, safe cancellation default, and the dedicated daemon command. The live UIA gate verifies the native confirmation and cancellation path | Validated | -| Project remove/trash | Distinct reversible remove and filesystem Trash choices | Remove from GraphCode is distinct, confirmed, and explicitly preserves files. Local project menus now add a separate confirmed Move Folder to Recycle Bin action using the Windows undo-capable shell operation; remote projects retain only the GraphCode removal action | Validated | - -## Accessibility, input, and visual behavior - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| UI Automation tree | Names, roles, selection, invoke/toggle, focus, live status for every visible surface | The synchronized live C++ provider exposes stable project rows, loop rows, project/overview/Quick Chat cards, worktree rows, destinations, canvas primary action, zoom controls, policy actions, focus, selection-change events, and status. The live gate uses explicitly in-process deterministic fixtures to validate populated RawView/ControlView navigation, real bounds, observable Quick Chat/workspace invocation effects, tagged-command isolation, identity-preserving reorder/removal, events, concurrency, and teardown. The Workspace menu's New, Rename, and Delete lifecycle commands and dynamic workspace-switch rows are now part of that native provider tree instead of existing only in `Accessibility.zig`; the Zig contract was reduced to the native fixed table, and a pinned-Zig executable test now fails when any contract id lacks an exact native fixed-table id. The live gate now also traverses the Workspace menu in RawView and ControlView, checks its fixed lifecycle names and InvokePattern exposure, and verifies at least one `workspace-switch-*` row under the same parent. `TerminalSurface.zig` retains reported selection and cell metrics, but callback metadata does not prove applied terminal selection. Exact-pin Winghostty `f5abc059` source already creates an embedded child-HWND Text/Text2 provider and routes `WM_GETOBJECT` to it; the earlier missing-provider premise was inaccurate. GraphCode now feeds that provider owned UTF-8 from its unchanged rendered-cell grid with independent UTF-16 length/cursor offsets, rather than a rolling raw VT byte tail. Focused producer tests exercise overwritten text, chunk splits, reset/rollover, Unicode scalar representation, bounds, allocation/lifetime and injected publication failures without native APIs. This is fixed current-grid content, not transcript/scrollback or full Unicode terminal rendering. Render/text publication remains best-effort: failures are reported and the provider may retain its last successful, no-longer-current snapshot. Native terminal text results, applied selection, visible caret/geometry, range/HRESULT conformance, retained-generation behavior, atomicity, daemon-to-model UIA integration, and remaining dialogs still need separate evidence or implementation; keyboard discovery and HelpText residuals are not closed | Partial | -| Reproducible DPI/geometry regression coverage | Control metrics for GraphCode-owned chrome scale correctly and predictably across 100/125/150/200% DPI | `Tools/windows/visual-baseline.ps1` previously only checked that each DPI variant's `scale`/`viewport` were present and positive. It now reimplements `Dpi.zig`'s exact `scale()` rounding formula, self-checked against `Dpi.zig`'s own fixed-point unit-test cases, reads the real base pixel values straight out of `DesignTokens.zig` (not a copy baked into the manifest), and asserts the scaled geometry for `sidebar_width`, `tab_bar_height`, `pane_header_height`, and `loop_bar_height` at the real Windows per-monitor DPI values (96/120/144/192) is monotonic and matches the 96-DPI base exactly at 100%. Every `regionGeometry` entry is required to target a `deterministicScreenshotRegions` (GraphCode-owned) region, never a Winghostty-owned one, keeping third-party terminal pixels structurally out of scope. **This check performs static manifest/geometry metadata validation, not rendered-output comparison: it never launches the app, captures a window, or rasterizes a bitmap.** It re-derives expected numeric geometry from source-of-truth code and checks the manifest against that math, which is materially stronger than the prior presence-only checks and does catch real drift, but it is not a screenshot diff and should not be read as one; this repo/CI has no deterministic way to rasterize a live Win32 window. Manually re-verified that corrupting either a DPI value or a `DesignTokens.zig` constant makes the script fail | Validated | -| Keyboard discovery | Every shortcut represented by a menu item or visible hint where practical | Restored File, Loop, Terminal, View, and Help menus expose the primary project, graph, terminal, workspace, settings, update, and zoom commands with shortcut labels. The shared node popup no longer advertises Enter for Open Terminal or Ctrl+E for Edit Details: root Enter has no standalone open action, and Ctrl+E renames a selected loop or edits a selected edge. Two executable contracts failed on the old captions and pass on the corrected ones through the actual menu builder and `GetMenuStringW`, preserving command IDs, mappings, order, and enabled states across ordinary/resolved/composite/unwired targets. They inspect unattached menu handles without creating windows or displaying popups; mapper checks preserve toolbar Enter activation and existing Ctrl+E/F2 routes. The Windows README now distinguishes root, terminal, and dialog contexts and corrects toggle/chat-delete bindings. Pure mapper regressions cover Ctrl+Shift+OEM/legacy ASCII comma selecting product Settings, plain Ctrl+comma retaining Advanced Connection Settings, and unmodified/unrelated keys. An unattached native accelerator-table test checks the matching documented product binding and the unchanged original 15 bindings/order; this is not physical keyboard, focus, or dialog-opening evidence. Some context-only actions and canvas gestures still lack visible hints. No live keyboard or macOS runtime walkthrough was performed; remaining context-only/gesture discovery, other caption/routing discrepancies, and keyboard popup access still need evidence | Partial | -| IME/dead keys/layouts | Native composition in forms and terminal | Winghostty gate covers terminal IME; generic EDIT controls cover forms | Partial | -| Clipboard/selection | Terminal copy/paste and mouse selection | Terminal-context Ctrl+Shift+C copies the active surface's reported accessibility selection range through Winghostty into Windows `CF_UNICODETEXT`; Ctrl+Shift+V reads only `CF_UNICODETEXT`, converts UTF-16 to UTF-8, and calls Winghostty's paste validator and paste entry point with `allow_unsafe=0`. Clipboard conversion tests preserve Unicode, CRLF, LF, and empty text; app routing tests prove the shortcuts are terminal-context-only and preserve the existing global Ctrl+Shift+C Clone Repository route. Unsafe multiline/control-containing pastes are rejected with a status message rather than forced through, and no confirmation UI is provided. Clipboard Win32 calls, mouse-driven selection, actual rendered selection extraction, provider callback notifications, and end-to-end paste/copy on a live desktop were not exercised here; this row remains Partial | Partial | -| Per-monitor DPI | Layout and controls scale correctly across monitors | The process now declares real per-monitor-v2 DPI awareness at startup (`Win32.enablePerMonitorDpiAwareness()`, called before any window is created) instead of relying on system-DPI bitmap stretching; without this, Windows never delivers real per-monitor `WM_DPICHANGED` data to a DPI-unaware process. `App.zig` seeds the real startup DPI via `GetDpiForWindow` immediately after window creation (rather than assuming 96 DPI/100% until the first monitor move) and forwards every live `WM_DPICHANGED` to `TerminalWorkspace.Workspace.setDpi()`. Previously, `TerminalSurface.zig`'s `onDpiChanged` callback silently discarded the `dpi`/`scale` winghostty reported, and every terminal surface was created with `font_scale` hardcoded to `1.0`, so terminal text never actually rescaled on a DPI change or on a monitor with non-100% DPI at launch. `Workspace.setDpi()` now propagates the real runtime DPI to every live surface via winghostty's own `winghostty_surface_notify_dpi_changed` + `winghostty_surface_set_font_scale` (the two operations the provider actually exposes for this), and `surfaceOptions()` seeds new surfaces' `font_scale` from the workspace's last-known DPI instead of a fixed `1.0`. Deliberately does not also pre-scale `options.input.cell_width`/`cell_height` (kept at their 96-DPI logical baseline) so the DPI ratio is applied exactly once, through `font_scale`, avoiding double scaling. `onMetricsChanged`/`onAccessibilitySelection`, previously also fully discarded, now record the host's reported cell metrics and terminal text-selection range per surface instead of losing them. Verified with `zig build` (full app, pinned Zig 0.15.2 against the exact pinned Winghostty provider) and `zig test src/TerminalSurface.zig` (new `Dpi.fontScale` unit test plus all 13 pre-existing tests, 14/14). No live multi-monitor walkthrough was recorded (this environment has no interactive multi-DPI desktop), so this remains Partial pending that end-to-end evidence | Partial | -| Dark visual language | Dark canvas/cards/sheets and legible state hierarchy | Existing `DesignTokens`, repository dialogs and `NativeForms` supply the dark native palette and teaching tiles. [Actual production-renderer captures](visual-baseline/rendered-windows/README.md) now preserve canvas/sidebar, Product Settings and workspace clients at native 96 DPI. The real PNG comparator verifies exact opaque canvas/card/sidebar/dialog samples; setup uses a synthetic disconnected fixture, UIA invocation and native WM_COMMAND, not keyboard-menu proof. Settings retains native light buttons; current macOS Settings uses a native grouped form, so these were not speculatively darkened. Other sheets/state combinations, legibility on every surface and a compatible current macOS capture remain unverified | Partial | -| Font rendering quality | Legible, ClearType-quality text on every surface, matching macOS's default anti-aliased text | The shared `AppFont.zig` cache requests Segoe UI at `CLEARTYPE_QUALITY`, with per-DPI native controls and logical-size selection for buffered canvas painting. [Actual 96-DPI glyph samples](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) preserve card/sidebar/Settings-title pixels with 27/31/55 distinct colors; the native button sample has two. These counts and recorded system font-smoothing settings are observations, not legibility thresholds or actual font-face certification. Native control LOGFONT metadata, every-surface/multi-DPI review, terminal-text readability and matched current macOS evidence remain unavailable. No speculative font fix was made | Partial | -| Line/shape anti-aliasing | Smooth, anti-aliased lines/curves/rounded corners matching macOS's Core Graphics default | Existing `GdiplusAA` draws solid Beziers, rounded cards and metric segments with GDI fallbacks; axis-aligned grid lines and dashed/preview paths remain plain GDI. [Real app captures](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) now use production GDI+ startup, with both disabling automation hooks unset, rather than the ordinary UIA gate or a standalone drawing surrogate. The sampled selected-card corner has 31 colors and metric sparkline 30; original pixels and source/UIA-mapped regions are preserved. Other colors are not automatically antialias coverage, and there is no invented quality threshold. All edge styles/DPI states and equality with macOS Core Graphics remain unproven | Partial | - -| Color palette fidelity | Windows tones/gradients match macOS `Theme.swift` 1:1 (not just "generically dark") | [Immutable before/after app captures and replay](visual-baseline/rendered-windows/README.md) prove a bounded COLORREF bug: the same 96-DPI workspace focus-strip ROI [500,130,64,2] changed from 128/128 orange RGB255,132,10 pixels to 128/128 blue RGB10,132,255 after only `pane_focus_tint` changed from `0x000A84FF` to `0x00FF840A`. The production comparator independently derives current Theme.paneFocusTint, decodes Windows BGR and checks actual pixels; old-orange/channel-swap/delta-1 controls fail. Canvas/grid/card/sidebar/dialog/selected-tab opaque samples also match exact source colors. Existing two-stop `GdiGradient` chrome is measured, not equated with macOS three-stop/material compositing. All tones/states and a compatible current macOS rendered comparison remain unverified; historical manifest and separate two-token currentThemeContract are unchanged | Partial | - -**Known out-of-scope CI gap surfaced while validating the row above (PR #398):** the live `windows-shell` UIA gate's "New Loop" assertion invoked via the sidebar's `project-new-loop-*` element (`Tools/windows/uia-live-gate.ps1`, "project-row New Loop did not open the node form") fails intermittently/deterministically across unrelated branches (reproduced on `coneilen-microsoft-canvas-workspace-detail-parity` and `coneilen-microsoft-updates-dialogs-quick-chats-parity` as well, with no relation to dialog rendering code). This is pre-existing test-infrastructure flakiness, not a visual-polish regression; it is out of this pass's scope and is flagged here for a dedicated follow-up. - -**Known shared-environment gate instability surfaced while validating the Window toolbar/Update command rows above:** with the local shell toolchain unblocked (PR #433), multiple parity sessions now build and run `graphcode-windows.exe`/`zmx.exe` concurrently on the same interactive desktop. The pre-existing worktree reorder/removal focus-retention stress block in `Tools/windows/uia-live-gate.ps1` (`Retain-FocusWithRetry`, its `Start-Job` concurrent-UIA-read stress, and the plain `Get-DirectChildren` tree walks around it) repeatedly hit raw, uncaught COM exceptions (`GetFirstChild`/`GetNextSibling` "Could not open the process token"/"Unrecognized error.") at different, unrelated call sites across many local runs, and a separate run was independently derailed by another desktop application (Chrome) stealing the foreground window during a modal-dialog wait. None of this reproduced from this branch's own changes — a minimal, standalone re-run that skips straight to the Update command assertions using the same shell process, native menu, and gate helpers passed cleanly and repeatably. This matches flakiness independently reported by sibling parity sessions and is a pre-existing, shared test-infrastructure limitation, not a product regression; it blocked getting one single uninterrupted top-to-bottom `uia-live-gate.ps1` run this session and is flagged here for follow-up (likely hardening `Get-DirectChildren`/`Retain-FocusWithRetry` against concurrent-desktop contention). - -## Audit conclusion - -The Windows branch has substantial protocol, lifecycle, persistence, terminal, graph -mutation, tray, and packaging behavior, but it does **not** currently have complete UI -or screen parity. The previous parity statement conflated backend reachability with -user-visible parity. The largest corrective work is: - -1. Restore and complete the application menu and navigation state model. -2. Implement the sidebar, global graph, Quick Chats canvas, project canvas chrome, and - loop workspace as distinct application-owned surfaces. -3. Replace raw protocol forms with structured node, edge, settings, repository, and - worktree screens. -4. Implement the missing update, project-management, rename/delete, empty, and - connection-info states. -5. Expand UI Automation and live walkthrough coverage to every row above before any - complete-parity claim. +# Windows UI parity ledger + +This is a source-derived completion ledger, not a requirements sketch. A row is +`Validated` only when the Windows implementation exposes the same user-visible +information and actions as macOS and has runtime evidence. Platform-native chrome may +differ, but hiding a feature behind an undocumented shortcut or replacing a structured +screen with raw protocol fields is not parity. + +Statuses: + +- `Validated`: source mapping, automated coverage, and live walkthrough agree. +- `Partial`: some behavior exists, but visible controls, state, or interaction is absent + or materially different. +- `Missing`: no equivalent reachable Windows surface. +- `Blocked`: requires a deliberate platform decision or unavailable dependency. +- `Divergent`: Windows exposes a different product concept in the place where the macOS + surface belongs; it must be separated or redesigned before parity. + +## Application shell and navigation + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Main split view | Persistent sidebar; detail switches among welcome, global graph, project canvas, Quick Chats canvas, and loop workspace | Explicit destinations exist; the earlier stub walkthrough covered project → overview → workspace → Show in Graph with sidebar retention. The shared header now follows destination identity, distinguishes a Quick Chat workspace from a previously selected project, and keeps detail-panel toggling inside the loop workspace. Production-state and layout tests cover the destinations, narrow widths, and sidebar/panel independence. The current live attempt stopped at foreground acquisition before UIA root access: no new live UIA SetFocus, F6, Jump, pixels, or sidebar-effect proof was obtained, and provider-backed workspace/panel behavior remains unverified | Partial | +| Window toolbar | Needs-you chip, worktree notice, jump field, contextual loop-panel toggle | Render, hit-test, UIA exposure, and focus order share header visibility/layout. Needs-you opens the selected attention target; cached local worktree notices honor the owning project and count/size threshold. Jump opens the existing palette even with no projects. The detail toggle is limited to loop workspaces with supported edge/metric content; workspace/project identity is no longer overpainted. F6 entry/exit, scoped traversal/activation, focus rendering, and marshaled UIA button focus are implemented, while ordinary Tab/Shift+Tab/Ctrl+Tab contracts remain. The header paints inside the buffered logical pass; literal 96/144/192-DPI bounds and hidden native client tests cover logical layout/input and exactly-once physical UIA conversion without rescaling terminal tabs. Production-helper tests, a hidden native Tab-to-command test, and a hidden empty-palette create/cancel test pass. Live focus/event-deadlock, keyboard activation, rendered pixels, sidebar effects, and provider-backed panel proof are still absent because foreground acquisition failed. Cross-project worktree-notice discovery/aggregation, summary/board/mailroom panel content, and macOS visual parity remain separate residuals; private live tooling is not CI coverage | Partial | +| Jump palette | Search field, ranked cross-project results, type/state/project context, mouse and keyboard selection | Ctrl+P and Ctrl+J open a native modal palette with live exact-ID, exact-title, title-prefix, and substring ranking across projects. Results visibly include project, loop type, and state; Up/Down, Return, Escape, and mouse double-click are supported. The deterministic UIA gate verifies a visible search field, contextual cross-project results, and keyboard navigation changing the selected loop. | Validated | +| File/Loop/Terminal menus | Discoverable project, worktree, navigation, workspace, update, settings, and help commands with state-aware enablement | Startup menu replacement and UTF-16 corruption are fixed and the five readable runtime groups were probed. Reclaim/Reveal require a selected row (`worktreeRowSelected()` retains dedicated unit coverage); Save requires the Worktrees dialog open. Loop commands now gray out when they have no actionable target: Jump requires a loaded loop anywhere, Next/Previous require at least two loops or one unselected loop in the active graph, Create Edge requires two active-graph loops, and Stop requires a selected loop. These distinct thresholds follow their different production handlers and are intentional. Terminal Next/Previous Tab and Focus Next/Previous Pane require multiple tabs or panes in the selected tab. A hidden-window test calls `MainWindow.updateMenu` and verifies the real native HMENU `MF_GRAYED` bits in both unavailable and available states; this is unit/runtime Win32-menu evidence, not a live application walkthrough. The Worktrees-dialog UIA gate evidence remains outstanding: obtaining it requires launching the shell in a foreground desktop session, and no application executable is built in this worktree; provider builds/provisioning and foreground-gated UIA automation were not performed. Broader project-management parity remains incomplete | Partial | +| Workspace lifecycle | List/switch, create, rename, delete with fail-closed confirmation, and keyboard/UIA reachability | Windows discovers `Default` plus `.graphcode-*` siblings and exposes New/Rename/Delete and Ctrl+Alt paging. Manage now has an owned row model/native list, Default/creation-time/name-tie order, identity-deduplicated current-outside-home inclusion, full-path detail, current/default/open/uncertain-window refusals, and captured-identity Open/New/Rename handoff after modal teardown. An App-owned single joinable reader supplies bounded **saved top-level** graph-header counts, not live/descendant totals; current live content, missing/unreadable/invalid/duplicate/limited/unsupported-location data remain explicitly unavailable, not partial zero. Fixed local/non-reparse reads use existing project JSON and the documented mailroom-array exception, no daemon/backend or window activation. Done/Escape cancels; pending actions wait for reader cancellation/join while the modal pumps, and shutdown drains before allocator teardown (a stalled local disk can delay shutdown). Manager Delete is disabled pending recoverable deletion/teardown; existing menu deletion and ordinary menu ordering/Open/New semantics are unchanged. Next/Previous now reread the manager-ordered list, include validated current outside home, filter identified running windows, wrap both ways, recheck the target, and use a restore-only API with no launcher. Closed/disappeared targets never cold-open; any unidentified flag or lookup/restore failure refuses with status. Existing SID/session/class/published-metadata checks are preserved, not executable-path attestation. Menu capability counts known rows plus implicit current, without periodic window polling. Injected production-helper RED/GREEN, allocation-failure/owned-refresh, ordering/dedup/offset/race/refusal tests and pure menu-capability tests execute in the existing App/MainWindow roots; full native roots compile/link without execution and the ReleaseSafe app builds without launch. Manager coverage remains pure owned-data/explicit disposable fixtures, controlled memory-only joined threads, and filtered form/App data seams; no new shown manager/native-control/window-lookup/UIA/runtime proof is claimed. Existing lifecycle helper and never-shown native-control regressions cover accepted text lifetime, allocation cleanup, normalized naming/collisions, lexical identity, exact-window and one-launch routing, Default/current/open refusals, child-only support/daemon-pipe isolation, canonical plus legacy reservations, unidentified-window refusal, fail-closed Settings rebinding, and disposable confirmed mutation preservation. Lexical identity is not junction equivalence. Shown-dialog accessibility/keyboard/layout, full multi-instance/save-reload behavior, reliable live totals, real running-cycle keyboard/window proof, and recoverable deletion with session/daemon teardown remain residuals. Shared-host menu failures still have an unproven cause; no full lifecycle parity or host-only diagnosis is claimed | Partial | +| Help menu | GraphCode Basics and normal About entry | The live Help menu exposes GraphCode Basics, which reopens onboarding, and About GraphCode, which opens a native versioned product dialog. The populated UIA gate verifies the dialog identity, version text, and close behavior | Validated | +| Update command | Check for Updates, disabled while checking/installing | Reachable from the Help menu's native menu bar, immediately reports "Checking for updates..." status and disables the command the instant a check is invoked, and — after fixing a real bug where the background check's completion never refreshed the menu bit because the only refresh path was gated on unrelated daemon-connectivity state — now reliably re-enables the command once the check settles. A dedicated live UIA gate assertion (reading the real native `HMENU` bit via `GetMenuState`, not the fragile UIA tree) and a Win32-window-backed unit test both confirm the disable/re-enable cycle. When the settled result is an available update, the gate additionally waits for the real offer, dismisses it via Later, verifies the modal disappears, and requires the shell owner to be enabled again before continuing; failure/no-update outcomes are required not to present an offer. In-app installation is now implemented as a separate scope (see Install progress/Relaunch prompt, Partial) | Validated | +| Tray lifecycle | Restore and exit without foreground daemon window | `TrayLive.Tests.ps1` exercises the physical icon, Open, close-to-hide, single-instance restore, Explorer recovery, popup contents, and visible Exit activation | Validated | +| Connection failure presentation | Explicit visible failure without replacing normal navigation | A persistent inline canvas banner now reports daemon unavailability while leaving sidebar and destination navigation intact; ingress errors take precedence when present. The live UIA gate forces the disconnected state and verifies the dedicated banner text and bounds | Validated | + +Running-cycle follow-up evidence: the final lookup actually used for activation +now refuses mixed identified/unidentified results before activating that same +target; ordinary Open keeps its target-first policy. The real accelerator +descriptor and eligible pretranslation/top-level fallback now wire Ctrl+Alt +paging to the same cycle action before terminal-child dispatch. Pure injected +final-lookup, descriptor/modifier, and message-data tests cover these paths and +preserve Ctrl-only tabs/F6/F10 routing. The corrected native menu regression is +compiled, not locally executed. This is not an atomic global-window snapshot or +native keyboard/accelerator/window proof; the workspace row remains Partial. + +## First-run and empty states + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Four-page onboarding | Visual terminology tour, Skip/Back/Continue/Get Started, backend selection, reopen, persisted seen state | Custom rounded Win32 onboarding; all pages exercised and persistence verified | Validated | +| No-project Welcome detail | Graph icon, pitch, explanatory copy, Open Folder action, inline error | Windows matches the centered Graph identity, pitch, explanatory copy, and single Open Folder action. Persistent project-ingress failures now also render as a bounded, wrapped inline canvas alert without replacing navigation; focused geometry coverage and the populated UIA gate verify the alert text and live bounds | Validated | +| Empty global graph | “Nothing running yet”, explanatory copy, Open Folder action, New Loop action | The dedicated overview empty state exposes both bounded Open Folder and New Loop actions; New Loop targets the daemon's `graphcode://global` project. The live UIA gate switches to an empty model, verifies both visible native controls, invokes New Loop, and observes the node form | Validated | +| Empty project canvas | Project-specific empty message and New Loop action | The dedicated “No loops yet” project state exposes its visible New Loop action. The live UIA gate installs an empty local project, invokes that exact command, and observes the project-scoped node form | Validated | +| Empty Quick Chats canvas | Explanation of Quick Chats and New Chat action | Live walkthrough verified the dedicated explanation and New Chat action with corrected non-overlapping layout | Validated | + +## Sidebar + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Graph row | Pinned global graph row with graph glyph | The sidebar now keeps a dedicated Graph destination visible even with no open project, labels it with a graph identity glyph, and routes it through the existing global-overview hit target and UIA destination | Validated | +| Quick Chats group | Selectable header, hover New Chat, disclosure, child rows | The native header remains selectable, reveals a hover-only New Chat action and disclosure, and exposes stable selectable child rows with Rename/Delete context actions. Focused menu tests cover stable chat identity; the live UIA gate invokes New Chat, collapses and restores children, and verifies child runtime identity survives. | Validated | +| Local/remote sections | Group labels, independent collapse, folder/network glyphs | LOCAL and REMOTE retain local/folder and remote/network identity and now toggle independently as native section actions. Focused layout coverage validates mixed ordering, and the live UIA gate collapses LOCAL while proving the REMOTE row and its stable automation identity remain present before restoring LOCAL. | Validated | +| Project rows | Selection, folder type, hover New Loop, disclosure | Open project rows retain selection and local/remote glyphs, reveal hover-only New Loop and disclosure controls, and collapse/restore their own loop tree without changing row identity. The live UIA gate invokes the project-row New Loop action into the real native node form and exercises project collapse/expand through stable UIA actions. | Validated | +| Nested loop tree | Edge-derived hierarchy, persisted expansion, drag reorder of roots | Handoff edges derive a cycle-safe root/descendant tree; nested rows disclose and collapse by stable node ID, expanded IDs persist atomically in the GraphCode support directory, and root rows now reorder through live pointer drag backed by the existing transactional `root` records. Focused Sidebar/Wire coverage and the deterministic UIA gate verify observable reorder plus emission of the new `sidebarNodesReordered` daemon command for server-side persistence parity. | Validated | +| Loop row presentation | Type stripe, title, elapsed time, state indicator | Rows now show a loop-type stripe, title, compact state indicator, and a compact elapsed value derived from `createdAt`. `Sidebar.elapsedText` now has focused boundary coverage for every unit rollover (seconds/minutes/hours/days) and its invalid-input guards (`created_at<=0`, `now<=created_at`), reverified via `zig test` and the full `WindowsShell.Tests.ps1` suite. This is still text painted directly onto the sidebar's `HDC` with no UIA identity of its own (the same limitation `Sidebar.updateBannerAt` had before this change), so a live assertion that reads the *rendered pixels* of the elapsed column was not captured this session; only the formatting logic and the row's overall live-rendering-without-crashing are executable evidence today | Partial | +| Project context menu | Move, worktrees, settings, Explorer, remote info, close, remove, delete loops/project | Project rows expose the lifecycle actions plus the Windows Recycle Bin path for local folders. Move is deliberately **not** an Explorer `/select` alias: `GraphContextMenu.moveProjectMenuItem()` appends "Move Project... (unavailable: daemon support required)" with `MF_GRAYED` while `Wire.supportsProjectRelocation()` is false, and the stale command path surfaces that explicit reason instead of opening Explorer. The live UIA gate now drives the real `TrackPopupMenu` popup (`MainWindow.wm_uia_context_menu` -> the same `GraphContextMenu.show()` the mouse path calls) and asserts the live menu's ordered items, that Move is command 5149 with that exact text and a disabled state, that a remote project's menu omits Move/Recycle Bin/Explorer entirely, and that the popup dismisses without wedging the shell. Note the observation channel: a popup menu appears in the UIA tree only as an empty Pane with no `MenuItem` children, so item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, but not UIA-tree evidence. | Validated | +| Loop context menu | Open, composite actions, rename, stop, delete | Sidebar and canvas loop rows share stable-ID Open, Rename, Stop, and Delete actions. Composite cards expose Open Group, Pilot Once, and Arm Schedule; the drilled-in canvas addresses mutations through the parent composite. The live UIA gate now opens and reads all three `wm_uia_context_menu` loop variants: target 3 (a plain wired loop) asserts Open/Rename/Stop/Delete are present and that every composite-only and unwired-only command is absent; target 6 (a composite, not-yet-piloted loop) asserts Open Group/Pilot Once/Arm Schedule are present with Arm Schedule rendered `MF_GRAYED` (not piloted), and that unwired-only commands are absent; target 7 (an unwired loop node, added live via the sidebar-reorder fixture mutation) asserts Wire it up/Mark as entry are present and composite-only commands are absent. As with the project context menu, item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, not UIA-tree evidence | Validated | +| Recent projects | Reachable from Add Folder menu | Recent and currently-open projects are now exposed as distinct sidebar rows, with unopened recents remaining under the LOCAL/REMOTE sections while open workspaces use separate `open-project` identities. The deterministic UIA gate verifies the split presentation and section behavior. The live gate now also walks the Recent Folders submenu reachable from Add Folder end-to-end: it sends a real `WM_INITMENUPOPUP` (the message `App.zig` uses to refresh `recent_folders` from the live model before a popup shows, so the read reflects the fixture's recent projects rather than pre-fixture placeholder state), reads the submenu's live items in fixture order with their stable `recent_folder_command_base`-derived command IDs, and invokes the second entry through the real `WM_COMMAND` route, confirming the shell routes it without crashing | Validated | +| Add Folder menu | Open Folder, Clone, Add Remote, recents | File now groups Open Folder, Clone Repository, Add Remote Repository, and Add Codespace under Add Folder and adds a dedicated Recent Folders submenu with its own command range; that submenu is now located rather than positionally indexed, so a new ingress entry can no longer silently retarget the rebuild. Focused MainWindow coverage validates the native menu structure and recent-folder command wiring. The live UIA gate now reads the Add Folder submenu directly off the live `HMENU` (`GetMenu`/`GetSubMenu`, not `TrackPopupMenu`, since this is the persistent menu bar) and asserts all four action labels plus the Recent Folders submenu; see the Recent projects row above for the live Recent Folders walkthrough this shares | Validated | +| Sidebar update banner | Available version and click-to-install action | A persistent footer banner now shows the retained offered version and reopens the native update offer when clicked. The live UIA gate now drives this through the real click path rather than the `GRAPHCODE_UIA_SHOW_UPDATE` bypass: since `Sidebar.updateBannerRect` has no UIA identity of its own, the gate computes the banner's live pixel geometry from the shell's real client height (matching the same viewport-bottom formula the paint code uses) and posts a genuine synthetic `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at that point, then verifies the real `GraphCode Update Available` dialog opens with the offered `GraphCode 9.9.9-test` version text, dismisses it via the live Later command, and confirms the shell survives. **In-app install remains deliberately Blocked** — the offer still hands installation off to the verified release page, and this row's evidence does not claim otherwise | Validated | +| Sidebar error footer | Persistent, scoped project-ingress error | Folder, clone, remote, and daemon-open failures now persist in a dedicated red sidebar footer independently of transient status. Successful project ingress clears it, wrapped layout preserves long messages, and the deterministic UIA gate verifies the dedicated footer identity plus multi-line bounds below the update offer. | Validated | +| Needs-you section | Navigable list with reason/project and Stop action | Up to four entries now expose selection, explicit reason copy, stable UIA identities, click/UIA navigation, and a dedicated Stop action. Focused routing coverage plus the deterministic UIA gate verify Stop targets the populated entry's real project path and loop ID. | Validated | +| Activity strip | Optional bottom strip, summary, attention-only filter, horizontally scrolling actionable events | Activity events retain project/node identity and timestamps, render timestamped cards, expose stable UIA rows plus scroll controls, and now keep a real horizontal viewport with an attention-only filter. Focused Sidebar coverage and the deterministic UIA gate verify scroll-state changes, attention-only filtering, and card navigation into the selected loop workspace. | Validated | + +## Graph overview and project canvas + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Cross-project global graph | Every open folder as a lane on one canvas | Existing lanes stack vertically, as on macOS. Production geometry tests now use four Alpha loops and two Beta loops, literal accumulated lane/card bounds, both distinct Open/Worktrees targets, transformed hit testing, a recent-only exclusion, and an empty ordinary-folder lane. `App.applyOverviewLaneAction` is the existing lane dispatch extracted without changing its ordering or selection semantics; the real overview callback consumes it before the unchanged loop/pan paths. Never-shown native tests exercise both projects' Open actions and emitted project/card selection and UIA bounds at 96/144/192 DPI. Interleaved graph refresh retains the other project's selected loop and updates stable card identity/geometry. Real scoped Worktrees actions start with no inspection, inspect two independent disposable Git roots, and require exact inspection/dialog paths and emitted primary rows, non-reclaimable primary safety, and preserved sentinel bytes; pre-seeded rows or an Invoke return cannot satisfy them. Deliberate wrong-identity/action/geometry controls are rejected before dispatch, not claimed as disabled-production-dispatcher or historical bug evidence. These are production-helper/model/UIA-data results, not shown rendering, OS input, COM invocation, mounted terminal/focus, or macOS runtime proof. The shared live overview segment still covers one project's two cards; simultaneous two-project capture and loop navigation remain unproved. macOS topology/START furniture, richer lane captions/chips, global-lane filtering, and remote/all-project worktree binding behavior also remain outside this slice | Partial | +| Folder lanes/bands | Project caption, worktree chip, open/close and folder actions | Overview lanes render distinct Open and Worktrees actions beside the project caption; click routing selects the project or opens scoped worktree inspection. Focused geometry/input coverage passes, and the local Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` posts real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` messages at the lane's Open and Worktrees hit-test rects against the live executable and verifies Open routes to the project canvas (synchronized cards render at a new position) and Worktrees opens the scoped inspection view, both confirmed passing across many consecutive live runs. This also uncovered and fixed two real accessibility bugs along the way: `App.zig`'s `.overview` mouse-click switch arm and its `.cycle_attention` action handler were both missing the `syncAccessibility()` call that keeps the live UIA tree in sync with what is rendered, so a lane's Open/Worktrees click previously had no observable effect through the accessibility tree even though the underlying surface did change | Validated | +| Notebook grid | Grid pans and zooms with canvas | `GraphCanvas.drawGrid` derives its cell size and offset from the exact same `CanvasState.zoom`/`pan_x`/`pan_y` fields consumed by `overviewCardBounds`, `overviewLaneBounds`, and the loop-card geometry, so the same focused pan/zoom coverage (`GraphCanvas.zig`: "canvas hit testing follows pan and zoom", "overview and quick chat geometry applies pan and zoom consistently") indirectly proves the grid cannot desynchronize from the content it underlays. The Windows shell toolchain blocker is resolved, but the grid itself is a 1px `0x00161815` GDI line pattern with no UIA surface of its own, and this session did not add a live pixel-scan assertion (the connector-handle and attention-rail blocks already show this pattern is feasible) to directly confirm grid line spacing changes with zoom in the running executable. Left Partial rather than claim live evidence that was not actually captured | Partial | +| Pan and anchored zoom | Pan, pointer-centered wheel/pinch zoom | Mouse pan and pointer-centered wheel zoom remain intact and unchanged, with the same focused regression coverage as before (`GraphCanvas.zig`: "canvas zoom keeps the graph point beneath the cursor stable", "canvas wheel zoom scales high-resolution trackpad deltas"). Native touchscreen pinch-zoom is now implemented and routed through the main window: `MainWindow.zig` registers only `GID_ZOOM` via `SetGestureConfig`, leaving every other gesture class (`GID_PAN`/`GID_ROTATE`/`GID_TWOFINGERTAP`/`GID_PRESSANDTAP`) at its existing OS default rather than explicitly blocking gestures this app has no opinion on, with a real registration test against a genuine `HWND` plus two independent negative controls — a malformed native `SetGestureConfig` call and a genuinely invalid `HWND` passed straight through the production `registerCanvasGestureConfig` helper itself — proving the helper's own `GetLastError()` capture path actually fires, not just the raw Win32 API. `App.zig`'s `WM_GESTURE` case decodes `GID_ZOOM` via a pure `CanvasInput.classifyGesture` decision table (including a distinct outcome for a gesture delivered as a single combined begin+end message, so it can never reuse a stale prior gesture's baseline), requires the active surface to actually render the graph canvas (not just the wheel-region rectangle, which the terminal workspace surface shares), and applies `GraphCanvas.zig`'s `beginPinchZoom`/`continuePinchZoom`/`endPinchZoom` against a per-gesture identity hash of surface+project so a same-region destination change mid-gesture (surface switch, or project switch while still graph-capable) resets the baseline instead of silently continuing to scale the wrong canvas; a failed `GetClientRect` is guarded and treated as unhandled rather than classified against an undefined rect, and the gesture handle is closed before any call that could re-enter the message loop. This is source-mapped, automated routing/unit evidence, not live hardware-input evidence: this session held no live UIA capture slot this pass, so pinch is proven by code mapping and a full deterministic test suite (non-compounding/clamp/zero-distance/lifecycle/context-mismatch/routing matrix, verified with genuine temporary-regression RED/GREEN passes against the production helpers, not GREEN-only), not an actual touchscreen or Precision Touchpad device. Per Microsoft's documented default, Precision Touchpad pinch on a classic Win32 window is emulated as synthetic Ctrl+`WM_MOUSEWHEEL`, not delivered as `WM_GESTURE`, so this implementation targets true touchscreen digitizers specifically; Precision Touchpad pinch behavior is not separately implemented or verified here. Touch-driven pan (`GID_PAN`) remains a genuine unimplemented gap: this app forwards it unhandled rather than half-handling it, but does not explicitly block it either. Left Partial: touchscreen pinch has real source and automated-test coverage but no live device evidence, and touch pan is still unimplemented | Partial | +| Zoom controls | Zoom out, actual size, zoom in, fit with shortcuts/help | Visible bottom-right controls provide zoom out, percentage/actual size, zoom in, and fit. A visible shortcut/help line accompanies the controls; Ctrl+-, Ctrl+0, Ctrl+=, and Ctrl+9 remain represented in the View menu. The Windows shell toolchain blocker is resolved and `Tools\windows\uia-live-gate.ps1` now runs against the live executable: it locates the `zoom-out`, `actual-size`, `zoom-in`, and `fit-canvas` UIA fragments, requires non-empty bounds, resolves each `InvokePattern`, and then actually invokes zoom-in, actual-size, zoom-out, and fit-canvas in sequence against the running shell, all of which completed without error across many consecutive live runs | Validated | +| New Loop canvas button | Visible top-right add action | A live-validated top-right New Loop button is now present on non-empty project canvases and remains centered in the empty state | Validated | +| Composite breadcrumb | Current group, project back action, loop count | Open Group swaps the project canvas to the authoritative nested graph, renders its cards and edges through the normal interactive canvas, and exposes a clickable `Project > Group` breadcrumb with loop count that restores and reselects the parent. Nested graph selection survives daemon refreshes, and the populated live UIA gate invokes Open Group, verifies both nested cards, and invokes the bounded Back breadcrumb to restore the parent canvas | Validated | +| Canvas attention rail | Count/oldest context and Review action | The rail exposes a clickable Review target and uses `createdAt` from the daemon model when present to show a true `oldest ` label alongside the oldest attention item title. Focused hit testing passes, and the Windows shell toolchain blocker is now resolved: the rail has no dedicated UIA element of its own (it is a full-width band GDI hit-test region), so `Tools\windows\uia-live-gate.ps1` posts a real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at the rail's exact screen rect against the live executable and verifies the click drives `App.zig`'s `.review_attention` -> `selectNextAttention()` routing by observing the resulting `SelectionItemPattern` selection actually move from one card to the NEEDS YOU card, passing across many consecutive live runs | Validated | +| Node positioning | Persisted positions and direct card movement where supported | Project cards can be dragged directly, with movement transformed correctly at non-default zoom, shared geometry/hit testing updated during the drag, and capture-loss cancellation restoring the prior position. Offsets are keyed to stable node identity, remapped across daemon reorder, and atomically persisted under the configured GraphCode support directory. Focused reorder/reload regressions and a real physical drag capture validate the complete flow | Validated | +| Connector handles | Hover handles and drag-to-connect | The right-edge connector tracks hover, paints a visible handle and plus affordance, and preserves the drag-to-connect path. Focused rendering/input coverage passes, and the Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` synthesizes real `WM_MOUSEMOVE` hover messages at the source card's outgoing connector position and confirms the live `0x7ACDFF` hover handle pixel actually appears on screen (`Test-ScreenPixelNear`), then drives a full `WM_LBUTTONDOWN`/`WM_MOUSEMOVE`/`WM_LBUTTONUP` drag from that connector onto a second card and confirms the resulting native "Create or edit edge" dialog locks its From/To fields to the exact dragged source and dropped target loop IDs, all passing across many consecutive live runs | Validated | +| Loop card identity | Loop-type stripe, title, state pill, entry/cycle role | Project and overview cards now use loop-type-colored stripes while retaining lifecycle state text, START, UNWIRED, and attention labels. Focused color regression coverage passes; live evidence remains blocked | Partial | +| Loop card live detail | Goal/prompt/check line, progress, metric change, elapsed/backend/model/worktree metadata | Cards prioritize goal, trigger, or check detail, retain current activity, and add metric pass/change text, elapsed age, backend identity, token usage, model tier, and worktree/branch metadata from the same decoded daemon fields used by the workspace loop bar. Focused card metadata tests (`GraphCanvas.zig`: "loop card metadata includes backend elapsed and token usage when reported") pass, and the `windows-shell` CI job's live UIA gate exercises the populated card fixture end to end (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, passing, merged as PR #399) | Validated | +| Loop card attention | Reason-aware amber presentation and primary action | NEEDS YOU cards render a card-level reason-specific primary button: `Reply` for reported awaiting-input sessions and `Inspect` for other attention reasons, both routed through the normal loop-opening path. Focused action-label tests (`GraphCanvas.zig`: "attention cards expose reason-specific primary actions") pass, and the live UIA gate's `attention-action-*` assertion for the deterministic awaiting-input card passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | +| Unwired card recovery | Explanation, Wire it up, Mark as entry | Cards with no inbound or outbound edge now show an explicit UNWIRED warning and recovery explanation. Their native context menu exposes Wire it up, which enters the existing drag-to-connect flow, and Mark as entry, which changes the card to START for the session. Focused role/action tests plus live menu and post-action captures validate the flow | Validated | +| Worktree reclaim offer | Reclaim and Keep actions on resolved card | Safe resolved cards with a matching landed, clean, pushed worktree expose separate Reclaim and Keep targets. Reclaim revalidates safety and Keep suppresses the offer for the session. Canvas Reclaim/Keep descendants are now emitted through the UIA provider and invoke the same fail-closed paths. Focused geometry/safety coverage passes, and `Tools\windows\uia-live-gate.ps1` now asserts the live Reclaim/Keep descendants under the Graph fragment (name, non-empty bounds, InvokePattern, and correct RawView/ControlView sibling linkage) via the `windows-shell` CI job (PR #385, run 35422364203, passing) | Validated | +| Composite card actions | Open Group, Pilot Once, Arm Schedule | Canvas and sidebar composite menus expose all three actions. Open Group is live-validated; nested creates, edits, deletes, edge changes, pilot, and arm commands use the daemon's authoritative `subGraphCommand` envelope; and Arm Schedule is disabled unless the decoded pilot state is exactly `piloted` | Validated | +| Edge presentation | Kind style, fired state, cycle label | Windows retains typed optional cycle guards and derives fired state from authoritative `fireCount`, with missing/null legacy fallback. Raw snapshot JSON reaches the production label formatter for maximum/until/flat-pass and empty-guard summaries; signed/null/malformed inputs, Unicode/long text, owned copies, refresh/composite transitions, and allocation-failure cleanup have executable coverage. Kind styling, selected emphasis, fixed 148-by-20 label bounds, ellipsis, and collision placement are retained. This is exact-string and static-geometry evidence only: full visible wording, the macOS context-menu summary, rendered pixels, and live label evidence remain unverified | Partial | +| Edge creation sheet | Kind/condition/transform/cycle controls with conditional validation | The guided native form provides endpoint selectors, conditional fields, validation, keyboard traversal, scrolling, and recap. Menu and connector creation now share the full draft sender, retaining condition, payload transform, all optional cycle guards, and spawn target. Project/composite identity and endpoint IDs/titles are owned across the modal; stale model/selection/client subgraph addressing and missing or changed endpoint choices are explicitly rejected without retargeting. The update subscription can still observe another cached project or refresh independently: outgoing creation explicitly addresses the captured project. Registered pure production-path tests inspect the real client's outgoing queue, including cached-project selection, Unicode/quotes, callback-induced graph changes, cancellation, and allocation failures. The reduced connector sender fails the same accepted-draft fixture that the full sender passes. Earlier live evidence only opened the form and checked endpoints/recap before cancellation; this fix has no new live modal/keyboard, daemon acceptance, persistence, or cross-platform parity evidence | Partial | +| Custody child creation | New Child Node on unresolved parents, inherited editable backend, daemon-owned custody | Project-canvas/sidebar node menus consume the tested unresolved-only item plan (5119). Owned popup target/settings/child-only exact-project worktree snapshots feed the existing guarded node form and template/attachment continuation. Original popup context is checked before normal initial selection; final guards never reselect, reject project/composite/address drift or deleted/resolved/type/backend-changed parents, and allow rename/reorder/unresolved progress. Tests exercise the production initializer/transfer boundary and real data-only client queue, including cached B while observing A, root-versus-composite same IDs, UUID/null wire fields, cancellation and allocation failures. `createdBy` produces one create command: GraphStore owns the fired handoff/report-back memo and normal startup. No new protocol or parent/session mutation. Native display/action results, overview right-click, daemon acceptance/persistence, and inherited downstream-send failure handling remain unverified. The merged ordinary-creation path now retains the owned worktree snapshots whose evidence is recorded in Node creation sheet, while custody retains its pre-popup owned snapshot; template backend settings remain a separate residual | Partial | +| Edge editing | Preserve identity/configuration and refuse stale edits | Windows uses an owned modal snapshot and one checked `updateEdge`, retaining endpoints, current fireCount, transform/spawn settings and unchanged optional legacy guards. Root/direct-composite scope and current cache are rechecked without treating observation subscription as authorization. Existing serialized daemon commands, blocking, parent roll-up and root publication remain authoritative; deeper new edit wrappers are refused. Production-helper regressions exercise actual Wire JSON/unstarted queues, foreign scopes, refresh, cancellation and allocation cleanup. Edit-only live/submit capture reads complete Unicode text and blocks acceptance on per-field read/allocation errors; injected-reader and initializer tests show no controls. In-memory Swift store tests cover CAS, kind/blocking rules, root publication, preview refusal and serialized legacy-child writeback followed by an edit that retains runtime progress. This is not native editing, daemon acceptance/persistence or macOS runtime evidence; macOS sources expose creation-time configuration and delete, not an existing edit UI | Partial | +| Node creation sheet | Loop-type teaching tiles, conditional fields, backend/model/branch pickers, recap, validation reason | A guided native form provides loop-type/backend/model choices, type-specific fields, explanatory copy, accessible checkboxes, inline validation, keyboard traversal, scrolling, and a live-updating recap. The native Branch picker offers inspection choices plus “This folder”; existing tests cover its empty state and selected binding, and earlier gate evidence covers the recap, Branch picker, and distinct Attach control. Staging now reserves a uniquely named file exclusively: the production append/removal regression first demonstrated A/B/C → remove B → add D overwriting C's bytes, then passed with C's original path and bytes preserved. Offline in-file coverage also checks template-restored/legacy files, preexisting destination collisions, partial-write rollback, allocation failures without orphan copies, invalid inputs, exactly eight files and the ninth rejection, and the exact 10 MiB/+1-byte boundary. Existing attachment DTO/wire tests remain passing; these are no-window helper/filesystem results, not OS input or daemon persistence evidence. Creation choices now own an exact-project snapshot of inspected path/branch strings across cache/model replacement; baseline aliasing and foreign-cache regressions fail before the fix and pass afterward. Noninteractive production-projection tests cover snapshot lifetime, projection without a loaded graph, and allocation-failure cleanup. Separate pure initializer/builder-to-Wire tests verify exact non-default repository/id/path/branch fields, initial/current/template-restored selection, and “This folder” clearing with available choices; legacy empty-choice hidden binding remains unchanged. This does not establish actual native non-default selection, a Git-inspection-to-creation flow, or daemon save/reload. **Still partial:** actual inspected nondefault binding and project-scoped ownership across pumped modals remain unproved; New branch and remote/global branch visibility differ from macOS; authentic OS picker acceptance/cancellation, clipboard paste/drop, and native-input → request → daemon save/reload evidence remain outstanding. Per-file removed-file reclamation and legacy unguarded cleanup remain separate residuals. Creation now retains the original project/composite context and validates after modal teardown, before attachment transfer, across normal, template-load-error, empty-library, and template-loop submissions without switching selection or client scope. Bounded offline production-boundary RED/GREEN and owned temporary-file tests cover stale-scope refusal, loaded-project closure, same-path refresh/promotion, path-only starts, valid empty composites, template continuation, allocation unwinding, and checked cleanup failures. Guarded staging reserves its attachment-directory leaf exclusively; cancellation/refusal and zero-reference acceptance abandon only that owned leaf, while nonzero accepted attachment bytes survive. Zero-reference drafts preserve existing empty-ID/wire behavior. These are helper/model/unstarted-client-queue proofs, not live modal, OS picker, or daemon-persistence evidence; path-only external disappearance and downstream send failure remain limitations | Partial | +| Node update/rename | Dedicated rename prompt and safe typed updates | Rename retains its dedicated safe prompt. Edit Details uses `NativeForms.update` and `sendUpdateNodeForm`; its project/node identity is now owned across the modal message loop and re-resolved afterward, and changed strings are compared against the form's owned initial snapshot rather than potentially freed graph strings. Original numeric scalars and clear-versus-unchanged semantics are preserved. Production-helper tests cover source mutation, unchanged/changed typed fields, cancellation, clearing, and allocation failures. The lifetime test rejects the original borrowed baseline. Live attempts stopped at background-menu activation before reaching editor or rename actions, so app-level dispatch, cancellation, and returned model results remain unverified in this work | Partial | +| Delete confirmations | Named object, consequences, safe default | Loop deletion names the loop and explains graph-connection removal. Edge deletion now names both endpoint loops and the connection kind, explains that the loops remain, re-resolves the stable edge after confirmation, and defaults to cancellation | Validated | +| Canvas context menu | Folder actions on background; complete node/edge actions | Project backgrounds now expose existing Worktrees, Project Settings, and Explorer routes with captured project context; unavailable non-filesystem actions are disabled and global scope omits them. Create Edge remains present but disabled with fewer than two loops. Resolved node menus omit Stop, matching macOS; composite/template/unwired actions and the absence of Message/Memo are preserved. Tests inspect the same native HMENU builder used by `TrackPopupMenu`, including an exact disabled-item assertion that fails when Create Edge is removed. The existing gate already inspects plain/composite/unwired popup contents; its succeeded-node Stop assertion now requires absence. Focused live attempts read the real background popup but did not achieve an action/result, and a minimal native control failed foreground acquisition before opening its menu; the cause remains unproven. Live node/edge/background action results, New Child live proof, and import/export remain deferred; custody child creation and sketch promotion retain their separate Partial evidence rows | Partial | +| Sketch promotion | Promote a sketch to Goal, Turn, or Timed without replacing its identity or session | Canvas and sidebar context menus use one production submenu plan and focused native forms. Owned popup/form snapshots reject stale scope, selection, type, and deletion; initial selection of a different cached project remains supported independently of the observation subscription. Filtered pure tests exercise the actual App selection adapter, typed native-form builder, and unstarted DaemonClient outbound queue, including composite addressing and allocation failures. Three emitted fixtures decode as existing Swift `promoteNode` variants, and the full Windows application builds ReleaseSafe. A native Win32 test attaches the production node popup to a hidden HWND and checks the Goal/Turn/Timed submenu's presence, IDs, labels, and enabled states through the actual HMENU; it does not show or select the popup. Local command construction/queueing and this native menu state are proven, but native keyboard interaction, UIA, app launch, and real-daemon acceptance/persistence remain unexercised | Partial | + +## Quick Chats + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Quick Chats canvas | Band, cards, pan/zoom, add button, empty state | The real executable was exercised with two deterministic chats. The band, transformed cards, top-right New Chat action, bottom-right zoom controls, and 100% → 110% zoom transition were captured live. Context actions are wired, and the populated live UIA gate validates named, bounded, invokable Quick Chat cards plus the populated-canvas New Chat action | Validated | +| Quick Chat cards | Title, chat identity, optional backend badge, open/rename/delete menu | A populated live fixture verified title/default-chat identity/optional backend rendering and direct opening (`openQuickChat` was observed by the protocol stub). Cards now expose Open Chat, Rename, and Delete Chat context actions | Validated | +| Create chat | Visible New Chat controls | Empty and populated Quick Chats canvases expose the New Chat action in the macOS placements. The live UIA gate invokes the populated canvas action and the sidebar/header action; the existing empty-state walkthrough invokes the centered empty-state action | Validated | +| Rename chat | Single title prompt from row/card | Uses a dedicated single-title modal from the card/keyboard action, trims input, and rejects empty titles | Validated | +| Delete chat | Named confirmation explaining session/scrollback deletion | Uses a named warning that explains terminal-session and scrollback removal, defaults to cancellation, and only sends deletion after confirmation | Validated | +| Chat workspace | Opens a persistent terminal workspace | Opening a Quick Chat now exposes a bounded, selected `Quick Chat terminal workspace` UIA surface while the existing terminal panel remains persistent. The full native UIA walkthrough verifies the card invocation and workspace transition without duplicating loop-workspace implementation | Validated | + +## Loop terminal workspace + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| 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. Production is still 120x40 without PTY resize negotiation; 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 | +| 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 | +| Mounted background tabs | Switching preserves live terminal surfaces | Workspace implementation tests still cover the topology, and the live UIA gate now creates a second mounted tab, switches between the original and background tab, and asserts both tab automation identities survive the round trip without shell exit/reconnection | Partial | +| Right loop panel | Minimap, upstream/downstream, fired conditions, metric sparkline, branch/start/usage footer | The full workspace right rail includes the selected-loop map, upstream/downstream cards, fired-edge coloring, edge conditions, branch/worktree identity, metric/goal detail, model tier, a metric-history sparkline from decoded samples, start-time/usage/backend footer text, a collapse/expand control that no longer reserves rail width while hidden, and dedicated UIA children for sparkline/start/usage/toggle (`workspace-detail-sparkline-*`, start, usage, and toggle automation IDs). The live UIA gate asserts those children and toggles collapse/expand, and that gate passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | +| Show in Graph | Visible loop-bar and menu action | The live gate invokes the real `workspace-show-graph-*` loop-bar action through `InvokePattern`, requires the exact selected `UIA loop A` `canvas-card-*` identity and selected state with strict workspace-chrome absence, then returns through the exact supported `loop-row-*` identity and requires the same project toolbar, selected-loop bar, and Show in Graph identities. A focused isolated pair at `.graphcode-evidence/show-in-graph-20260924-151455` used source gate SHA-256 `AE4A2C71...`, focused harness `78E6821E...`, and shell `8E0AA9E7...`: GREEN invoked the action and completed the exact round trip (stdout `E94DCA9D...`); RED disabled only that invocation and failed the unchanged chrome-absence assertion (stderr `3198914D...`). This confirms the route without a product fix; graph cards remain intentionally non-invokable. The focused early return did not exercise later downstream-provider rebinding. A full integrated gate pass and live native Loop-menu invocation remain unverified. | Partial | + +**Live-gate infrastructure fix (this session):** the `windows-shell` CI job's `uia-live-gate.ps1` step was, until now, never actually exercising any of the workspace chrome above: `App.init()` unconditionally skipped `Workspace.init()` under `GRAPHCODE_UIA_GATE=1` regardless of whether a real `zmx` executable was supplied (a pre-existing guard predating this workstream), so every "Partial" row above had never been run against a real workspace at all. Fixed in `App.zig` to build the real workspace under the gate whenever `GRAPHCODE_ZMX` is present. That surfaced a second, genuine regression: the newly-real terminal surface competed for native Win32 keyboard focus with the rest of the UI after navigating away from the workspace (`App.openGlobalOverview()` and friends). Root-caused to `Workspace.poll()` (driven by the main window's 100ms `WM_TIMER`) unconditionally draining terminal output and calling `winghostty_surface_notify_accessibility_text()` regardless of workspace visibility, which kept re-asserting UI Automation focus on the terminal no matter what Win32-level focus fixes were made. Fixed by adding `Workspace.collapse()`/`Workspace.collapsed`, skipping `resize()`/`syncTopology()`'s pane refocus and terminal-output polling entirely while the workspace is hidden, plus a `WM_ACTIVATE` handler that reasserts the app's own focus policy after `DefWindowProc`'s default child-focus restoration on window reactivation. All of this is now covered by the passing `windows-shell` CI job (commits `a31813b`..`cba010f`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793). Note: the separate `windows-spikes`/`windows-hardening` jobs (`validate.ps1 -Task all`) run the identical gate script but under much heavier CI load and still intermittently hit this same assertion's 15-second retry window; this has been confirmed as pre-existing, cross-branch flakiness unrelated to this workstream (an unrelated sibling branch, `coneilen-microsoft-repository-settings-parity`, shows both a pass and an unrelated failure on the same job across consecutive runs), not a regression introduced here. + +## Repository ingress + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Open Folder | Native picker from Welcome and Add Folder menu | Welcome and File menu commands use the Windows folder-only File Open dialog with filesystem/path validation. The live UIA gate invokes the empty-state action, verifies the titled native picker, and cancels it safely | Validated | +| Clone Repository sheet | Repository, location picker, derived folder, branch, depth, progress, inline failure, cancel | Clone runs behind a progress-capable native operation sheet with live output, cancellation, terminal status, and shared dark painting. Windows-shell contracts pass. The UIA gate opens the real sheet, verifies URL/destination/branch/depth fields, submits it empty to verify the inline URL error, and cancels it | Validated | +| Add Remote Repository sheet | Server/user/port/path, explanation, validation progress, inline selectable error | SSH validation runs on a worker while a validation sheet remains open and Connect is unavailable until completion. It shares Clone’s dark themed native dialog class. Windows-shell contracts pass. The UIA gate opens the real “Add SSH Repository” sheet, verifies host/user/port/path fields, submits it empty to verify inline validation, and cancels it | Validated | +| Remote Connection info | Read-only selectable connection sheet | Remote project context menus expose a dedicated read-only connection-information dialog with the encoded remote project identity and management guidance. The live UIA gate opens the native sheet, verifies both pieces of content, and closes it | Validated | +| Add Codespace sheet | Authenticated codespace discovery, selection, workspace path, validation progress, empty/error/retry/cancel, opens the remote project | `Codespaces.zig` + `WindowsCodespaceDialog.zig` provide discovery, failure remediation, validation, submit gating, and `codespace://` project opening. Deterministic Codespaces/dialog/windows-shell coverage remains green. **Still partial:** the available token lacks `codespace` scope; no real discovery-success, selection, validated dial, or rendered-sheet UIA walkthrough was possible. Only the 403/remediation path was exercised against real `gh` | Partial | + +## Settings and worktrees + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Product Settings window | Backend, three permission pickers, model picker/auto toggle, activity, briefing, beta with explanatory copy | `WindowsProductSettings.zig` exposes native backend/model/Claude/Copilot/Codex selectors, routing/activity/briefing/beta controls, macOS-equivalent consequence copy, and Save/Cancel. Focused tests cover settings preservation and selector copy; `GRAPHCODE_UIA_GATE` mutation 15 opens the real window against an isolated settings file, verifies every required visible control/explanation, proves control-targeted Return saves while preserving unknown fields, and proves Escape cancels byte-for-byte | Validated | +| Infrastructure diagnostics | If retained, separate advanced surface | Daemon pipe/support-directory overrides are now explicitly labeled “Advanced Connection Settings...” while the normal Settings command opens product settings | Validated | +| Project Settings sheet | Resolve policy radio rows, safety explanation, size/count thresholds, immediate save | Valid threshold/radio edits persist immediately, while empty/partial threshold input preserves the last valid persisted value; Done remains dismiss-only. The shared dark native form is covered by focused and Windows-shell tests. The UIA gate uses the gate-only hook to open it and verifies policy, threshold-unit, and worktree content before cancelling | Validated | +| Worktree sweep sheet | Safe/look/in-use grouping, size summaries, default selections, reveal, inline destructive confirmation, recovery note | Sweep storage supports 256 rows, real directory sizes and aggregate totals, reveal, and a second destructive confirmation for dirty rows. Focused and Windows-shell coverage pass. The UIA gate opens the real sheet through the gate-only hook and verifies safe/look group rendering and Remove Selected. The fixture proves sheet rendering and interaction affordances, not Git worktree discovery | Validated | +| Worktree notice chip | Threshold-driven titlebar and lane notice | Explicit inspection installs compact value observations on each exact `GraphSummary` owner; replacing the full inspection, selecting another project, or reordering lanes does not transfer its counts. Overview chips share one presentation/geometry source for paint, hit testing, and path-keyed UIA data, reuse the existing Worktrees action, and show uninspected, incomplete-size, policy-unavailable, stale, and failed-refresh states without fabricated zero/freshness. Checked policy outcomes distinguish missing/legacy defaults from unreadable or malformed configuration. Failed policy writes also reconcile the captured owner's checked readback while preserving the original write error. The shared inclusive count-or-binary-GiB evaluator uses exact known bytes; partial measurements are approximate, and unknown policy or stale observations cannot claim a configured breach. Relevant binding/state/connection/mutation signals stale observations; nested comparisons reuse the existing decoder and scoped IDs under depth, work, and scratch-memory bounds, and comparison uncertainty remains explicitly stale. Title/position-only edits and supported reordering do not manufacture a refresh. Existing owner close/restore eviction removes observations. An exact-current-owner raw-inspection accessor keeps correct-owner rows visible below threshold or when stale/policy-unknown, while excluding foreign current-root rows without changing retained modal state. Pure production-helper/mapper tests cover boundaries, atomic replacement, ownership, nested changes/limits, injected partial-write reconciliation, error states, old action rectangles, path resolution, and literal 96/144/192-DPI UIA-data bounds; the sizing, rounding-label, raw-inspection, nested-binding, and failed-write regressions have RED/GREEN evidence. These are synthetic-data/helper results, not actual filesystem-failure, Git-inspection, native modal, keyboard/click, or COM/UIA walkthrough evidence. Windows still counts all inspection rows including primary/prunable and sums logical file bytes; macOS excludes the opened checkout and sums non-prunable allocated usage. **Still partial:** automatic discovery, global titlebar aggregation, remote/project-canvas band parity, and authentic multi-lane/boundary review-action proof remain outstanding | Partial | + +**Bounded worktree subprocess reliability (2026-09-26):** local job-bounded +`WorktreeGitProcess.Tests.ps1` calls the actual production helper. The baseline +with only `create_no_window` launch instrumentation reproduced upper/mixed-case +repository redirection, outside index/object writes, and an unread real-Git +stderr hang. Child-only environment scoping, concurrent bounded pipe collection, +and owned-child/result cleanup now cover those cases. Real selected/forced +removals and synthetic nonempty output from all three mutation paths exercise +allocation ownership; synthetic auth/config preservation does not use secrets. +The normal WindowsShell runner invokes this suite. No production wall-clock +deadline, OS wait/kill fault injection, live UI/provider walkthrough, or full +parity is claimed. The pre-existing direct `reclaim` command's self-removal +failure on the tested Windows Git remains unchanged; the synthetic direct-call +case proves ownership only. All existing Partial rows remain Partial. + +## Updates and dialogs + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Available update alert | Install, Release Notes, Later | The native offer now conditions Install on whether the real feed check resolved a Windows asset URL (`WindowsUpdates.zig`/`App.showAvailableUpdate`): when one exists Install is enabled and triggers the real download/verify/extract/upgrade pipeline; when none exists (the real, currently-live state — see Install progress) Install stays visibly disabled with an honest reason, replacing the old permanent "not implemented" label. `UpdateOfferDialog.zig`'s `buttonsFor(installable)` is unit-tested for both states, and a real, live `feed-check` run against the actual `scgopi/GraphCode` releases API (`Tools/windows/Tests/WindowsUpdateInstall.Live.Tests.ps1`) confirms the disabled path is genuine, not simulated, for the current release. What is **not** live-witnessed: clicking Install through a real enabled offer end-to-end in the running UI, because no Windows release asset currently exists to enable it against — that gap is honestly disclosed rather than faked | Partial | +| Install progress | In-window progress indicator | `WindowsUpdateInstall.zig` now implements the full download → SHA-256 checksum verify → extract → `GraphCode-Setup.ps1 -Command Upgrade` pipeline, reusing the existing packaging verification/rollback logic rather than a second copy, and reports phase/fraction progress through a `ProgressFn` callback. `UpdateInstallDialog.zig` renders that progress in a native window (download %, verifying, extracting, installing) plus a failure state; both are unit-tested (14 tests total across the two files, part of the 42-file/273-test hermetic `WindowsShell.Tests.ps1` suite). Real, non-simulated live evidence (`WindowsUpdateInstall.Live.Tests.ps1`, invoked directly — not part of the hermetic suite since it makes real network calls): a real HTTPS download of a real multi-megabyte GitHub release asset streams genuine progress (100+ real progress reports, 0→100%) and its SHA-256 is verified against the asset's real published digest before extraction is attempted; a deliberately wrong digest is rejected with `ChecksumMismatch` strictly before extraction, proving the checksum gate is not vacuous. Honestly out of scope and **not** provable right now: extracting and upgrading a real Windows ZIP asset end-to-end, because the last recorded and just-reconfirmed-live asset check found only macOS DMGs published — there is no real Windows asset to extract. Also unproven: whether `Move-InstallDirectory`'s rename succeeds while `graphcode-windows.exe` is the actual running, self-updating process (the existing `Packaging.RealLifecycle.Tests.ps1` proves the *opposite* guarantee — that a locked file blocks and rolls back — not this scenario) | Partial | +| Relaunch prompt | Relaunch Now/Later and session continuity explanation | `UpdateInstallDialog.zig` presents Relaunch Now / Later with session-continuity copy after a successful install, and `App.runInstall`/`relaunchAfterUpdate` wire the outcome: Relaunch Now respawns the executable (same `CreateProcessW` pattern as `launchWorkspace`) and then quits the current process; Later leaves the update staged and shows an honest status message. Pure logic (`relaunch_message`, outcome handling) is unit-tested; the real Win32 window/thread code compiles and runs but is not live-driven by UI automation in this PR. This row cannot move past `Partial` genuinely: there is no real Windows release asset to drive a real install to completion today, so the actual relaunch — and whether zmx-backed terminal sessions survive an Upgrade-triggered restart specifically, as opposed to the differently-scoped scenario `DaemonHandoff.Live.Tests.ps1` already covers — remains unproven live. Faking that would violate this fleet's evidence policy, so the row is left honestly `Partial` rather than asserted `Validated` | Partial | +| Install failure | Download in Browser/Cancel with reason | The Windows flow exposes Release Notes/Later and a disabled Install action explaining that in-app installation is not implemented, then hands off through the verified browser release page when notes are requested. This remains Partial until download/install failure handling exists | Partial | +| Loop rename | Title field, Return submits, explanatory text | The dedicated single-title modal explains where the title appears, prepopulates the current value, trims and validates submission, and re-resolves the stable loop ID after the modal. The populated UIA gate edits the native field and verifies Return submits and closes the dialog | Validated | +| Loop delete | Named loop and full consequence message | Names the loop, explains graph-connection removal, and defaults to cancellation | Validated | +| Chat rename/delete | Dedicated prompts | Dedicated single-title rename modal and named fail-closed deletion warning are wired from card actions and shortcuts | Validated | +| Project delete loops | Dedicated confirmation | Sidebar project menus expose Delete All Loops through one fail-closed implementation with graph and filesystem consequence copy, safe cancellation default, and the dedicated daemon command. The live UIA gate verifies the native confirmation and cancellation path | Validated | +| Project remove/trash | Distinct reversible remove and filesystem Trash choices | Remove from GraphCode is distinct, confirmed, and explicitly preserves files. Local project menus now add a separate confirmed Move Folder to Recycle Bin action using the Windows undo-capable shell operation; remote projects retain only the GraphCode removal action | Validated | + +## Accessibility, input, and visual behavior + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| UI Automation tree | Names, roles, selection, invoke/toggle, focus, live status for every visible surface | The synchronized live C++ provider exposes stable project rows, loop rows, project/overview/Quick Chat cards, worktree rows, destinations, canvas primary action, zoom controls, policy actions, focus, selection-change events, and status. The live gate uses explicitly in-process deterministic fixtures to validate populated RawView/ControlView navigation, real bounds, observable Quick Chat/workspace invocation effects, tagged-command isolation, identity-preserving reorder/removal, events, concurrency, and teardown. The Workspace menu's New, Rename, and Delete lifecycle commands and dynamic workspace-switch rows are now part of that native provider tree instead of existing only in `Accessibility.zig`; the Zig contract was reduced to the native fixed table, and a pinned-Zig executable test now fails when any contract id lacks an exact native fixed-table id. The live gate now also traverses the Workspace menu in RawView and ControlView, checks its fixed lifecycle names and InvokePattern exposure, and verifies at least one `workspace-switch-*` row under the same parent. `TerminalSurface.zig` retains reported selection and cell metrics, but callback metadata does not prove applied terminal selection. Exact-pin Winghostty `f5abc059` source already creates an embedded child-HWND Text/Text2 provider and routes `WM_GETOBJECT` to it; the earlier missing-provider premise was inaccurate. GraphCode now feeds that provider owned UTF-8 from its unchanged rendered-cell grid with independent UTF-16 length/cursor offsets, rather than a rolling raw VT byte tail. Focused producer tests exercise overwritten text, chunk splits, reset/rollover, Unicode scalar representation, bounds, allocation/lifetime and injected publication failures without native APIs. This is fixed current-grid content, not transcript/scrollback or full Unicode terminal rendering. Render/text publication remains best-effort: failures are reported and the provider may retain its last successful, no-longer-current snapshot. Native terminal text results, applied selection, visible caret/geometry, range/HRESULT conformance, retained-generation behavior, atomicity, daemon-to-model UIA integration, and remaining dialogs still need separate evidence or implementation; keyboard discovery and HelpText residuals are not closed | Partial | +| Reproducible DPI/geometry regression coverage | Control metrics for GraphCode-owned chrome scale correctly and predictably across 100/125/150/200% DPI | `Tools/windows/visual-baseline.ps1` previously only checked that each DPI variant's `scale`/`viewport` were present and positive. It now reimplements `Dpi.zig`'s exact `scale()` rounding formula, self-checked against `Dpi.zig`'s own fixed-point unit-test cases, reads the real base pixel values straight out of `DesignTokens.zig` (not a copy baked into the manifest), and asserts the scaled geometry for `sidebar_width`, `tab_bar_height`, `pane_header_height`, and `loop_bar_height` at the real Windows per-monitor DPI values (96/120/144/192) is monotonic and matches the 96-DPI base exactly at 100%. Every `regionGeometry` entry is required to target a `deterministicScreenshotRegions` (GraphCode-owned) region, never a Winghostty-owned one, keeping third-party terminal pixels structurally out of scope. **This check performs static manifest/geometry metadata validation, not rendered-output comparison: it never launches the app, captures a window, or rasterizes a bitmap.** It re-derives expected numeric geometry from source-of-truth code and checks the manifest against that math, which is materially stronger than the prior presence-only checks and does catch real drift, but it is not a screenshot diff and should not be read as one; this repo/CI has no deterministic way to rasterize a live Win32 window. Manually re-verified that corrupting either a DPI value or a `DesignTokens.zig` constant makes the script fail | Validated | +| Keyboard discovery | Every shortcut represented by a menu item or visible hint where practical | Restored File, Loop, Terminal, View, and Help menus expose the primary project, graph, terminal, workspace, settings, update, and zoom commands with shortcut labels. The shared node popup no longer advertises Enter for Open Terminal or Ctrl+E for Edit Details: root Enter has no standalone open action, and Ctrl+E renames a selected loop or edits a selected edge. Two executable contracts failed on the old captions and pass on the corrected ones through the actual menu builder and `GetMenuStringW`, preserving command IDs, mappings, order, and enabled states across ordinary/resolved/composite/unwired targets. They inspect unattached menu handles without creating windows or displaying popups; mapper checks preserve toolbar Enter activation and existing Ctrl+E/F2 routes. The Windows README now distinguishes root, terminal, and dialog contexts and corrects toggle/chat-delete bindings. Pure mapper regressions cover Ctrl+Shift+OEM/legacy ASCII comma selecting product Settings, plain Ctrl+comma retaining Advanced Connection Settings, and unmodified/unrelated keys. An unattached native accelerator-table test checks the matching documented product binding and the unchanged original 15 bindings/order; this is not physical keyboard, focus, or dialog-opening evidence. Some context-only actions and canvas gestures still lack visible hints. No live keyboard or macOS runtime walkthrough was performed; remaining context-only/gesture discovery, other caption/routing discrepancies, and keyboard popup access still need evidence | Partial | +| IME/dead keys/layouts | Native composition in forms and terminal | Winghostty gate covers terminal IME; generic EDIT controls cover forms | Partial | +| Clipboard/selection | Terminal copy/paste and mouse selection | Terminal-context Ctrl+Shift+C copies the active surface's reported accessibility selection range through Winghostty into Windows `CF_UNICODETEXT`; Ctrl+Shift+V reads only `CF_UNICODETEXT`, converts UTF-16 to UTF-8, and calls Winghostty's paste validator and paste entry point with `allow_unsafe=0`. Clipboard conversion tests preserve Unicode, CRLF, LF, and empty text; app routing tests prove the shortcuts are terminal-context-only and preserve the existing global Ctrl+Shift+C Clone Repository route. Unsafe multiline/control-containing pastes are rejected with a status message rather than forced through, and no confirmation UI is provided. Clipboard Win32 calls, mouse-driven selection, actual rendered selection extraction, provider callback notifications, and end-to-end paste/copy on a live desktop were not exercised here; this row remains Partial | Partial | +| Per-monitor DPI | Layout and controls scale correctly across monitors | The process now declares real per-monitor-v2 DPI awareness at startup (`Win32.enablePerMonitorDpiAwareness()`, called before any window is created) instead of relying on system-DPI bitmap stretching; without this, Windows never delivers real per-monitor `WM_DPICHANGED` data to a DPI-unaware process. `App.zig` seeds the real startup DPI via `GetDpiForWindow` immediately after window creation (rather than assuming 96 DPI/100% until the first monitor move) and forwards every live `WM_DPICHANGED` to `TerminalWorkspace.Workspace.setDpi()`. Previously, `TerminalSurface.zig`'s `onDpiChanged` callback silently discarded the `dpi`/`scale` winghostty reported, and every terminal surface was created with `font_scale` hardcoded to `1.0`, so terminal text never actually rescaled on a DPI change or on a monitor with non-100% DPI at launch. `Workspace.setDpi()` now propagates the real runtime DPI to every live surface via winghostty's own `winghostty_surface_notify_dpi_changed` + `winghostty_surface_set_font_scale` (the two operations the provider actually exposes for this), and `surfaceOptions()` seeds new surfaces' `font_scale` from the workspace's last-known DPI instead of a fixed `1.0`. Deliberately does not also pre-scale `options.input.cell_width`/`cell_height` (kept at their 96-DPI logical baseline) so the DPI ratio is applied exactly once, through `font_scale`, avoiding double scaling. `onMetricsChanged`/`onAccessibilitySelection`, previously also fully discarded, now record the host's reported cell metrics and terminal text-selection range per surface instead of losing them. Verified with `zig build` (full app, pinned Zig 0.15.2 against the exact pinned Winghostty provider) and `zig test src/TerminalSurface.zig` (new `Dpi.fontScale` unit test plus all 13 pre-existing tests, 14/14). No live multi-monitor walkthrough was recorded (this environment has no interactive multi-DPI desktop), so this remains Partial pending that end-to-end evidence | Partial | +| Dark visual language | Dark canvas/cards/sheets and legible state hierarchy | Existing `DesignTokens`, repository dialogs and `NativeForms` supply the dark native palette and teaching tiles. [Actual production-renderer captures](visual-baseline/rendered-windows/README.md) now preserve canvas/sidebar, Product Settings and workspace clients at native 96 DPI. The real PNG comparator verifies exact opaque canvas/card/sidebar/dialog samples; setup uses a synthetic disconnected fixture, UIA invocation and native WM_COMMAND, not keyboard-menu proof. Settings retains native light buttons; current macOS Settings uses a native grouped form, so these were not speculatively darkened. Other sheets/state combinations, legibility on every surface and a compatible current macOS capture remain unverified | Partial | +| Font rendering quality | Legible, ClearType-quality text on every surface, matching macOS's default anti-aliased text | The shared `AppFont.zig` cache requests Segoe UI at `CLEARTYPE_QUALITY`, with per-DPI native controls and logical-size selection for buffered canvas painting. [Actual 96-DPI glyph samples](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) preserve card/sidebar/Settings-title pixels with 27/31/55 distinct colors; the native button sample has two. These counts and recorded system font-smoothing settings are observations, not legibility thresholds or actual font-face certification. Native control LOGFONT metadata, every-surface/multi-DPI review, terminal-text readability and matched current macOS evidence remain unavailable. No speculative font fix was made | Partial | +| Line/shape anti-aliasing | Smooth, anti-aliased lines/curves/rounded corners matching macOS's Core Graphics default | Existing `GdiplusAA` draws solid Beziers, rounded cards and metric segments with GDI fallbacks; axis-aligned grid lines and dashed/preview paths remain plain GDI. [Real app captures](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) now use production GDI+ startup, with both disabling automation hooks unset, rather than the ordinary UIA gate or a standalone drawing surrogate. The sampled selected-card corner has 31 colors and metric sparkline 30; original pixels and source/UIA-mapped regions are preserved. Other colors are not automatically antialias coverage, and there is no invented quality threshold. All edge styles/DPI states and equality with macOS Core Graphics remain unproven | Partial | + +| Color palette fidelity | Windows tones/gradients match macOS `Theme.swift` 1:1 (not just "generically dark") | [Immutable before/after app captures and replay](visual-baseline/rendered-windows/README.md) prove a bounded COLORREF bug: the same 96-DPI workspace focus-strip ROI [500,130,64,2] changed from 128/128 orange RGB255,132,10 pixels to 128/128 blue RGB10,132,255 after only `pane_focus_tint` changed from `0x000A84FF` to `0x00FF840A`. The production comparator independently derives current Theme.paneFocusTint, decodes Windows BGR and checks actual pixels; old-orange/channel-swap/delta-1 controls fail. Canvas/grid/card/sidebar/dialog/selected-tab opaque samples also match exact source colors. Existing two-stop `GdiGradient` chrome is measured, not equated with macOS three-stop/material compositing. All tones/states and a compatible current macOS rendered comparison remain unverified; historical manifest and separate two-token currentThemeContract are unchanged | Partial | + +**Known out-of-scope CI gap surfaced while validating the row above (PR #398):** the live `windows-shell` UIA gate's "New Loop" assertion invoked via the sidebar's `project-new-loop-*` element (`Tools/windows/uia-live-gate.ps1`, "project-row New Loop did not open the node form") fails intermittently/deterministically across unrelated branches (reproduced on `coneilen-microsoft-canvas-workspace-detail-parity` and `coneilen-microsoft-updates-dialogs-quick-chats-parity` as well, with no relation to dialog rendering code). This is pre-existing test-infrastructure flakiness, not a visual-polish regression; it is out of this pass's scope and is flagged here for a dedicated follow-up. + +**Known shared-environment gate instability surfaced while validating the Window toolbar/Update command rows above:** with the local shell toolchain unblocked (PR #433), multiple parity sessions now build and run `graphcode-windows.exe`/`zmx.exe` concurrently on the same interactive desktop. The pre-existing worktree reorder/removal focus-retention stress block in `Tools/windows/uia-live-gate.ps1` (`Retain-FocusWithRetry`, its `Start-Job` concurrent-UIA-read stress, and the plain `Get-DirectChildren` tree walks around it) repeatedly hit raw, uncaught COM exceptions (`GetFirstChild`/`GetNextSibling` "Could not open the process token"/"Unrecognized error.") at different, unrelated call sites across many local runs, and a separate run was independently derailed by another desktop application (Chrome) stealing the foreground window during a modal-dialog wait. None of this reproduced from this branch's own changes — a minimal, standalone re-run that skips straight to the Update command assertions using the same shell process, native menu, and gate helpers passed cleanly and repeatably. This matches flakiness independently reported by sibling parity sessions and is a pre-existing, shared test-infrastructure limitation, not a product regression; it blocked getting one single uninterrupted top-to-bottom `uia-live-gate.ps1` run this session and is flagged here for follow-up (likely hardening `Get-DirectChildren`/`Retain-FocusWithRetry` against concurrent-desktop contention). + +## Audit conclusion + +The Windows branch has substantial protocol, lifecycle, persistence, terminal, graph +mutation, tray, and packaging behavior, but it does **not** currently have complete UI +or screen parity. The previous parity statement conflated backend reachability with +user-visible parity. The largest corrective work is: + +1. Restore and complete the application menu and navigation state model. +2. Implement the sidebar, global graph, Quick Chats canvas, project canvas chrome, and + loop workspace as distinct application-owned surfaces. +3. Replace raw protocol forms with structured node, edge, settings, repository, and + worktree screens. +4. Implement the missing update, project-management, rename/delete, empty, and + connection-info states. +5. Expand UI Automation and live walkthrough coverage to every row above before any + complete-parity claim. From a77f7265d5a5f3bb274ad51dfb12de368e8d4b2c Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Sun, 27 Sep 2026 13:19:58 -0700 Subject: [PATCH 2/2] fix(windows): preserve tracked LF for native menu test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- graphcode-windows/src/GraphContextMenu.zig | 1476 ++++++++++---------- investigation/ui-parity-matrix.md | 426 +++--- 2 files changed, 951 insertions(+), 951 deletions(-) diff --git a/graphcode-windows/src/GraphContextMenu.zig b/graphcode-windows/src/GraphContextMenu.zig index ba3f5d85..10e63f80 100644 --- a/graphcode-windows/src/GraphContextMenu.zig +++ b/graphcode-windows/src/GraphContextMenu.zig @@ -1,738 +1,738 @@ -const c = @import("Win32.zig").c; -const Wire = @import("Wire.zig"); -const SketchPromotion = @import("SketchPromotion.zig"); - -pub const NodeTarget = struct { - project_path: []const u8, - id: []const u8, - composite: bool = false, - can_arm: bool = false, - unwired: bool = false, - follows_template: bool = false, - resolved: bool = false, - can_create_child: bool = true, - sketch: bool = false, - promotion_context: ?*const SketchPromotion.Context = null, -}; - -pub const BackgroundTarget = struct { - project_path: []const u8, - local_filesystem: bool, - can_create_edge: bool, -}; - -pub const EdgeTarget = struct { - project_path: []const u8, - id: []const u8, -}; - -pub const QuickChatTarget = struct { - id: []const u8, -}; - -pub const ProjectTarget = struct { - path: []const u8, - remote: bool, -}; - -pub const Target = union(enum) { - background: BackgroundTarget, - quick_chats, - project: ProjectTarget, - node: NodeTarget, - edge: EdgeTarget, - quick_chat: QuickChatTarget, -}; - -pub const Action = enum { - none, - edit_node, - promote_goal, - promote_turn, - promote_timed, - rename_node, - stop_node, - delete_node, - open_terminal, - new_child_node, - message_node, - memo_node, - open_composite, - pilot_composite, - arm_composite, - save_node_template, - detach_template, - wire_node, - mark_entry, - edit_edge, - delete_edge, - create_edge, - open_quick_chat, - rename_quick_chat, - delete_quick_chat, - open_project, - new_project_loop, - inspect_project_worktrees, - project_settings, - reveal_project, - remote_project_info, - close_project, - remove_project, - move_project, - trash_project, - delete_project_loops, - new_quick_chat, -}; - -pub const Callback = *const fn (?*anyopaque, Action, Target) void; - -pub fn requiresConfirmation(action: Action) bool { - return action == .delete_node or action == .delete_edge or action == .delete_quick_chat or - action == .remove_project or action == .trash_project or action == .delete_project_loops; -} - -pub fn shouldApply(action: Action, confirmed: bool) bool { - return !requiresConfirmation(action) or confirmed; -} - -pub fn canEditEdge(edge_id: []const u8) bool { - return edge_id.len != 0; -} - -const ids = struct { - const edit_node = 5100; - const rename_node = 5101; - const stop_node = 5102; - const delete_node = 5103; - const open_terminal = 5104; - const message_node = 5105; - const memo_node = 5106; - const open_composite = 5113; - const pilot_composite = 5107; - const arm_composite = 5108; - const save_node_template = 5114; - const detach_template = 5115; - const new_child_node = 5119; - const promote_goal = 5116; - const promote_turn = 5117; - const promote_timed = 5118; - const wire_node = 5109; - const mark_entry = 5112; - const edit_edge = 5110; - const delete_edge = 5111; - const create_edge = 5120; - const open_quick_chat = 5130; - const rename_quick_chat = 5131; - const delete_quick_chat = 5132; - const open_project = 5140; - const new_project_loop = 5141; - const inspect_project_worktrees = 5142; - const project_settings = 5143; - const reveal_project = 5144; - const remote_project_info = 5145; - const close_project = 5146; - const remove_project = 5147; - const move_project = 5149; - const trash_project = 5151; - const delete_project_loops = 5148; - const new_quick_chat = 5150; -}; - -pub const move_project_menu_text = "Move Project... (unavailable: daemon support required)"; - -pub const MoveProjectMenuItem = struct { - id: usize = ids.move_project, - text: []const u8 = move_project_menu_text, - enabled: bool, -}; - -/// Builds the real "Move Project..." popup item used by `show()` for the -/// project context menu. Exposed so tests can exercise the exact same -/// data the live Win32 menu is constructed from, rather than only a -/// separate accessibility contract model. -pub fn moveProjectMenuItem() MoveProjectMenuItem { - return .{ .enabled = Wire.supportsProjectRelocation() }; -} - -pub const NodeMenuItem = struct { - id: usize = 0, - text: []const u8 = "", - enabled: bool = true, -}; - -pub fn newChildNodeMenuItem(node: NodeTarget) ?NodeMenuItem { - if (node.resolved) return null; - return .{ .id = ids.new_child_node, .text = "New Child Node...", .enabled = node.can_create_child }; -} - -pub const NodeMenuPlan = struct { - items: [15]NodeMenuItem = undefined, - len: usize = 0, - - fn add(self: *NodeMenuPlan, item: NodeMenuItem) void { - self.items[self.len] = item; - self.len += 1; - } -}; - -pub fn nodeMenuPlan(node: NodeTarget) NodeMenuPlan { - var plan = NodeMenuPlan{}; - plan.add(.{ .id = ids.open_terminal, .text = "Open Terminal" }); - if (newChildNodeMenuItem(node)) |item| plan.add(item); - if (node.unwired) { - plan.add(.{ .id = ids.wire_node, .text = "Wire it up" }); - plan.add(.{ .id = ids.mark_entry, .text = "Mark as entry" }); - plan.add(.{}); - } - if (node.composite) { - plan.add(.{ .id = ids.open_composite, .text = "Open Group" }); - plan.add(.{ .id = ids.pilot_composite, .text = "Pilot Once" }); - plan.add(.{ .id = ids.arm_composite, .text = "Arm Schedule", .enabled = node.can_arm }); - plan.add(.{}); - } - plan.add(.{ .id = ids.edit_node, .text = "Edit Details..." }); - plan.add(.{ .id = ids.save_node_template, .text = "Save as Template..." }); - if (node.follows_template) plan.add(.{ .id = ids.detach_template, .text = "Detach from Template" }); - plan.add(.{ .id = ids.rename_node, .text = "Rename...\tF2" }); - if (!node.resolved) plan.add(.{ .id = ids.stop_node, .text = "Stop\tCtrl+S" }); - plan.add(.{ .id = ids.delete_node, .text = "Delete Loop...\tDelete" }); - return plan; -} - -pub const PromotionItem = struct { id: usize, text: []const u8, action: Action }; -const promotion_items = [_]PromotionItem{ - .{ .id = ids.promote_goal, .text = "Goal - asks for a done check", .action = .promote_goal }, - .{ .id = ids.promote_turn, .text = "Turn - asks where to pause", .action = .promote_turn }, - .{ .id = ids.promote_timed, .text = "Timed - asks for a cadence", .action = .promote_timed }, -}; - -pub fn promotionItems(node: NodeTarget) []const PromotionItem { - return if (node.sketch) &promotion_items else &.{}; -} - -pub fn promotionTarget(action: Action) ?SketchPromotion.Target { - return switch (action) { - .promote_goal => .goal, - .promote_turn => .turn, - .promote_timed => .timed, - else => null, - }; -} - -pub fn promotionEnabled(node: NodeTarget) bool { - return node.sketch and node.promotion_context != null; -} - -pub fn show( - parent: c.HWND, - target: Target, - x: i32, - y: i32, - context: ?*anyopaque, - callback: Callback, -) void { - const menu = buildMenu(target) orelse return; - defer _ = c.DestroyMenu(menu); - const command = c.TrackPopupMenu( - menu, - c.TPM_RETURNCMD | c.TPM_NONOTIFY | c.TPM_RIGHTBUTTON, - x, - y, - 0, - parent, - null, - ); - const action = actionForCommand(command); - if (action != .none) callback(context, action, target); -} - -fn buildMenu(target: Target) c.HMENU { - const menu = c.CreatePopupMenu() orelse return null; - switch (target) { - .background => |background| { - if (!std.mem.eql(u8, background.project_path, "graphcode://global")) { - appendEnabled(menu, ids.inspect_project_worktrees, "Worktrees...", background.local_filesystem); - appendEnabled(menu, ids.project_settings, "Project Settings...", background.local_filesystem); - appendEnabled(menu, ids.reveal_project, "Show in Explorer", background.local_filesystem); - separator(menu); - } - appendEnabled(menu, ids.create_edge, "Create Edge", background.can_create_edge); - }, - .quick_chats => append(menu, ids.new_quick_chat, "New Chat"), - .project => |project| { - append(menu, ids.open_project, "Open Project"); - append(menu, ids.new_project_loop, "New Loop...\tCtrl+N"); - separator(menu); - append(menu, ids.inspect_project_worktrees, "Worktrees..."); - append(menu, ids.project_settings, "Project Settings..."); - if (project.remote) - append(menu, ids.remote_project_info, "Remote Connection Info") - else - append(menu, ids.reveal_project, "Show in Explorer"); - separator(menu); - append(menu, ids.close_project, "Close Project"); - if (!project.remote) { - const move_item = moveProjectMenuItem(); - appendEnabled(menu, move_item.id, move_item.text, move_item.enabled); - append(menu, ids.trash_project, "Move to Recycle Bin..."); - } - append(menu, ids.remove_project, "Remove from GraphCode..."); - append(menu, ids.delete_project_loops, "Delete All Loops..."); - }, - .node => |node| { - const plan = nodeMenuPlan(node); - for (plan.items[0..plan.len]) |item| { - if (item.id == 0) separator(menu) else appendEnabled(menu, item.id, item.text, item.enabled); - if (item.id == ids.edit_node) { - const items = promotionItems(node); - if (items.len != 0) { - const submenu = c.CreatePopupMenu() orelse { - _ = c.DestroyMenu(menu); - return null; - }; - for (items) |promotion_item| appendEnabled(submenu, promotion_item.id, promotion_item.text, promotionEnabled(node)); - if (c.AppendMenuW(menu, c.MF_POPUP | c.MF_STRING, @intFromPtr(submenu), std.unicode.utf8ToUtf16LeStringLiteral("Promote to...").ptr) == 0) { - _ = c.DestroyMenu(submenu); - _ = c.DestroyMenu(menu); - return null; - } - } - } - } - }, - .edge => { - append(menu, ids.edit_edge, "Edit Edge..."); - append(menu, ids.delete_edge, "Delete Edge"); - }, - .quick_chat => { - append(menu, ids.open_quick_chat, "Open Chat"); - append(menu, ids.rename_quick_chat, "Rename...\tCtrl+Shift+Q"); - append(menu, ids.delete_quick_chat, "Delete Chat...\tCtrl+Shift+Delete"); - }, - } - return menu; -} - -pub fn confirm(parent: c.HWND, title: []const u8, message: []const u8) bool { - const title_wide = toWide(title) orelse return false; - defer std.heap.c_allocator.free(title_wide); - const message_wide = toWide(message) orelse return false; - defer std.heap.c_allocator.free(message_wide); - return c.MessageBoxW(parent, message_wide.ptr, title_wide.ptr, c.MB_ICONWARNING | c.MB_YESNO | c.MB_DEFBUTTON2) == c.IDYES; -} - -fn actionForCommand(command: c_int) Action { - return switch (command) { - ids.rename_node => .rename_node, - ids.stop_node => .stop_node, - ids.delete_node => .delete_node, - ids.open_terminal => .open_terminal, - ids.new_child_node => .new_child_node, - ids.edit_node => .edit_node, - ids.promote_goal => .promote_goal, - ids.promote_turn => .promote_turn, - ids.promote_timed => .promote_timed, - ids.open_composite => .open_composite, - ids.pilot_composite => .pilot_composite, - ids.arm_composite => .arm_composite, - ids.save_node_template => .save_node_template, - ids.detach_template => .detach_template, - ids.wire_node => .wire_node, - ids.mark_entry => .mark_entry, - ids.edit_edge => .edit_edge, - ids.delete_edge => .delete_edge, - ids.create_edge => .create_edge, - ids.open_quick_chat => .open_quick_chat, - ids.rename_quick_chat => .rename_quick_chat, - ids.delete_quick_chat => .delete_quick_chat, - ids.open_project => .open_project, - ids.new_project_loop => .new_project_loop, - ids.inspect_project_worktrees => .inspect_project_worktrees, - ids.project_settings => .project_settings, - ids.reveal_project => .reveal_project, - ids.remote_project_info => .remote_project_info, - ids.close_project => .close_project, - ids.remove_project => .remove_project, - ids.move_project => .move_project, - ids.trash_project => .trash_project, - ids.delete_project_loops => .delete_project_loops, - ids.new_quick_chat => .new_quick_chat, - else => .none, - }; -} - -fn append(menu: c.HMENU, id: usize, text: []const u8) void { - appendEnabled(menu, id, text, true); -} - -fn appendEnabled(menu: c.HMENU, id: usize, text: []const u8, enabled: bool) void { - const wide = toWide(text) orelse return; - defer std.heap.c_allocator.free(wide); - var flags: c.UINT = @intCast(c.MF_STRING); - if (!enabled) flags |= @intCast(c.MF_GRAYED); - _ = c.AppendMenuW(menu, flags, id, wide.ptr); -} - -fn separator(menu: c.HMENU) void { - _ = c.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); -} - -fn toWide(text: []const u8) ?[]u16 { - const raw = std.unicode.utf8ToUtf16LeAlloc(std.heap.c_allocator, text) catch return null; - const result = std.heap.c_allocator.alloc(u16, raw.len + 1) catch { - std.heap.c_allocator.free(raw); - return null; - }; - @memcpy(result[0..raw.len], raw); - result[raw.len] = 0; - std.heap.c_allocator.free(raw); - return result; -} - -const std = @import("std"); - -test "custody child menu plan is unresolved-only and uses its reserved command" { - const target = NodeTarget{ .project_path = "B", .id = "11111111-1111-4111-8111-111111111111" }; - const item = newChildNodeMenuItem(target).?; - try std.testing.expectEqual(@as(usize, 5119), item.id); - try std.testing.expectEqualStrings("New Child Node...", item.text); - try std.testing.expect(item.enabled); - try std.testing.expectEqual(Action.new_child_node, actionForCommand(@intCast(item.id))); - var resolved = target; - resolved.resolved = true; - try std.testing.expect(newChildNodeMenuItem(resolved) == null); - try std.testing.expectEqual(Action.none, actionForCommand(0)); -} - -test "custody child node menu plan preserves existing items when creation is unavailable" { - const expected_unresolved = [_]usize{ 5104, 5119, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5102, 5103 }; - const expected_resolved = [_]usize{ 5104, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5103 }; - for ([_]bool{ false, true }) |resolved| { - const plan = nodeMenuPlan(.{ - .project_path = "B", - .id = "parent", - .composite = true, - .unwired = true, - .follows_template = true, - .resolved = resolved, - .can_create_child = false, - }); - const expected: []const usize = if (resolved) &expected_resolved else &expected_unresolved; - try std.testing.expectEqual(expected.len, plan.len); - for (plan.items[0..plan.len], expected) |item, id| { - try std.testing.expectEqual(id, item.id); - try std.testing.expectEqual(id != 5119 and id != 5108, item.enabled); - } - try std.testing.expectEqualStrings("Open Terminal", plan.items[0].text); - try std.testing.expectEqualStrings("Delete Loop...\tDelete", plan.items[plan.len - 1].text); - } -} - -test "sketch promotion real menu plan exposes only three eligible target actions" { - var node = NodeTarget{ .project_path = "B", .id = "id" }; - try std.testing.expectEqual(@as(usize, 0), promotionItems(node).len); - node.sketch = true; - try std.testing.expectEqual(@as(usize, 3), promotionItems(node).len); - try std.testing.expect(!promotionEnabled(node)); - const context = SketchPromotion.Context{}; - node.promotion_context = &context; - try std.testing.expect(promotionEnabled(node)); - for (promotionItems(node), [_]SketchPromotion.Target{ .goal, .turn, .timed }) |item, target| { - try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); - try std.testing.expectEqual(target, promotionTarget(item.action).?); - } - for (std.enums.values(Action)) |action| { - if (action != .promote_goal and action != .promote_turn and action != .promote_timed) - try std.testing.expect(promotionTarget(action) == null); - } -} - -test "sketch promotion production popup installs native Goal Turn Timed submenu on hidden HWND" { - const hwnd = c.CreateWindowExW( - 0, - std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), - std.unicode.utf8ToUtf16LeStringLiteral("Promotion menu test"), - c.WS_OVERLAPPEDWINDOW, - 0, - 0, - 0, - 0, - null, - null, - c.GetModuleHandleW(null), - null, - ) orelse return error.WindowCreationFailed; - defer _ = c.DestroyWindow(hwnd); - - const context = SketchPromotion.Context{}; - const cases = [_]struct { sketch: bool, context: ?*const SketchPromotion.Context, enabled: bool }{ - .{ .sketch = false, .context = null, .enabled = false }, - .{ .sketch = true, .context = null, .enabled = false }, - .{ .sketch = true, .context = &context, .enabled = true }, - }; - for (cases) |case| { - const popup = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "sketch-a", - .sketch = case.sketch, - .promotion_context = case.context, - } }) orelse return error.MenuCreationFailed; - const bar = c.CreateMenu() orelse { - _ = c.DestroyMenu(popup); - return error.MenuCreationFailed; - }; - defer { - _ = c.SetMenu(hwnd, null); - _ = c.DestroyMenu(bar); - } - if (c.AppendMenuW(bar, c.MF_POPUP | c.MF_STRING, @intFromPtr(popup), std.unicode.utf8ToUtf16LeStringLiteral("Node").ptr) == 0) { - _ = c.DestroyMenu(popup); - return error.MenuInstallFailed; - } - if (c.SetMenu(hwnd, bar) == 0) return error.MenuInstallFailed; - const menu = c.GetSubMenu(c.GetMenu(hwnd), 0); - try std.testing.expect(menu != null); - var edit_position: c_int = 0; - while (edit_position < c.GetMenuItemCount(menu) and c.GetMenuItemID(menu, edit_position) != ids.edit_node) : (edit_position += 1) {} - try std.testing.expect(edit_position < c.GetMenuItemCount(menu)); - const submenu = c.GetSubMenu(menu, edit_position + 1); - if (!case.sketch) { - try std.testing.expect(submenu == null); - for ([_]c.UINT{ 5116, 5117, 5118 }) |id| - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); - continue; - } - try std.testing.expect(submenu != null); - var title: [64]u16 = undefined; - const title_len = c.GetMenuStringW(menu, @intCast(edit_position + 1), &title, title.len, c.MF_BYPOSITION); - try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Promote to..."), title[0..@intCast(title_len)]); - try std.testing.expectEqual(@as(c_int, 3), c.GetMenuItemCount(submenu)); - for ([_]struct { id: c.UINT, label: []const u16, action: Action }{ - .{ .id = 5116, .label = std.unicode.utf8ToUtf16LeStringLiteral("Goal - asks for a done check"), .action = .promote_goal }, - .{ .id = 5117, .label = std.unicode.utf8ToUtf16LeStringLiteral("Turn - asks where to pause"), .action = .promote_turn }, - .{ .id = 5118, .label = std.unicode.utf8ToUtf16LeStringLiteral("Timed - asks for a cadence"), .action = .promote_timed }, - }, 0..) |item, index| { - try std.testing.expectEqual(item.id, c.GetMenuItemID(submenu, @intCast(index))); - const state = c.GetMenuState(submenu, item.id, c.MF_BYCOMMAND); - try std.testing.expect(state != std.math.maxInt(c.UINT)); - try std.testing.expectEqual(case.enabled, state & c.MF_GRAYED == 0); - var label: [64]u16 = undefined; - const length = c.GetMenuStringW(submenu, item.id, &label, label.len, c.MF_BYCOMMAND); - try std.testing.expectEqualSlices(u16, item.label, label[0..@intCast(length)]); - try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); - } - } -} - -test "background menu exposes supported folder actions and gates edge creation" { - const menu = buildMenu(.{ .background = .{ - .project_path = "C:\\fixture", - .local_filesystem = true, - .can_create_edge = false, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.inspect_project_worktrees, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.project_settings, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.reveal_project, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(menu, ids.create_edge, c.MF_BYCOMMAND)); -} - -test "resolved node menu hides Stop but retains rename and edit details" { - const menu = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-a", - .resolved = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.stop_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.rename_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.message_node, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.memo_node, c.MF_BYCOMMAND)); -} - -test "node shortcut captions keep Open Terminal without a standalone Enter binding" { - const InputRouter = @import("InputRouter.zig"); - try std.testing.expectEqual(InputRouter.Action.none, InputRouter.keyAction(c.VK_RETURN, false, false)); - try std.testing.expectEqual(InputRouter.HeaderKey.none, InputRouter.headerKey(c.VK_RETURN, false, false, false, false)); - try std.testing.expectEqual(InputRouter.HeaderKey.activate, InputRouter.headerKey(c.VK_RETURN, false, false, false, true)); - try std.testing.expectEqual(Action.open_terminal, actionForCommand(ids.open_terminal)); - - const targets = [_]NodeTarget{ - .{ .project_path = "C:\\fixture", .id = "ordinary" }, - .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, - .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, - .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, - }; - for (targets) |target| { - const menu = buildMenu(.{ .node = target }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, ids.open_terminal), c.GetMenuItemID(menu, 0)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.open_terminal, c.MF_BYCOMMAND)); - var caption: [128]u16 = undefined; - const length = c.GetMenuStringW(menu, ids.open_terminal, &caption, caption.len, c.MF_BYCOMMAND); - try std.testing.expect(length > 0 and length < caption.len - 1); - try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Open Terminal"), caption[0..@intCast(length)]); - } -} - -test "node shortcut captions keep Edit Details without the rename Ctrl E hint" { - const InputRouter = @import("InputRouter.zig"); - try std.testing.expectEqual(InputRouter.Action.edit_node, InputRouter.keyAction('E', true, false)); - try std.testing.expectEqual(InputRouter.Action.rename_selected, InputRouter.keyAction(c.VK_F2, false, false)); - try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); - try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); - - const targets = [_]struct { node: NodeTarget, edit_position: c_int }{ - .{ .node = .{ .project_path = "C:\\fixture", .id = "ordinary" }, .edit_position = 2 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, .edit_position = 1 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, .edit_position = 6 }, - .{ .node = .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, .edit_position = 5 }, - }; - for (targets) |target| { - const menu = buildMenu(.{ .node = target.node }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, ids.edit_node), c.GetMenuItemID(menu, target.edit_position)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); - var caption: [128]u16 = undefined; - const length = c.GetMenuStringW(menu, ids.edit_node, &caption, caption.len, c.MF_BYCOMMAND); - try std.testing.expect(length > 0 and length < caption.len - 1); - try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Edit Details..."), caption[0..@intCast(length)]); - } -} - -test "background menu disables unavailable folder actions and omits them for global scope" { - const remote = buildMenu(.{ .background = .{ - .project_path = "ssh://builder/fixture", - .local_filesystem = false, - .can_create_edge = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(remote); - for ([_]c.UINT{ ids.inspect_project_worktrees, ids.project_settings, ids.reveal_project }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(remote, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(remote, ids.create_edge, c.MF_BYCOMMAND)); - - const global = buildMenu(.{ .background = .{ - .project_path = "graphcode://global", - .local_filesystem = false, - .can_create_edge = false, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(global); - try std.testing.expectEqual(@as(c_int, 1), c.GetMenuItemCount(global)); - try std.testing.expectEqual(@as(c.UINT, ids.create_edge), c.GetMenuItemID(global, 0)); -} - -test "native node menu variants expose only eligible actions" { - for ([_]bool{ false, true }) |can_arm| { - const menu = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-b", - .composite = true, - .can_arm = can_arm, - .follows_template = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c.UINT, if (can_arm) c.MF_ENABLED else c.MF_GRAYED), c.GetMenuState(menu, ids.arm_composite, c.MF_BYCOMMAND)); - for ([_]c.UINT{ ids.open_composite, ids.pilot_composite, ids.stop_node, ids.detach_template }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.wire_node, c.MF_BYCOMMAND)); - } - const unwired = buildMenu(.{ .node = .{ - .project_path = "C:\\fixture", - .id = "node-c", - .unwired = true, - } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(unwired); - for ([_]c.UINT{ ids.wire_node, ids.mark_entry, ids.stop_node }) |id| - try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(unwired, id, c.MF_BYCOMMAND)); - try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(unwired, ids.arm_composite, c.MF_BYCOMMAND)); -} - -test "edge menu preserves edit and delete command ordering" { - const menu = buildMenu(.{ .edge = .{ .project_path = "C:\\fixture", .id = "edge-a" } }) orelse return error.MenuCreationFailed; - defer _ = c.DestroyMenu(menu); - try std.testing.expectEqual(@as(c_int, 2), c.GetMenuItemCount(menu)); - try std.testing.expectEqual(@as(c.UINT, ids.edit_edge), c.GetMenuItemID(menu, 0)); - try std.testing.expectEqual(@as(c.UINT, ids.delete_edge), c.GetMenuItemID(menu, 1)); -} - -test "context actions remain stable when graph IDs are reordered" { - try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); - try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); - try std.testing.expectEqual(Action.delete_edge, actionForCommand(ids.delete_edge)); - try std.testing.expectEqual(Action.none, actionForCommand(0)); - try std.testing.expectEqual(Action.pilot_composite, actionForCommand(ids.pilot_composite)); - try std.testing.expectEqual(Action.open_composite, actionForCommand(ids.open_composite)); - try std.testing.expectEqual(Action.arm_composite, actionForCommand(ids.arm_composite)); - try std.testing.expectEqual(Action.wire_node, actionForCommand(ids.wire_node)); - try std.testing.expectEqual(Action.mark_entry, actionForCommand(ids.mark_entry)); - try std.testing.expectEqual(Action.save_node_template, actionForCommand(ids.save_node_template)); - try std.testing.expectEqual(Action.detach_template, actionForCommand(ids.detach_template)); -} - -test "destructive context actions cannot bypass a cancelled confirmation" { - try std.testing.expect(!shouldApply(.delete_node, false)); - try std.testing.expect(!shouldApply(.delete_edge, false)); - try std.testing.expect(!shouldApply(.delete_quick_chat, false)); - try std.testing.expect(!shouldApply(.remove_project, false)); - try std.testing.expect(!shouldApply(.trash_project, false)); - try std.testing.expect(!shouldApply(.delete_project_loops, false)); - try std.testing.expect(shouldApply(.rename_node, false)); -} - -test "quick chat context targets preserve stable identity" { - const target = QuickChatTarget{ .id = "chat-a" }; - try std.testing.expectEqualStrings("chat-a", target.id); - try std.testing.expectEqual(Action.open_quick_chat, actionForCommand(ids.open_quick_chat)); - try std.testing.expectEqual(Action.rename_quick_chat, actionForCommand(ids.rename_quick_chat)); - try std.testing.expectEqual(Action.delete_quick_chat, actionForCommand(ids.delete_quick_chat)); -} - -test "edge editing requires a stable edge identifier" { - try std.testing.expect(!canEditEdge("")); - try std.testing.expect(canEditEdge("edge-1")); -} - -test "context targets carry stable copied identity rather than collection indices" { - const node = NodeTarget{ .project_path = "C:\\work\\graph", .id = "node-a" }; - const edge = EdgeTarget{ .project_path = "C:\\work\\graph", .id = "edge-a" }; - try std.testing.expectEqualStrings("node-a", node.id); - try std.testing.expectEqualStrings("edge-a", edge.id); - try std.testing.expectEqualStrings("C:\\work\\graph", edge.project_path); -} - -test "project context commands expose ingress management and safe destructive actions" { - const target = ProjectTarget{ .path = "C:\\work\\graph", .remote = false }; - try std.testing.expectEqualStrings("C:\\work\\graph", target.path); - try std.testing.expectEqual(Action.open_project, actionForCommand(ids.open_project)); - try std.testing.expectEqual(Action.project_settings, actionForCommand(ids.project_settings)); - try std.testing.expectEqual(Action.remove_project, actionForCommand(ids.remove_project)); - try std.testing.expectEqual(Action.move_project, actionForCommand(ids.move_project)); - try std.testing.expectEqual(Action.trash_project, actionForCommand(ids.trash_project)); - try std.testing.expectEqual(Action.delete_project_loops, actionForCommand(ids.delete_project_loops)); -} - -test "project relocation is visibly unavailable rather than an Explorer alias" { - try std.testing.expect(!Wire.supportsProjectRelocation()); - try std.testing.expect(std.mem.indexOf( - u8, - Wire.project_relocation_unavailable_reason, - "authoritative moveProject command", - ) != null); -} - -test "the real Move Project menu item is disabled with its explicit reason inline" { - // This exercises moveProjectMenuItem() directly: the same function - // show() calls to append the actual Win32 popup entry, not a - // separate accessibility-only model. It fails the moment the item's - // command id, label, or enabled state drift from what the live - // context menu renders. - const item = moveProjectMenuItem(); - try std.testing.expectEqual(@as(usize, ids.move_project), item.id); - try std.testing.expectEqualStrings( - "Move Project... (unavailable: daemon support required)", - item.text, - ); - try std.testing.expect(!item.enabled); - try std.testing.expectEqual(Action.move_project, actionForCommand(@intCast(item.id))); -} +const c = @import("Win32.zig").c; +const Wire = @import("Wire.zig"); +const SketchPromotion = @import("SketchPromotion.zig"); + +pub const NodeTarget = struct { + project_path: []const u8, + id: []const u8, + composite: bool = false, + can_arm: bool = false, + unwired: bool = false, + follows_template: bool = false, + resolved: bool = false, + can_create_child: bool = true, + sketch: bool = false, + promotion_context: ?*const SketchPromotion.Context = null, +}; + +pub const BackgroundTarget = struct { + project_path: []const u8, + local_filesystem: bool, + can_create_edge: bool, +}; + +pub const EdgeTarget = struct { + project_path: []const u8, + id: []const u8, +}; + +pub const QuickChatTarget = struct { + id: []const u8, +}; + +pub const ProjectTarget = struct { + path: []const u8, + remote: bool, +}; + +pub const Target = union(enum) { + background: BackgroundTarget, + quick_chats, + project: ProjectTarget, + node: NodeTarget, + edge: EdgeTarget, + quick_chat: QuickChatTarget, +}; + +pub const Action = enum { + none, + edit_node, + promote_goal, + promote_turn, + promote_timed, + rename_node, + stop_node, + delete_node, + open_terminal, + new_child_node, + message_node, + memo_node, + open_composite, + pilot_composite, + arm_composite, + save_node_template, + detach_template, + wire_node, + mark_entry, + edit_edge, + delete_edge, + create_edge, + open_quick_chat, + rename_quick_chat, + delete_quick_chat, + open_project, + new_project_loop, + inspect_project_worktrees, + project_settings, + reveal_project, + remote_project_info, + close_project, + remove_project, + move_project, + trash_project, + delete_project_loops, + new_quick_chat, +}; + +pub const Callback = *const fn (?*anyopaque, Action, Target) void; + +pub fn requiresConfirmation(action: Action) bool { + return action == .delete_node or action == .delete_edge or action == .delete_quick_chat or + action == .remove_project or action == .trash_project or action == .delete_project_loops; +} + +pub fn shouldApply(action: Action, confirmed: bool) bool { + return !requiresConfirmation(action) or confirmed; +} + +pub fn canEditEdge(edge_id: []const u8) bool { + return edge_id.len != 0; +} + +const ids = struct { + const edit_node = 5100; + const rename_node = 5101; + const stop_node = 5102; + const delete_node = 5103; + const open_terminal = 5104; + const message_node = 5105; + const memo_node = 5106; + const open_composite = 5113; + const pilot_composite = 5107; + const arm_composite = 5108; + const save_node_template = 5114; + const detach_template = 5115; + const new_child_node = 5119; + const promote_goal = 5116; + const promote_turn = 5117; + const promote_timed = 5118; + const wire_node = 5109; + const mark_entry = 5112; + const edit_edge = 5110; + const delete_edge = 5111; + const create_edge = 5120; + const open_quick_chat = 5130; + const rename_quick_chat = 5131; + const delete_quick_chat = 5132; + const open_project = 5140; + const new_project_loop = 5141; + const inspect_project_worktrees = 5142; + const project_settings = 5143; + const reveal_project = 5144; + const remote_project_info = 5145; + const close_project = 5146; + const remove_project = 5147; + const move_project = 5149; + const trash_project = 5151; + const delete_project_loops = 5148; + const new_quick_chat = 5150; +}; + +pub const move_project_menu_text = "Move Project... (unavailable: daemon support required)"; + +pub const MoveProjectMenuItem = struct { + id: usize = ids.move_project, + text: []const u8 = move_project_menu_text, + enabled: bool, +}; + +/// Builds the real "Move Project..." popup item used by `show()` for the +/// project context menu. Exposed so tests can exercise the exact same +/// data the live Win32 menu is constructed from, rather than only a +/// separate accessibility contract model. +pub fn moveProjectMenuItem() MoveProjectMenuItem { + return .{ .enabled = Wire.supportsProjectRelocation() }; +} + +pub const NodeMenuItem = struct { + id: usize = 0, + text: []const u8 = "", + enabled: bool = true, +}; + +pub fn newChildNodeMenuItem(node: NodeTarget) ?NodeMenuItem { + if (node.resolved) return null; + return .{ .id = ids.new_child_node, .text = "New Child Node...", .enabled = node.can_create_child }; +} + +pub const NodeMenuPlan = struct { + items: [15]NodeMenuItem = undefined, + len: usize = 0, + + fn add(self: *NodeMenuPlan, item: NodeMenuItem) void { + self.items[self.len] = item; + self.len += 1; + } +}; + +pub fn nodeMenuPlan(node: NodeTarget) NodeMenuPlan { + var plan = NodeMenuPlan{}; + plan.add(.{ .id = ids.open_terminal, .text = "Open Terminal" }); + if (newChildNodeMenuItem(node)) |item| plan.add(item); + if (node.unwired) { + plan.add(.{ .id = ids.wire_node, .text = "Wire it up" }); + plan.add(.{ .id = ids.mark_entry, .text = "Mark as entry" }); + plan.add(.{}); + } + if (node.composite) { + plan.add(.{ .id = ids.open_composite, .text = "Open Group" }); + plan.add(.{ .id = ids.pilot_composite, .text = "Pilot Once" }); + plan.add(.{ .id = ids.arm_composite, .text = "Arm Schedule", .enabled = node.can_arm }); + plan.add(.{}); + } + plan.add(.{ .id = ids.edit_node, .text = "Edit Details..." }); + plan.add(.{ .id = ids.save_node_template, .text = "Save as Template..." }); + if (node.follows_template) plan.add(.{ .id = ids.detach_template, .text = "Detach from Template" }); + plan.add(.{ .id = ids.rename_node, .text = "Rename...\tF2" }); + if (!node.resolved) plan.add(.{ .id = ids.stop_node, .text = "Stop\tCtrl+S" }); + plan.add(.{ .id = ids.delete_node, .text = "Delete Loop...\tDelete" }); + return plan; +} + +pub const PromotionItem = struct { id: usize, text: []const u8, action: Action }; +const promotion_items = [_]PromotionItem{ + .{ .id = ids.promote_goal, .text = "Goal - asks for a done check", .action = .promote_goal }, + .{ .id = ids.promote_turn, .text = "Turn - asks where to pause", .action = .promote_turn }, + .{ .id = ids.promote_timed, .text = "Timed - asks for a cadence", .action = .promote_timed }, +}; + +pub fn promotionItems(node: NodeTarget) []const PromotionItem { + return if (node.sketch) &promotion_items else &.{}; +} + +pub fn promotionTarget(action: Action) ?SketchPromotion.Target { + return switch (action) { + .promote_goal => .goal, + .promote_turn => .turn, + .promote_timed => .timed, + else => null, + }; +} + +pub fn promotionEnabled(node: NodeTarget) bool { + return node.sketch and node.promotion_context != null; +} + +pub fn show( + parent: c.HWND, + target: Target, + x: i32, + y: i32, + context: ?*anyopaque, + callback: Callback, +) void { + const menu = buildMenu(target) orelse return; + defer _ = c.DestroyMenu(menu); + const command = c.TrackPopupMenu( + menu, + c.TPM_RETURNCMD | c.TPM_NONOTIFY | c.TPM_RIGHTBUTTON, + x, + y, + 0, + parent, + null, + ); + const action = actionForCommand(command); + if (action != .none) callback(context, action, target); +} + +fn buildMenu(target: Target) c.HMENU { + const menu = c.CreatePopupMenu() orelse return null; + switch (target) { + .background => |background| { + if (!std.mem.eql(u8, background.project_path, "graphcode://global")) { + appendEnabled(menu, ids.inspect_project_worktrees, "Worktrees...", background.local_filesystem); + appendEnabled(menu, ids.project_settings, "Project Settings...", background.local_filesystem); + appendEnabled(menu, ids.reveal_project, "Show in Explorer", background.local_filesystem); + separator(menu); + } + appendEnabled(menu, ids.create_edge, "Create Edge", background.can_create_edge); + }, + .quick_chats => append(menu, ids.new_quick_chat, "New Chat"), + .project => |project| { + append(menu, ids.open_project, "Open Project"); + append(menu, ids.new_project_loop, "New Loop...\tCtrl+N"); + separator(menu); + append(menu, ids.inspect_project_worktrees, "Worktrees..."); + append(menu, ids.project_settings, "Project Settings..."); + if (project.remote) + append(menu, ids.remote_project_info, "Remote Connection Info") + else + append(menu, ids.reveal_project, "Show in Explorer"); + separator(menu); + append(menu, ids.close_project, "Close Project"); + if (!project.remote) { + const move_item = moveProjectMenuItem(); + appendEnabled(menu, move_item.id, move_item.text, move_item.enabled); + append(menu, ids.trash_project, "Move to Recycle Bin..."); + } + append(menu, ids.remove_project, "Remove from GraphCode..."); + append(menu, ids.delete_project_loops, "Delete All Loops..."); + }, + .node => |node| { + const plan = nodeMenuPlan(node); + for (plan.items[0..plan.len]) |item| { + if (item.id == 0) separator(menu) else appendEnabled(menu, item.id, item.text, item.enabled); + if (item.id == ids.edit_node) { + const items = promotionItems(node); + if (items.len != 0) { + const submenu = c.CreatePopupMenu() orelse { + _ = c.DestroyMenu(menu); + return null; + }; + for (items) |promotion_item| appendEnabled(submenu, promotion_item.id, promotion_item.text, promotionEnabled(node)); + if (c.AppendMenuW(menu, c.MF_POPUP | c.MF_STRING, @intFromPtr(submenu), std.unicode.utf8ToUtf16LeStringLiteral("Promote to...").ptr) == 0) { + _ = c.DestroyMenu(submenu); + _ = c.DestroyMenu(menu); + return null; + } + } + } + } + }, + .edge => { + append(menu, ids.edit_edge, "Edit Edge..."); + append(menu, ids.delete_edge, "Delete Edge"); + }, + .quick_chat => { + append(menu, ids.open_quick_chat, "Open Chat"); + append(menu, ids.rename_quick_chat, "Rename...\tCtrl+Shift+Q"); + append(menu, ids.delete_quick_chat, "Delete Chat...\tCtrl+Shift+Delete"); + }, + } + return menu; +} + +pub fn confirm(parent: c.HWND, title: []const u8, message: []const u8) bool { + const title_wide = toWide(title) orelse return false; + defer std.heap.c_allocator.free(title_wide); + const message_wide = toWide(message) orelse return false; + defer std.heap.c_allocator.free(message_wide); + return c.MessageBoxW(parent, message_wide.ptr, title_wide.ptr, c.MB_ICONWARNING | c.MB_YESNO | c.MB_DEFBUTTON2) == c.IDYES; +} + +fn actionForCommand(command: c_int) Action { + return switch (command) { + ids.rename_node => .rename_node, + ids.stop_node => .stop_node, + ids.delete_node => .delete_node, + ids.open_terminal => .open_terminal, + ids.new_child_node => .new_child_node, + ids.edit_node => .edit_node, + ids.promote_goal => .promote_goal, + ids.promote_turn => .promote_turn, + ids.promote_timed => .promote_timed, + ids.open_composite => .open_composite, + ids.pilot_composite => .pilot_composite, + ids.arm_composite => .arm_composite, + ids.save_node_template => .save_node_template, + ids.detach_template => .detach_template, + ids.wire_node => .wire_node, + ids.mark_entry => .mark_entry, + ids.edit_edge => .edit_edge, + ids.delete_edge => .delete_edge, + ids.create_edge => .create_edge, + ids.open_quick_chat => .open_quick_chat, + ids.rename_quick_chat => .rename_quick_chat, + ids.delete_quick_chat => .delete_quick_chat, + ids.open_project => .open_project, + ids.new_project_loop => .new_project_loop, + ids.inspect_project_worktrees => .inspect_project_worktrees, + ids.project_settings => .project_settings, + ids.reveal_project => .reveal_project, + ids.remote_project_info => .remote_project_info, + ids.close_project => .close_project, + ids.remove_project => .remove_project, + ids.move_project => .move_project, + ids.trash_project => .trash_project, + ids.delete_project_loops => .delete_project_loops, + ids.new_quick_chat => .new_quick_chat, + else => .none, + }; +} + +fn append(menu: c.HMENU, id: usize, text: []const u8) void { + appendEnabled(menu, id, text, true); +} + +fn appendEnabled(menu: c.HMENU, id: usize, text: []const u8, enabled: bool) void { + const wide = toWide(text) orelse return; + defer std.heap.c_allocator.free(wide); + var flags: c.UINT = @intCast(c.MF_STRING); + if (!enabled) flags |= @intCast(c.MF_GRAYED); + _ = c.AppendMenuW(menu, flags, id, wide.ptr); +} + +fn separator(menu: c.HMENU) void { + _ = c.AppendMenuW(menu, c.MF_SEPARATOR, 0, null); +} + +fn toWide(text: []const u8) ?[]u16 { + const raw = std.unicode.utf8ToUtf16LeAlloc(std.heap.c_allocator, text) catch return null; + const result = std.heap.c_allocator.alloc(u16, raw.len + 1) catch { + std.heap.c_allocator.free(raw); + return null; + }; + @memcpy(result[0..raw.len], raw); + result[raw.len] = 0; + std.heap.c_allocator.free(raw); + return result; +} + +const std = @import("std"); + +test "custody child menu plan is unresolved-only and uses its reserved command" { + const target = NodeTarget{ .project_path = "B", .id = "11111111-1111-4111-8111-111111111111" }; + const item = newChildNodeMenuItem(target).?; + try std.testing.expectEqual(@as(usize, 5119), item.id); + try std.testing.expectEqualStrings("New Child Node...", item.text); + try std.testing.expect(item.enabled); + try std.testing.expectEqual(Action.new_child_node, actionForCommand(@intCast(item.id))); + var resolved = target; + resolved.resolved = true; + try std.testing.expect(newChildNodeMenuItem(resolved) == null); + try std.testing.expectEqual(Action.none, actionForCommand(0)); +} + +test "custody child node menu plan preserves existing items when creation is unavailable" { + const expected_unresolved = [_]usize{ 5104, 5119, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5102, 5103 }; + const expected_resolved = [_]usize{ 5104, 5109, 5112, 0, 5113, 5107, 5108, 0, 5100, 5114, 5115, 5101, 5103 }; + for ([_]bool{ false, true }) |resolved| { + const plan = nodeMenuPlan(.{ + .project_path = "B", + .id = "parent", + .composite = true, + .unwired = true, + .follows_template = true, + .resolved = resolved, + .can_create_child = false, + }); + const expected: []const usize = if (resolved) &expected_resolved else &expected_unresolved; + try std.testing.expectEqual(expected.len, plan.len); + for (plan.items[0..plan.len], expected) |item, id| { + try std.testing.expectEqual(id, item.id); + try std.testing.expectEqual(id != 5119 and id != 5108, item.enabled); + } + try std.testing.expectEqualStrings("Open Terminal", plan.items[0].text); + try std.testing.expectEqualStrings("Delete Loop...\tDelete", plan.items[plan.len - 1].text); + } +} + +test "sketch promotion real menu plan exposes only three eligible target actions" { + var node = NodeTarget{ .project_path = "B", .id = "id" }; + try std.testing.expectEqual(@as(usize, 0), promotionItems(node).len); + node.sketch = true; + try std.testing.expectEqual(@as(usize, 3), promotionItems(node).len); + try std.testing.expect(!promotionEnabled(node)); + const context = SketchPromotion.Context{}; + node.promotion_context = &context; + try std.testing.expect(promotionEnabled(node)); + for (promotionItems(node), [_]SketchPromotion.Target{ .goal, .turn, .timed }) |item, target| { + try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); + try std.testing.expectEqual(target, promotionTarget(item.action).?); + } + for (std.enums.values(Action)) |action| { + if (action != .promote_goal and action != .promote_turn and action != .promote_timed) + try std.testing.expect(promotionTarget(action) == null); + } +} + +test "sketch promotion production popup installs native Goal Turn Timed submenu on hidden HWND" { + const hwnd = c.CreateWindowExW( + 0, + std.unicode.utf8ToUtf16LeStringLiteral("STATIC"), + std.unicode.utf8ToUtf16LeStringLiteral("Promotion menu test"), + c.WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + null, + null, + c.GetModuleHandleW(null), + null, + ) orelse return error.WindowCreationFailed; + defer _ = c.DestroyWindow(hwnd); + + const context = SketchPromotion.Context{}; + const cases = [_]struct { sketch: bool, context: ?*const SketchPromotion.Context, enabled: bool }{ + .{ .sketch = false, .context = null, .enabled = false }, + .{ .sketch = true, .context = null, .enabled = false }, + .{ .sketch = true, .context = &context, .enabled = true }, + }; + for (cases) |case| { + const popup = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "sketch-a", + .sketch = case.sketch, + .promotion_context = case.context, + } }) orelse return error.MenuCreationFailed; + const bar = c.CreateMenu() orelse { + _ = c.DestroyMenu(popup); + return error.MenuCreationFailed; + }; + defer { + _ = c.SetMenu(hwnd, null); + _ = c.DestroyMenu(bar); + } + if (c.AppendMenuW(bar, c.MF_POPUP | c.MF_STRING, @intFromPtr(popup), std.unicode.utf8ToUtf16LeStringLiteral("Node").ptr) == 0) { + _ = c.DestroyMenu(popup); + return error.MenuInstallFailed; + } + if (c.SetMenu(hwnd, bar) == 0) return error.MenuInstallFailed; + const menu = c.GetSubMenu(c.GetMenu(hwnd), 0); + try std.testing.expect(menu != null); + var edit_position: c_int = 0; + while (edit_position < c.GetMenuItemCount(menu) and c.GetMenuItemID(menu, edit_position) != ids.edit_node) : (edit_position += 1) {} + try std.testing.expect(edit_position < c.GetMenuItemCount(menu)); + const submenu = c.GetSubMenu(menu, edit_position + 1); + if (!case.sketch) { + try std.testing.expect(submenu == null); + for ([_]c.UINT{ 5116, 5117, 5118 }) |id| + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); + continue; + } + try std.testing.expect(submenu != null); + var title: [64]u16 = undefined; + const title_len = c.GetMenuStringW(menu, @intCast(edit_position + 1), &title, title.len, c.MF_BYPOSITION); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Promote to..."), title[0..@intCast(title_len)]); + try std.testing.expectEqual(@as(c_int, 3), c.GetMenuItemCount(submenu)); + for ([_]struct { id: c.UINT, label: []const u16, action: Action }{ + .{ .id = 5116, .label = std.unicode.utf8ToUtf16LeStringLiteral("Goal - asks for a done check"), .action = .promote_goal }, + .{ .id = 5117, .label = std.unicode.utf8ToUtf16LeStringLiteral("Turn - asks where to pause"), .action = .promote_turn }, + .{ .id = 5118, .label = std.unicode.utf8ToUtf16LeStringLiteral("Timed - asks for a cadence"), .action = .promote_timed }, + }, 0..) |item, index| { + try std.testing.expectEqual(item.id, c.GetMenuItemID(submenu, @intCast(index))); + const state = c.GetMenuState(submenu, item.id, c.MF_BYCOMMAND); + try std.testing.expect(state != std.math.maxInt(c.UINT)); + try std.testing.expectEqual(case.enabled, state & c.MF_GRAYED == 0); + var label: [64]u16 = undefined; + const length = c.GetMenuStringW(submenu, item.id, &label, label.len, c.MF_BYCOMMAND); + try std.testing.expectEqualSlices(u16, item.label, label[0..@intCast(length)]); + try std.testing.expectEqual(item.action, actionForCommand(@intCast(item.id))); + } + } +} + +test "background menu exposes supported folder actions and gates edge creation" { + const menu = buildMenu(.{ .background = .{ + .project_path = "C:\\fixture", + .local_filesystem = true, + .can_create_edge = false, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.inspect_project_worktrees, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.project_settings, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.reveal_project, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(menu, ids.create_edge, c.MF_BYCOMMAND)); +} + +test "resolved node menu hides Stop but retains rename and edit details" { + const menu = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-a", + .resolved = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.stop_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.rename_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.message_node, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.memo_node, c.MF_BYCOMMAND)); +} + +test "node shortcut captions keep Open Terminal without a standalone Enter binding" { + const InputRouter = @import("InputRouter.zig"); + try std.testing.expectEqual(InputRouter.Action.none, InputRouter.keyAction(c.VK_RETURN, false, false)); + try std.testing.expectEqual(InputRouter.HeaderKey.none, InputRouter.headerKey(c.VK_RETURN, false, false, false, false)); + try std.testing.expectEqual(InputRouter.HeaderKey.activate, InputRouter.headerKey(c.VK_RETURN, false, false, false, true)); + try std.testing.expectEqual(Action.open_terminal, actionForCommand(ids.open_terminal)); + + const targets = [_]NodeTarget{ + .{ .project_path = "C:\\fixture", .id = "ordinary" }, + .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, + .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, + .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, + }; + for (targets) |target| { + const menu = buildMenu(.{ .node = target }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, ids.open_terminal), c.GetMenuItemID(menu, 0)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.open_terminal, c.MF_BYCOMMAND)); + var caption: [128]u16 = undefined; + const length = c.GetMenuStringW(menu, ids.open_terminal, &caption, caption.len, c.MF_BYCOMMAND); + try std.testing.expect(length > 0 and length < caption.len - 1); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Open Terminal"), caption[0..@intCast(length)]); + } +} + +test "node shortcut captions keep Edit Details without the rename Ctrl E hint" { + const InputRouter = @import("InputRouter.zig"); + try std.testing.expectEqual(InputRouter.Action.edit_node, InputRouter.keyAction('E', true, false)); + try std.testing.expectEqual(InputRouter.Action.rename_selected, InputRouter.keyAction(c.VK_F2, false, false)); + try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); + try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); + + const targets = [_]struct { node: NodeTarget, edit_position: c_int }{ + .{ .node = .{ .project_path = "C:\\fixture", .id = "ordinary" }, .edit_position = 2 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "resolved", .resolved = true }, .edit_position = 1 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "composite", .composite = true }, .edit_position = 6 }, + .{ .node = .{ .project_path = "C:\\fixture", .id = "unwired", .unwired = true }, .edit_position = 5 }, + }; + for (targets) |target| { + const menu = buildMenu(.{ .node = target.node }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, ids.edit_node), c.GetMenuItemID(menu, target.edit_position)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, ids.edit_node, c.MF_BYCOMMAND)); + var caption: [128]u16 = undefined; + const length = c.GetMenuStringW(menu, ids.edit_node, &caption, caption.len, c.MF_BYCOMMAND); + try std.testing.expect(length > 0 and length < caption.len - 1); + try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("Edit Details..."), caption[0..@intCast(length)]); + } +} + +test "background menu disables unavailable folder actions and omits them for global scope" { + const remote = buildMenu(.{ .background = .{ + .project_path = "ssh://builder/fixture", + .local_filesystem = false, + .can_create_edge = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(remote); + for ([_]c.UINT{ ids.inspect_project_worktrees, ids.project_settings, ids.reveal_project }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_GRAYED), c.GetMenuState(remote, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(remote, ids.create_edge, c.MF_BYCOMMAND)); + + const global = buildMenu(.{ .background = .{ + .project_path = "graphcode://global", + .local_filesystem = false, + .can_create_edge = false, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(global); + try std.testing.expectEqual(@as(c_int, 1), c.GetMenuItemCount(global)); + try std.testing.expectEqual(@as(c.UINT, ids.create_edge), c.GetMenuItemID(global, 0)); +} + +test "native node menu variants expose only eligible actions" { + for ([_]bool{ false, true }) |can_arm| { + const menu = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-b", + .composite = true, + .can_arm = can_arm, + .follows_template = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c.UINT, if (can_arm) c.MF_ENABLED else c.MF_GRAYED), c.GetMenuState(menu, ids.arm_composite, c.MF_BYCOMMAND)); + for ([_]c.UINT{ ids.open_composite, ids.pilot_composite, ids.stop_node, ids.detach_template }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(menu, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(menu, ids.wire_node, c.MF_BYCOMMAND)); + } + const unwired = buildMenu(.{ .node = .{ + .project_path = "C:\\fixture", + .id = "node-c", + .unwired = true, + } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(unwired); + for ([_]c.UINT{ ids.wire_node, ids.mark_entry, ids.stop_node }) |id| + try std.testing.expectEqual(@as(c.UINT, c.MF_ENABLED), c.GetMenuState(unwired, id, c.MF_BYCOMMAND)); + try std.testing.expectEqual(std.math.maxInt(c.UINT), c.GetMenuState(unwired, ids.arm_composite, c.MF_BYCOMMAND)); +} + +test "edge menu preserves edit and delete command ordering" { + const menu = buildMenu(.{ .edge = .{ .project_path = "C:\\fixture", .id = "edge-a" } }) orelse return error.MenuCreationFailed; + defer _ = c.DestroyMenu(menu); + try std.testing.expectEqual(@as(c_int, 2), c.GetMenuItemCount(menu)); + try std.testing.expectEqual(@as(c.UINT, ids.edit_edge), c.GetMenuItemID(menu, 0)); + try std.testing.expectEqual(@as(c.UINT, ids.delete_edge), c.GetMenuItemID(menu, 1)); +} + +test "context actions remain stable when graph IDs are reordered" { + try std.testing.expectEqual(Action.edit_node, actionForCommand(ids.edit_node)); + try std.testing.expectEqual(Action.rename_node, actionForCommand(ids.rename_node)); + try std.testing.expectEqual(Action.delete_edge, actionForCommand(ids.delete_edge)); + try std.testing.expectEqual(Action.none, actionForCommand(0)); + try std.testing.expectEqual(Action.pilot_composite, actionForCommand(ids.pilot_composite)); + try std.testing.expectEqual(Action.open_composite, actionForCommand(ids.open_composite)); + try std.testing.expectEqual(Action.arm_composite, actionForCommand(ids.arm_composite)); + try std.testing.expectEqual(Action.wire_node, actionForCommand(ids.wire_node)); + try std.testing.expectEqual(Action.mark_entry, actionForCommand(ids.mark_entry)); + try std.testing.expectEqual(Action.save_node_template, actionForCommand(ids.save_node_template)); + try std.testing.expectEqual(Action.detach_template, actionForCommand(ids.detach_template)); +} + +test "destructive context actions cannot bypass a cancelled confirmation" { + try std.testing.expect(!shouldApply(.delete_node, false)); + try std.testing.expect(!shouldApply(.delete_edge, false)); + try std.testing.expect(!shouldApply(.delete_quick_chat, false)); + try std.testing.expect(!shouldApply(.remove_project, false)); + try std.testing.expect(!shouldApply(.trash_project, false)); + try std.testing.expect(!shouldApply(.delete_project_loops, false)); + try std.testing.expect(shouldApply(.rename_node, false)); +} + +test "quick chat context targets preserve stable identity" { + const target = QuickChatTarget{ .id = "chat-a" }; + try std.testing.expectEqualStrings("chat-a", target.id); + try std.testing.expectEqual(Action.open_quick_chat, actionForCommand(ids.open_quick_chat)); + try std.testing.expectEqual(Action.rename_quick_chat, actionForCommand(ids.rename_quick_chat)); + try std.testing.expectEqual(Action.delete_quick_chat, actionForCommand(ids.delete_quick_chat)); +} + +test "edge editing requires a stable edge identifier" { + try std.testing.expect(!canEditEdge("")); + try std.testing.expect(canEditEdge("edge-1")); +} + +test "context targets carry stable copied identity rather than collection indices" { + const node = NodeTarget{ .project_path = "C:\\work\\graph", .id = "node-a" }; + const edge = EdgeTarget{ .project_path = "C:\\work\\graph", .id = "edge-a" }; + try std.testing.expectEqualStrings("node-a", node.id); + try std.testing.expectEqualStrings("edge-a", edge.id); + try std.testing.expectEqualStrings("C:\\work\\graph", edge.project_path); +} + +test "project context commands expose ingress management and safe destructive actions" { + const target = ProjectTarget{ .path = "C:\\work\\graph", .remote = false }; + try std.testing.expectEqualStrings("C:\\work\\graph", target.path); + try std.testing.expectEqual(Action.open_project, actionForCommand(ids.open_project)); + try std.testing.expectEqual(Action.project_settings, actionForCommand(ids.project_settings)); + try std.testing.expectEqual(Action.remove_project, actionForCommand(ids.remove_project)); + try std.testing.expectEqual(Action.move_project, actionForCommand(ids.move_project)); + try std.testing.expectEqual(Action.trash_project, actionForCommand(ids.trash_project)); + try std.testing.expectEqual(Action.delete_project_loops, actionForCommand(ids.delete_project_loops)); +} + +test "project relocation is visibly unavailable rather than an Explorer alias" { + try std.testing.expect(!Wire.supportsProjectRelocation()); + try std.testing.expect(std.mem.indexOf( + u8, + Wire.project_relocation_unavailable_reason, + "authoritative moveProject command", + ) != null); +} + +test "the real Move Project menu item is disabled with its explicit reason inline" { + // This exercises moveProjectMenuItem() directly: the same function + // show() calls to append the actual Win32 popup entry, not a + // separate accessibility-only model. It fails the moment the item's + // command id, label, or enabled state drift from what the live + // context menu renders. + const item = moveProjectMenuItem(); + try std.testing.expectEqual(@as(usize, ids.move_project), item.id); + try std.testing.expectEqualStrings( + "Move Project... (unavailable: daemon support required)", + item.text, + ); + try std.testing.expect(!item.enabled); + try std.testing.expectEqual(Action.move_project, actionForCommand(@intCast(item.id))); +} diff --git a/investigation/ui-parity-matrix.md b/investigation/ui-parity-matrix.md index 6ed5f39a..dc6cf715 100644 --- a/investigation/ui-parity-matrix.md +++ b/investigation/ui-parity-matrix.md @@ -1,213 +1,213 @@ -# Windows UI parity ledger - -This is a source-derived completion ledger, not a requirements sketch. A row is -`Validated` only when the Windows implementation exposes the same user-visible -information and actions as macOS and has runtime evidence. Platform-native chrome may -differ, but hiding a feature behind an undocumented shortcut or replacing a structured -screen with raw protocol fields is not parity. - -Statuses: - -- `Validated`: source mapping, automated coverage, and live walkthrough agree. -- `Partial`: some behavior exists, but visible controls, state, or interaction is absent - or materially different. -- `Missing`: no equivalent reachable Windows surface. -- `Blocked`: requires a deliberate platform decision or unavailable dependency. -- `Divergent`: Windows exposes a different product concept in the place where the macOS - surface belongs; it must be separated or redesigned before parity. - -## Application shell and navigation - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Main split view | Persistent sidebar; detail switches among welcome, global graph, project canvas, Quick Chats canvas, and loop workspace | Explicit destinations exist; the earlier stub walkthrough covered project → overview → workspace → Show in Graph with sidebar retention. The shared header now follows destination identity, distinguishes a Quick Chat workspace from a previously selected project, and keeps detail-panel toggling inside the loop workspace. Production-state and layout tests cover the destinations, narrow widths, and sidebar/panel independence. The current live attempt stopped at foreground acquisition before UIA root access: no new live UIA SetFocus, F6, Jump, pixels, or sidebar-effect proof was obtained, and provider-backed workspace/panel behavior remains unverified | Partial | -| Window toolbar | Needs-you chip, worktree notice, jump field, contextual loop-panel toggle | Render, hit-test, UIA exposure, and focus order share header visibility/layout. Needs-you opens the selected attention target; cached local worktree notices honor the owning project and count/size threshold. Jump opens the existing palette even with no projects. The detail toggle is limited to loop workspaces with supported edge/metric content; workspace/project identity is no longer overpainted. F6 entry/exit, scoped traversal/activation, focus rendering, and marshaled UIA button focus are implemented, while ordinary Tab/Shift+Tab/Ctrl+Tab contracts remain. The header paints inside the buffered logical pass; literal 96/144/192-DPI bounds and hidden native client tests cover logical layout/input and exactly-once physical UIA conversion without rescaling terminal tabs. Production-helper tests, a hidden native Tab-to-command test, and a hidden empty-palette create/cancel test pass. Live focus/event-deadlock, keyboard activation, rendered pixels, sidebar effects, and provider-backed panel proof are still absent because foreground acquisition failed. Cross-project worktree-notice discovery/aggregation, summary/board/mailroom panel content, and macOS visual parity remain separate residuals; private live tooling is not CI coverage | Partial | -| Jump palette | Search field, ranked cross-project results, type/state/project context, mouse and keyboard selection | Ctrl+P and Ctrl+J open a native modal palette with live exact-ID, exact-title, title-prefix, and substring ranking across projects. Results visibly include project, loop type, and state; Up/Down, Return, Escape, and mouse double-click are supported. The deterministic UIA gate verifies a visible search field, contextual cross-project results, and keyboard navigation changing the selected loop. | Validated | -| File/Loop/Terminal menus | Discoverable project, worktree, navigation, workspace, update, settings, and help commands with state-aware enablement | Startup menu replacement and UTF-16 corruption are fixed and the five readable runtime groups were probed. Reclaim/Reveal require a selected row (`worktreeRowSelected()` retains dedicated unit coverage); Save requires the Worktrees dialog open. Loop commands now gray out when they have no actionable target: Jump requires a loaded loop anywhere, Next/Previous require at least two loops or one unselected loop in the active graph, Create Edge requires two active-graph loops, and Stop requires a selected loop. These distinct thresholds follow their different production handlers and are intentional. Terminal Next/Previous Tab and Focus Next/Previous Pane require multiple tabs or panes in the selected tab. A hidden-window test calls `MainWindow.updateMenu` and verifies the real native HMENU `MF_GRAYED` bits in both unavailable and available states; this is unit/runtime Win32-menu evidence, not a live application walkthrough. The Worktrees-dialog UIA gate evidence remains outstanding: obtaining it requires launching the shell in a foreground desktop session, and no application executable is built in this worktree; provider builds/provisioning and foreground-gated UIA automation were not performed. Broader project-management parity remains incomplete | Partial | -| Workspace lifecycle | List/switch, create, rename, delete with fail-closed confirmation, and keyboard/UIA reachability | Windows discovers `Default` plus `.graphcode-*` siblings and exposes New/Rename/Delete and Ctrl+Alt paging. Manage now has an owned row model/native list, Default/creation-time/name-tie order, identity-deduplicated current-outside-home inclusion, full-path detail, current/default/open/uncertain-window refusals, and captured-identity Open/New/Rename handoff after modal teardown. An App-owned single joinable reader supplies bounded **saved top-level** graph-header counts, not live/descendant totals; current live content, missing/unreadable/invalid/duplicate/limited/unsupported-location data remain explicitly unavailable, not partial zero. Fixed local/non-reparse reads use existing project JSON and the documented mailroom-array exception, no daemon/backend or window activation. Done/Escape cancels; pending actions wait for reader cancellation/join while the modal pumps, and shutdown drains before allocator teardown (a stalled local disk can delay shutdown). Manager Delete is disabled pending recoverable deletion/teardown; existing menu deletion and ordinary menu ordering/Open/New semantics are unchanged. Next/Previous now reread the manager-ordered list, include validated current outside home, filter identified running windows, wrap both ways, recheck the target, and use a restore-only API with no launcher. Closed/disappeared targets never cold-open; any unidentified flag or lookup/restore failure refuses with status. Existing SID/session/class/published-metadata checks are preserved, not executable-path attestation. Menu capability counts known rows plus implicit current, without periodic window polling. Injected production-helper RED/GREEN, allocation-failure/owned-refresh, ordering/dedup/offset/race/refusal tests and pure menu-capability tests execute in the existing App/MainWindow roots; full native roots compile/link without execution and the ReleaseSafe app builds without launch. Manager coverage remains pure owned-data/explicit disposable fixtures, controlled memory-only joined threads, and filtered form/App data seams; no new shown manager/native-control/window-lookup/UIA/runtime proof is claimed. Existing lifecycle helper and never-shown native-control regressions cover accepted text lifetime, allocation cleanup, normalized naming/collisions, lexical identity, exact-window and one-launch routing, Default/current/open refusals, child-only support/daemon-pipe isolation, canonical plus legacy reservations, unidentified-window refusal, fail-closed Settings rebinding, and disposable confirmed mutation preservation. Lexical identity is not junction equivalence. Shown-dialog accessibility/keyboard/layout, full multi-instance/save-reload behavior, reliable live totals, real running-cycle keyboard/window proof, and recoverable deletion with session/daemon teardown remain residuals. Shared-host menu failures still have an unproven cause; no full lifecycle parity or host-only diagnosis is claimed | Partial | -| Help menu | GraphCode Basics and normal About entry | The live Help menu exposes GraphCode Basics, which reopens onboarding, and About GraphCode, which opens a native versioned product dialog. The populated UIA gate verifies the dialog identity, version text, and close behavior | Validated | -| Update command | Check for Updates, disabled while checking/installing | Reachable from the Help menu's native menu bar, immediately reports "Checking for updates..." status and disables the command the instant a check is invoked, and — after fixing a real bug where the background check's completion never refreshed the menu bit because the only refresh path was gated on unrelated daemon-connectivity state — now reliably re-enables the command once the check settles. A dedicated live UIA gate assertion (reading the real native `HMENU` bit via `GetMenuState`, not the fragile UIA tree) and a Win32-window-backed unit test both confirm the disable/re-enable cycle. When the settled result is an available update, the gate additionally waits for the real offer, dismisses it via Later, verifies the modal disappears, and requires the shell owner to be enabled again before continuing; failure/no-update outcomes are required not to present an offer. In-app installation is now implemented as a separate scope (see Install progress/Relaunch prompt, Partial) | Validated | -| Tray lifecycle | Restore and exit without foreground daemon window | `TrayLive.Tests.ps1` exercises the physical icon, Open, close-to-hide, single-instance restore, Explorer recovery, popup contents, and visible Exit activation | Validated | -| Connection failure presentation | Explicit visible failure without replacing normal navigation | A persistent inline canvas banner now reports daemon unavailability while leaving sidebar and destination navigation intact; ingress errors take precedence when present. The live UIA gate forces the disconnected state and verifies the dedicated banner text and bounds | Validated | - -Running-cycle follow-up evidence: the final lookup actually used for activation -now refuses mixed identified/unidentified results before activating that same -target; ordinary Open keeps its target-first policy. The real accelerator -descriptor and eligible pretranslation/top-level fallback now wire Ctrl+Alt -paging to the same cycle action before terminal-child dispatch. Pure injected -final-lookup, descriptor/modifier, and message-data tests cover these paths and -preserve Ctrl-only tabs/F6/F10 routing. The corrected native menu regression is -compiled, not locally executed. This is not an atomic global-window snapshot or -native keyboard/accelerator/window proof; the workspace row remains Partial. - -## First-run and empty states - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Four-page onboarding | Visual terminology tour, Skip/Back/Continue/Get Started, backend selection, reopen, persisted seen state | Custom rounded Win32 onboarding; all pages exercised and persistence verified | Validated | -| No-project Welcome detail | Graph icon, pitch, explanatory copy, Open Folder action, inline error | Windows matches the centered Graph identity, pitch, explanatory copy, and single Open Folder action. Persistent project-ingress failures now also render as a bounded, wrapped inline canvas alert without replacing navigation; focused geometry coverage and the populated UIA gate verify the alert text and live bounds | Validated | -| Empty global graph | “Nothing running yet”, explanatory copy, Open Folder action, New Loop action | The dedicated overview empty state exposes both bounded Open Folder and New Loop actions; New Loop targets the daemon's `graphcode://global` project. The live UIA gate switches to an empty model, verifies both visible native controls, invokes New Loop, and observes the node form | Validated | -| Empty project canvas | Project-specific empty message and New Loop action | The dedicated “No loops yet” project state exposes its visible New Loop action. The live UIA gate installs an empty local project, invokes that exact command, and observes the project-scoped node form | Validated | -| Empty Quick Chats canvas | Explanation of Quick Chats and New Chat action | Live walkthrough verified the dedicated explanation and New Chat action with corrected non-overlapping layout | Validated | - -## Sidebar - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Graph row | Pinned global graph row with graph glyph | The sidebar now keeps a dedicated Graph destination visible even with no open project, labels it with a graph identity glyph, and routes it through the existing global-overview hit target and UIA destination | Validated | -| Quick Chats group | Selectable header, hover New Chat, disclosure, child rows | The native header remains selectable, reveals a hover-only New Chat action and disclosure, and exposes stable selectable child rows with Rename/Delete context actions. Focused menu tests cover stable chat identity; the live UIA gate invokes New Chat, collapses and restores children, and verifies child runtime identity survives. | Validated | -| Local/remote sections | Group labels, independent collapse, folder/network glyphs | LOCAL and REMOTE retain local/folder and remote/network identity and now toggle independently as native section actions. Focused layout coverage validates mixed ordering, and the live UIA gate collapses LOCAL while proving the REMOTE row and its stable automation identity remain present before restoring LOCAL. | Validated | -| Project rows | Selection, folder type, hover New Loop, disclosure | Open project rows retain selection and local/remote glyphs, reveal hover-only New Loop and disclosure controls, and collapse/restore their own loop tree without changing row identity. The live UIA gate invokes the project-row New Loop action into the real native node form and exercises project collapse/expand through stable UIA actions. | Validated | -| Nested loop tree | Edge-derived hierarchy, persisted expansion, drag reorder of roots | Handoff edges derive a cycle-safe root/descendant tree; nested rows disclose and collapse by stable node ID, expanded IDs persist atomically in the GraphCode support directory, and root rows now reorder through live pointer drag backed by the existing transactional `root` records. Focused Sidebar/Wire coverage and the deterministic UIA gate verify observable reorder plus emission of the new `sidebarNodesReordered` daemon command for server-side persistence parity. | Validated | -| Loop row presentation | Type stripe, title, elapsed time, state indicator | Rows now show a loop-type stripe, title, compact state indicator, and a compact elapsed value derived from `createdAt`. `Sidebar.elapsedText` now has focused boundary coverage for every unit rollover (seconds/minutes/hours/days) and its invalid-input guards (`created_at<=0`, `now<=created_at`), reverified via `zig test` and the full `WindowsShell.Tests.ps1` suite. This is still text painted directly onto the sidebar's `HDC` with no UIA identity of its own (the same limitation `Sidebar.updateBannerAt` had before this change), so a live assertion that reads the *rendered pixels* of the elapsed column was not captured this session; only the formatting logic and the row's overall live-rendering-without-crashing are executable evidence today | Partial | -| Project context menu | Move, worktrees, settings, Explorer, remote info, close, remove, delete loops/project | Project rows expose the lifecycle actions plus the Windows Recycle Bin path for local folders. Move is deliberately **not** an Explorer `/select` alias: `GraphContextMenu.moveProjectMenuItem()` appends "Move Project... (unavailable: daemon support required)" with `MF_GRAYED` while `Wire.supportsProjectRelocation()` is false, and the stale command path surfaces that explicit reason instead of opening Explorer. The live UIA gate now drives the real `TrackPopupMenu` popup (`MainWindow.wm_uia_context_menu` -> the same `GraphContextMenu.show()` the mouse path calls) and asserts the live menu's ordered items, that Move is command 5149 with that exact text and a disabled state, that a remote project's menu omits Move/Recycle Bin/Explorer entirely, and that the popup dismisses without wedging the shell. Note the observation channel: a popup menu appears in the UIA tree only as an empty Pane with no `MenuItem` children, so item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, but not UIA-tree evidence. | Validated | -| Loop context menu | Open, composite actions, rename, stop, delete | Sidebar and canvas loop rows share stable-ID Open, Rename, Stop, and Delete actions. Composite cards expose Open Group, Pilot Once, and Arm Schedule; the drilled-in canvas addresses mutations through the parent composite. The live UIA gate now opens and reads all three `wm_uia_context_menu` loop variants: target 3 (a plain wired loop) asserts Open/Rename/Stop/Delete are present and that every composite-only and unwired-only command is absent; target 6 (a composite, not-yet-piloted loop) asserts Open Group/Pilot Once/Arm Schedule are present with Arm Schedule rendered `MF_GRAYED` (not piloted), and that unwired-only commands are absent; target 7 (an unwired loop node, added live via the sidebar-reorder fixture mutation) asserts Wire it up/Mark as entry are present and composite-only commands are absent. As with the project context menu, item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, not UIA-tree evidence | Validated | -| Recent projects | Reachable from Add Folder menu | Recent and currently-open projects are now exposed as distinct sidebar rows, with unopened recents remaining under the LOCAL/REMOTE sections while open workspaces use separate `open-project` identities. The deterministic UIA gate verifies the split presentation and section behavior. The live gate now also walks the Recent Folders submenu reachable from Add Folder end-to-end: it sends a real `WM_INITMENUPOPUP` (the message `App.zig` uses to refresh `recent_folders` from the live model before a popup shows, so the read reflects the fixture's recent projects rather than pre-fixture placeholder state), reads the submenu's live items in fixture order with their stable `recent_folder_command_base`-derived command IDs, and invokes the second entry through the real `WM_COMMAND` route, confirming the shell routes it without crashing | Validated | -| Add Folder menu | Open Folder, Clone, Add Remote, recents | File now groups Open Folder, Clone Repository, Add Remote Repository, and Add Codespace under Add Folder and adds a dedicated Recent Folders submenu with its own command range; that submenu is now located rather than positionally indexed, so a new ingress entry can no longer silently retarget the rebuild. Focused MainWindow coverage validates the native menu structure and recent-folder command wiring. The live UIA gate now reads the Add Folder submenu directly off the live `HMENU` (`GetMenu`/`GetSubMenu`, not `TrackPopupMenu`, since this is the persistent menu bar) and asserts all four action labels plus the Recent Folders submenu; see the Recent projects row above for the live Recent Folders walkthrough this shares | Validated | -| Sidebar update banner | Available version and click-to-install action | A persistent footer banner now shows the retained offered version and reopens the native update offer when clicked. The live UIA gate now drives this through the real click path rather than the `GRAPHCODE_UIA_SHOW_UPDATE` bypass: since `Sidebar.updateBannerRect` has no UIA identity of its own, the gate computes the banner's live pixel geometry from the shell's real client height (matching the same viewport-bottom formula the paint code uses) and posts a genuine synthetic `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at that point, then verifies the real `GraphCode Update Available` dialog opens with the offered `GraphCode 9.9.9-test` version text, dismisses it via the live Later command, and confirms the shell survives. **In-app install remains deliberately Blocked** — the offer still hands installation off to the verified release page, and this row's evidence does not claim otherwise | Validated | -| Sidebar error footer | Persistent, scoped project-ingress error | Folder, clone, remote, and daemon-open failures now persist in a dedicated red sidebar footer independently of transient status. Successful project ingress clears it, wrapped layout preserves long messages, and the deterministic UIA gate verifies the dedicated footer identity plus multi-line bounds below the update offer. | Validated | -| Needs-you section | Navigable list with reason/project and Stop action | Up to four entries now expose selection, explicit reason copy, stable UIA identities, click/UIA navigation, and a dedicated Stop action. Focused routing coverage plus the deterministic UIA gate verify Stop targets the populated entry's real project path and loop ID. | Validated | -| Activity strip | Optional bottom strip, summary, attention-only filter, horizontally scrolling actionable events | Activity events retain project/node identity and timestamps, render timestamped cards, expose stable UIA rows plus scroll controls, and now keep a real horizontal viewport with an attention-only filter. Focused Sidebar coverage and the deterministic UIA gate verify scroll-state changes, attention-only filtering, and card navigation into the selected loop workspace. | Validated | - -## Graph overview and project canvas - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Cross-project global graph | Every open folder as a lane on one canvas | Existing lanes stack vertically, as on macOS. Production geometry tests now use four Alpha loops and two Beta loops, literal accumulated lane/card bounds, both distinct Open/Worktrees targets, transformed hit testing, a recent-only exclusion, and an empty ordinary-folder lane. `App.applyOverviewLaneAction` is the existing lane dispatch extracted without changing its ordering or selection semantics; the real overview callback consumes it before the unchanged loop/pan paths. Never-shown native tests exercise both projects' Open actions and emitted project/card selection and UIA bounds at 96/144/192 DPI. Interleaved graph refresh retains the other project's selected loop and updates stable card identity/geometry. Real scoped Worktrees actions start with no inspection, inspect two independent disposable Git roots, and require exact inspection/dialog paths and emitted primary rows, non-reclaimable primary safety, and preserved sentinel bytes; pre-seeded rows or an Invoke return cannot satisfy them. Deliberate wrong-identity/action/geometry controls are rejected before dispatch, not claimed as disabled-production-dispatcher or historical bug evidence. These are production-helper/model/UIA-data results, not shown rendering, OS input, COM invocation, mounted terminal/focus, or macOS runtime proof. The shared live overview segment still covers one project's two cards; simultaneous two-project capture and loop navigation remain unproved. macOS topology/START furniture, richer lane captions/chips, global-lane filtering, and remote/all-project worktree binding behavior also remain outside this slice | Partial | -| Folder lanes/bands | Project caption, worktree chip, open/close and folder actions | Overview lanes render distinct Open and Worktrees actions beside the project caption; click routing selects the project or opens scoped worktree inspection. Focused geometry/input coverage passes, and the local Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` posts real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` messages at the lane's Open and Worktrees hit-test rects against the live executable and verifies Open routes to the project canvas (synchronized cards render at a new position) and Worktrees opens the scoped inspection view, both confirmed passing across many consecutive live runs. This also uncovered and fixed two real accessibility bugs along the way: `App.zig`'s `.overview` mouse-click switch arm and its `.cycle_attention` action handler were both missing the `syncAccessibility()` call that keeps the live UIA tree in sync with what is rendered, so a lane's Open/Worktrees click previously had no observable effect through the accessibility tree even though the underlying surface did change | Validated | -| Notebook grid | Grid pans and zooms with canvas | `GraphCanvas.drawGrid` derives its cell size and offset from the exact same `CanvasState.zoom`/`pan_x`/`pan_y` fields consumed by `overviewCardBounds`, `overviewLaneBounds`, and the loop-card geometry, so the same focused pan/zoom coverage (`GraphCanvas.zig`: "canvas hit testing follows pan and zoom", "overview and quick chat geometry applies pan and zoom consistently") indirectly proves the grid cannot desynchronize from the content it underlays. The Windows shell toolchain blocker is resolved, but the grid itself is a 1px `0x00161815` GDI line pattern with no UIA surface of its own, and this session did not add a live pixel-scan assertion (the connector-handle and attention-rail blocks already show this pattern is feasible) to directly confirm grid line spacing changes with zoom in the running executable. Left Partial rather than claim live evidence that was not actually captured | Partial | -| Pan and anchored zoom | Pan, pointer-centered wheel/pinch zoom | Mouse pan and pointer-centered wheel zoom remain intact and unchanged, with the same focused regression coverage as before (`GraphCanvas.zig`: "canvas zoom keeps the graph point beneath the cursor stable", "canvas wheel zoom scales high-resolution trackpad deltas"). Native touchscreen pinch-zoom is now implemented and routed through the main window: `MainWindow.zig` registers only `GID_ZOOM` via `SetGestureConfig`, leaving every other gesture class (`GID_PAN`/`GID_ROTATE`/`GID_TWOFINGERTAP`/`GID_PRESSANDTAP`) at its existing OS default rather than explicitly blocking gestures this app has no opinion on, with a real registration test against a genuine `HWND` plus two independent negative controls — a malformed native `SetGestureConfig` call and a genuinely invalid `HWND` passed straight through the production `registerCanvasGestureConfig` helper itself — proving the helper's own `GetLastError()` capture path actually fires, not just the raw Win32 API. `App.zig`'s `WM_GESTURE` case decodes `GID_ZOOM` via a pure `CanvasInput.classifyGesture` decision table (including a distinct outcome for a gesture delivered as a single combined begin+end message, so it can never reuse a stale prior gesture's baseline), requires the active surface to actually render the graph canvas (not just the wheel-region rectangle, which the terminal workspace surface shares), and applies `GraphCanvas.zig`'s `beginPinchZoom`/`continuePinchZoom`/`endPinchZoom` against a per-gesture identity hash of surface+project so a same-region destination change mid-gesture (surface switch, or project switch while still graph-capable) resets the baseline instead of silently continuing to scale the wrong canvas; a failed `GetClientRect` is guarded and treated as unhandled rather than classified against an undefined rect, and the gesture handle is closed before any call that could re-enter the message loop. This is source-mapped, automated routing/unit evidence, not live hardware-input evidence: this session held no live UIA capture slot this pass, so pinch is proven by code mapping and a full deterministic test suite (non-compounding/clamp/zero-distance/lifecycle/context-mismatch/routing matrix, verified with genuine temporary-regression RED/GREEN passes against the production helpers, not GREEN-only), not an actual touchscreen or Precision Touchpad device. Per Microsoft's documented default, Precision Touchpad pinch on a classic Win32 window is emulated as synthetic Ctrl+`WM_MOUSEWHEEL`, not delivered as `WM_GESTURE`, so this implementation targets true touchscreen digitizers specifically; Precision Touchpad pinch behavior is not separately implemented or verified here. Touch-driven pan (`GID_PAN`) remains a genuine unimplemented gap: this app forwards it unhandled rather than half-handling it, but does not explicitly block it either. Left Partial: touchscreen pinch has real source and automated-test coverage but no live device evidence, and touch pan is still unimplemented | Partial | -| Zoom controls | Zoom out, actual size, zoom in, fit with shortcuts/help | Visible bottom-right controls provide zoom out, percentage/actual size, zoom in, and fit. A visible shortcut/help line accompanies the controls; Ctrl+-, Ctrl+0, Ctrl+=, and Ctrl+9 remain represented in the View menu. The Windows shell toolchain blocker is resolved and `Tools\windows\uia-live-gate.ps1` now runs against the live executable: it locates the `zoom-out`, `actual-size`, `zoom-in`, and `fit-canvas` UIA fragments, requires non-empty bounds, resolves each `InvokePattern`, and then actually invokes zoom-in, actual-size, zoom-out, and fit-canvas in sequence against the running shell, all of which completed without error across many consecutive live runs | Validated | -| New Loop canvas button | Visible top-right add action | A live-validated top-right New Loop button is now present on non-empty project canvases and remains centered in the empty state | Validated | -| Composite breadcrumb | Current group, project back action, loop count | Open Group swaps the project canvas to the authoritative nested graph, renders its cards and edges through the normal interactive canvas, and exposes a clickable `Project > Group` breadcrumb with loop count that restores and reselects the parent. Nested graph selection survives daemon refreshes, and the populated live UIA gate invokes Open Group, verifies both nested cards, and invokes the bounded Back breadcrumb to restore the parent canvas | Validated | -| Canvas attention rail | Count/oldest context and Review action | The rail exposes a clickable Review target and uses `createdAt` from the daemon model when present to show a true `oldest ` label alongside the oldest attention item title. Focused hit testing passes, and the Windows shell toolchain blocker is now resolved: the rail has no dedicated UIA element of its own (it is a full-width band GDI hit-test region), so `Tools\windows\uia-live-gate.ps1` posts a real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at the rail's exact screen rect against the live executable and verifies the click drives `App.zig`'s `.review_attention` -> `selectNextAttention()` routing by observing the resulting `SelectionItemPattern` selection actually move from one card to the NEEDS YOU card, passing across many consecutive live runs | Validated | -| Node positioning | Persisted positions and direct card movement where supported | Project cards can be dragged directly, with movement transformed correctly at non-default zoom, shared geometry/hit testing updated during the drag, and capture-loss cancellation restoring the prior position. Offsets are keyed to stable node identity, remapped across daemon reorder, and atomically persisted under the configured GraphCode support directory. Focused reorder/reload regressions and a real physical drag capture validate the complete flow | Validated | -| Connector handles | Hover handles and drag-to-connect | The right-edge connector tracks hover, paints a visible handle and plus affordance, and preserves the drag-to-connect path. Focused rendering/input coverage passes, and the Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` synthesizes real `WM_MOUSEMOVE` hover messages at the source card's outgoing connector position and confirms the live `0x7ACDFF` hover handle pixel actually appears on screen (`Test-ScreenPixelNear`), then drives a full `WM_LBUTTONDOWN`/`WM_MOUSEMOVE`/`WM_LBUTTONUP` drag from that connector onto a second card and confirms the resulting native "Create or edit edge" dialog locks its From/To fields to the exact dragged source and dropped target loop IDs, all passing across many consecutive live runs | Validated | -| Loop card identity | Loop-type stripe, title, state pill, entry/cycle role | Project and overview cards now use loop-type-colored stripes while retaining lifecycle state text, START, UNWIRED, and attention labels. Focused color regression coverage passes; live evidence remains blocked | Partial | -| Loop card live detail | Goal/prompt/check line, progress, metric change, elapsed/backend/model/worktree metadata | Cards prioritize goal, trigger, or check detail, retain current activity, and add metric pass/change text, elapsed age, backend identity, token usage, model tier, and worktree/branch metadata from the same decoded daemon fields used by the workspace loop bar. Focused card metadata tests (`GraphCanvas.zig`: "loop card metadata includes backend elapsed and token usage when reported") pass, and the `windows-shell` CI job's live UIA gate exercises the populated card fixture end to end (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, passing, merged as PR #399) | Validated | -| Loop card attention | Reason-aware amber presentation and primary action | NEEDS YOU cards render a card-level reason-specific primary button: `Reply` for reported awaiting-input sessions and `Inspect` for other attention reasons, both routed through the normal loop-opening path. Focused action-label tests (`GraphCanvas.zig`: "attention cards expose reason-specific primary actions") pass, and the live UIA gate's `attention-action-*` assertion for the deterministic awaiting-input card passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | -| Unwired card recovery | Explanation, Wire it up, Mark as entry | Cards with no inbound or outbound edge now show an explicit UNWIRED warning and recovery explanation. Their native context menu exposes Wire it up, which enters the existing drag-to-connect flow, and Mark as entry, which changes the card to START for the session. Focused role/action tests plus live menu and post-action captures validate the flow | Validated | -| Worktree reclaim offer | Reclaim and Keep actions on resolved card | Safe resolved cards with a matching landed, clean, pushed worktree expose separate Reclaim and Keep targets. Reclaim revalidates safety and Keep suppresses the offer for the session. Canvas Reclaim/Keep descendants are now emitted through the UIA provider and invoke the same fail-closed paths. Focused geometry/safety coverage passes, and `Tools\windows\uia-live-gate.ps1` now asserts the live Reclaim/Keep descendants under the Graph fragment (name, non-empty bounds, InvokePattern, and correct RawView/ControlView sibling linkage) via the `windows-shell` CI job (PR #385, run 35422364203, passing) | Validated | -| Composite card actions | Open Group, Pilot Once, Arm Schedule | Canvas and sidebar composite menus expose all three actions. Open Group is live-validated; nested creates, edits, deletes, edge changes, pilot, and arm commands use the daemon's authoritative `subGraphCommand` envelope; and Arm Schedule is disabled unless the decoded pilot state is exactly `piloted` | Validated | -| Edge presentation | Kind style, fired state, cycle label | Windows retains typed optional cycle guards and derives fired state from authoritative `fireCount`, with missing/null legacy fallback. Raw snapshot JSON reaches the production label formatter for maximum/until/flat-pass and empty-guard summaries; signed/null/malformed inputs, Unicode/long text, owned copies, refresh/composite transitions, and allocation-failure cleanup have executable coverage. Kind styling, selected emphasis, fixed 148-by-20 label bounds, ellipsis, and collision placement are retained. This is exact-string and static-geometry evidence only: full visible wording, the macOS context-menu summary, rendered pixels, and live label evidence remain unverified | Partial | -| Edge creation sheet | Kind/condition/transform/cycle controls with conditional validation | The guided native form provides endpoint selectors, conditional fields, validation, keyboard traversal, scrolling, and recap. Menu and connector creation now share the full draft sender, retaining condition, payload transform, all optional cycle guards, and spawn target. Project/composite identity and endpoint IDs/titles are owned across the modal; stale model/selection/client subgraph addressing and missing or changed endpoint choices are explicitly rejected without retargeting. The update subscription can still observe another cached project or refresh independently: outgoing creation explicitly addresses the captured project. Registered pure production-path tests inspect the real client's outgoing queue, including cached-project selection, Unicode/quotes, callback-induced graph changes, cancellation, and allocation failures. The reduced connector sender fails the same accepted-draft fixture that the full sender passes. Earlier live evidence only opened the form and checked endpoints/recap before cancellation; this fix has no new live modal/keyboard, daemon acceptance, persistence, or cross-platform parity evidence | Partial | -| Custody child creation | New Child Node on unresolved parents, inherited editable backend, daemon-owned custody | Project-canvas/sidebar node menus consume the tested unresolved-only item plan (5119). Owned popup target/settings/child-only exact-project worktree snapshots feed the existing guarded node form and template/attachment continuation. Original popup context is checked before normal initial selection; final guards never reselect, reject project/composite/address drift or deleted/resolved/type/backend-changed parents, and allow rename/reorder/unresolved progress. Tests exercise the production initializer/transfer boundary and real data-only client queue, including cached B while observing A, root-versus-composite same IDs, UUID/null wire fields, cancellation and allocation failures. `createdBy` produces one create command: GraphStore owns the fired handoff/report-back memo and normal startup. No new protocol or parent/session mutation. Native display/action results, overview right-click, daemon acceptance/persistence, and inherited downstream-send failure handling remain unverified. The merged ordinary-creation path now retains the owned worktree snapshots whose evidence is recorded in Node creation sheet, while custody retains its pre-popup owned snapshot; template backend settings remain a separate residual | Partial | -| Edge editing | Preserve identity/configuration and refuse stale edits | Windows uses an owned modal snapshot and one checked `updateEdge`, retaining endpoints, current fireCount, transform/spawn settings and unchanged optional legacy guards. Root/direct-composite scope and current cache are rechecked without treating observation subscription as authorization. Existing serialized daemon commands, blocking, parent roll-up and root publication remain authoritative; deeper new edit wrappers are refused. Production-helper regressions exercise actual Wire JSON/unstarted queues, foreign scopes, refresh, cancellation and allocation cleanup. Edit-only live/submit capture reads complete Unicode text and blocks acceptance on per-field read/allocation errors; injected-reader and initializer tests show no controls. In-memory Swift store tests cover CAS, kind/blocking rules, root publication, preview refusal and serialized legacy-child writeback followed by an edit that retains runtime progress. This is not native editing, daemon acceptance/persistence or macOS runtime evidence; macOS sources expose creation-time configuration and delete, not an existing edit UI | Partial | -| Node creation sheet | Loop-type teaching tiles, conditional fields, backend/model/branch pickers, recap, validation reason | A guided native form provides loop-type/backend/model choices, type-specific fields, explanatory copy, accessible checkboxes, inline validation, keyboard traversal, scrolling, and a live-updating recap. The native Branch picker offers inspection choices plus “This folder”; existing tests cover its empty state and selected binding, and earlier gate evidence covers the recap, Branch picker, and distinct Attach control. Staging now reserves a uniquely named file exclusively: the production append/removal regression first demonstrated A/B/C → remove B → add D overwriting C's bytes, then passed with C's original path and bytes preserved. Offline in-file coverage also checks template-restored/legacy files, preexisting destination collisions, partial-write rollback, allocation failures without orphan copies, invalid inputs, exactly eight files and the ninth rejection, and the exact 10 MiB/+1-byte boundary. Existing attachment DTO/wire tests remain passing; these are no-window helper/filesystem results, not OS input or daemon persistence evidence. Creation choices now own an exact-project snapshot of inspected path/branch strings across cache/model replacement; baseline aliasing and foreign-cache regressions fail before the fix and pass afterward. Noninteractive production-projection tests cover snapshot lifetime, projection without a loaded graph, and allocation-failure cleanup. Separate pure initializer/builder-to-Wire tests verify exact non-default repository/id/path/branch fields, initial/current/template-restored selection, and “This folder” clearing with available choices; legacy empty-choice hidden binding remains unchanged. This does not establish actual native non-default selection, a Git-inspection-to-creation flow, or daemon save/reload. **Still partial:** actual inspected nondefault binding and project-scoped ownership across pumped modals remain unproved; New branch and remote/global branch visibility differ from macOS; authentic OS picker acceptance/cancellation, clipboard paste/drop, and native-input → request → daemon save/reload evidence remain outstanding. Per-file removed-file reclamation and legacy unguarded cleanup remain separate residuals. Creation now retains the original project/composite context and validates after modal teardown, before attachment transfer, across normal, template-load-error, empty-library, and template-loop submissions without switching selection or client scope. Bounded offline production-boundary RED/GREEN and owned temporary-file tests cover stale-scope refusal, loaded-project closure, same-path refresh/promotion, path-only starts, valid empty composites, template continuation, allocation unwinding, and checked cleanup failures. Guarded staging reserves its attachment-directory leaf exclusively; cancellation/refusal and zero-reference acceptance abandon only that owned leaf, while nonzero accepted attachment bytes survive. Zero-reference drafts preserve existing empty-ID/wire behavior. These are helper/model/unstarted-client-queue proofs, not live modal, OS picker, or daemon-persistence evidence; path-only external disappearance and downstream send failure remain limitations | Partial | -| Node update/rename | Dedicated rename prompt and safe typed updates | Rename retains its dedicated safe prompt. Edit Details uses `NativeForms.update` and `sendUpdateNodeForm`; its project/node identity is now owned across the modal message loop and re-resolved afterward, and changed strings are compared against the form's owned initial snapshot rather than potentially freed graph strings. Original numeric scalars and clear-versus-unchanged semantics are preserved. Production-helper tests cover source mutation, unchanged/changed typed fields, cancellation, clearing, and allocation failures. The lifetime test rejects the original borrowed baseline. Live attempts stopped at background-menu activation before reaching editor or rename actions, so app-level dispatch, cancellation, and returned model results remain unverified in this work | Partial | -| Delete confirmations | Named object, consequences, safe default | Loop deletion names the loop and explains graph-connection removal. Edge deletion now names both endpoint loops and the connection kind, explains that the loops remain, re-resolves the stable edge after confirmation, and defaults to cancellation | Validated | -| Canvas context menu | Folder actions on background; complete node/edge actions | Project backgrounds now expose existing Worktrees, Project Settings, and Explorer routes with captured project context; unavailable non-filesystem actions are disabled and global scope omits them. Create Edge remains present but disabled with fewer than two loops. Resolved node menus omit Stop, matching macOS; composite/template/unwired actions and the absence of Message/Memo are preserved. Tests inspect the same native HMENU builder used by `TrackPopupMenu`, including an exact disabled-item assertion that fails when Create Edge is removed. The existing gate already inspects plain/composite/unwired popup contents; its succeeded-node Stop assertion now requires absence. Focused live attempts read the real background popup but did not achieve an action/result, and a minimal native control failed foreground acquisition before opening its menu; the cause remains unproven. Live node/edge/background action results, New Child live proof, and import/export remain deferred; custody child creation and sketch promotion retain their separate Partial evidence rows | Partial | -| Sketch promotion | Promote a sketch to Goal, Turn, or Timed without replacing its identity or session | Canvas and sidebar context menus use one production submenu plan and focused native forms. Owned popup/form snapshots reject stale scope, selection, type, and deletion; initial selection of a different cached project remains supported independently of the observation subscription. Filtered pure tests exercise the actual App selection adapter, typed native-form builder, and unstarted DaemonClient outbound queue, including composite addressing and allocation failures. Three emitted fixtures decode as existing Swift `promoteNode` variants, and the full Windows application builds ReleaseSafe. A native Win32 test attaches the production node popup to a hidden HWND and checks the Goal/Turn/Timed submenu's presence, IDs, labels, and enabled states through the actual HMENU; it does not show or select the popup. Local command construction/queueing and this native menu state are proven, but native keyboard interaction, UIA, app launch, and real-daemon acceptance/persistence remain unexercised | Partial | - -## Quick Chats - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Quick Chats canvas | Band, cards, pan/zoom, add button, empty state | The real executable was exercised with two deterministic chats. The band, transformed cards, top-right New Chat action, bottom-right zoom controls, and 100% → 110% zoom transition were captured live. Context actions are wired, and the populated live UIA gate validates named, bounded, invokable Quick Chat cards plus the populated-canvas New Chat action | Validated | -| Quick Chat cards | Title, chat identity, optional backend badge, open/rename/delete menu | A populated live fixture verified title/default-chat identity/optional backend rendering and direct opening (`openQuickChat` was observed by the protocol stub). Cards now expose Open Chat, Rename, and Delete Chat context actions | Validated | -| Create chat | Visible New Chat controls | Empty and populated Quick Chats canvases expose the New Chat action in the macOS placements. The live UIA gate invokes the populated canvas action and the sidebar/header action; the existing empty-state walkthrough invokes the centered empty-state action | Validated | -| Rename chat | Single title prompt from row/card | Uses a dedicated single-title modal from the card/keyboard action, trims input, and rejects empty titles | Validated | -| Delete chat | Named confirmation explaining session/scrollback deletion | Uses a named warning that explains terminal-session and scrollback removal, defaults to cancellation, and only sends deletion after confirmation | Validated | -| Chat workspace | Opens a persistent terminal workspace | Opening a Quick Chat now exposes a bounded, selected `Quick Chat terminal workspace` UIA surface while the existing terminal panel remains persistent. The full native UIA walkthrough verifies the card invocation and workspace transition without duplicating loop-workspace implementation | Validated | - -## Loop terminal workspace - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| 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. Production is still 120x40 without PTY resize negotiation; 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 | -| 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 | -| Mounted background tabs | Switching preserves live terminal surfaces | Workspace implementation tests still cover the topology, and the live UIA gate now creates a second mounted tab, switches between the original and background tab, and asserts both tab automation identities survive the round trip without shell exit/reconnection | Partial | -| Right loop panel | Minimap, upstream/downstream, fired conditions, metric sparkline, branch/start/usage footer | The full workspace right rail includes the selected-loop map, upstream/downstream cards, fired-edge coloring, edge conditions, branch/worktree identity, metric/goal detail, model tier, a metric-history sparkline from decoded samples, start-time/usage/backend footer text, a collapse/expand control that no longer reserves rail width while hidden, and dedicated UIA children for sparkline/start/usage/toggle (`workspace-detail-sparkline-*`, start, usage, and toggle automation IDs). The live UIA gate asserts those children and toggles collapse/expand, and that gate passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | -| Show in Graph | Visible loop-bar and menu action | The live gate invokes the real `workspace-show-graph-*` loop-bar action through `InvokePattern`, requires the exact selected `UIA loop A` `canvas-card-*` identity and selected state with strict workspace-chrome absence, then returns through the exact supported `loop-row-*` identity and requires the same project toolbar, selected-loop bar, and Show in Graph identities. A focused isolated pair at `.graphcode-evidence/show-in-graph-20260924-151455` used source gate SHA-256 `AE4A2C71...`, focused harness `78E6821E...`, and shell `8E0AA9E7...`: GREEN invoked the action and completed the exact round trip (stdout `E94DCA9D...`); RED disabled only that invocation and failed the unchanged chrome-absence assertion (stderr `3198914D...`). This confirms the route without a product fix; graph cards remain intentionally non-invokable. The focused early return did not exercise later downstream-provider rebinding. A full integrated gate pass and live native Loop-menu invocation remain unverified. | Partial | - -**Live-gate infrastructure fix (this session):** the `windows-shell` CI job's `uia-live-gate.ps1` step was, until now, never actually exercising any of the workspace chrome above: `App.init()` unconditionally skipped `Workspace.init()` under `GRAPHCODE_UIA_GATE=1` regardless of whether a real `zmx` executable was supplied (a pre-existing guard predating this workstream), so every "Partial" row above had never been run against a real workspace at all. Fixed in `App.zig` to build the real workspace under the gate whenever `GRAPHCODE_ZMX` is present. That surfaced a second, genuine regression: the newly-real terminal surface competed for native Win32 keyboard focus with the rest of the UI after navigating away from the workspace (`App.openGlobalOverview()` and friends). Root-caused to `Workspace.poll()` (driven by the main window's 100ms `WM_TIMER`) unconditionally draining terminal output and calling `winghostty_surface_notify_accessibility_text()` regardless of workspace visibility, which kept re-asserting UI Automation focus on the terminal no matter what Win32-level focus fixes were made. Fixed by adding `Workspace.collapse()`/`Workspace.collapsed`, skipping `resize()`/`syncTopology()`'s pane refocus and terminal-output polling entirely while the workspace is hidden, plus a `WM_ACTIVATE` handler that reasserts the app's own focus policy after `DefWindowProc`'s default child-focus restoration on window reactivation. All of this is now covered by the passing `windows-shell` CI job (commits `a31813b`..`cba010f`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793). Note: the separate `windows-spikes`/`windows-hardening` jobs (`validate.ps1 -Task all`) run the identical gate script but under much heavier CI load and still intermittently hit this same assertion's 15-second retry window; this has been confirmed as pre-existing, cross-branch flakiness unrelated to this workstream (an unrelated sibling branch, `coneilen-microsoft-repository-settings-parity`, shows both a pass and an unrelated failure on the same job across consecutive runs), not a regression introduced here. - -## Repository ingress - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Open Folder | Native picker from Welcome and Add Folder menu | Welcome and File menu commands use the Windows folder-only File Open dialog with filesystem/path validation. The live UIA gate invokes the empty-state action, verifies the titled native picker, and cancels it safely | Validated | -| Clone Repository sheet | Repository, location picker, derived folder, branch, depth, progress, inline failure, cancel | Clone runs behind a progress-capable native operation sheet with live output, cancellation, terminal status, and shared dark painting. Windows-shell contracts pass. The UIA gate opens the real sheet, verifies URL/destination/branch/depth fields, submits it empty to verify the inline URL error, and cancels it | Validated | -| Add Remote Repository sheet | Server/user/port/path, explanation, validation progress, inline selectable error | SSH validation runs on a worker while a validation sheet remains open and Connect is unavailable until completion. It shares Clone’s dark themed native dialog class. Windows-shell contracts pass. The UIA gate opens the real “Add SSH Repository” sheet, verifies host/user/port/path fields, submits it empty to verify inline validation, and cancels it | Validated | -| Remote Connection info | Read-only selectable connection sheet | Remote project context menus expose a dedicated read-only connection-information dialog with the encoded remote project identity and management guidance. The live UIA gate opens the native sheet, verifies both pieces of content, and closes it | Validated | -| Add Codespace sheet | Authenticated codespace discovery, selection, workspace path, validation progress, empty/error/retry/cancel, opens the remote project | `Codespaces.zig` + `WindowsCodespaceDialog.zig` provide discovery, failure remediation, validation, submit gating, and `codespace://` project opening. Deterministic Codespaces/dialog/windows-shell coverage remains green. **Still partial:** the available token lacks `codespace` scope; no real discovery-success, selection, validated dial, or rendered-sheet UIA walkthrough was possible. Only the 403/remediation path was exercised against real `gh` | Partial | - -## Settings and worktrees - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Product Settings window | Backend, three permission pickers, model picker/auto toggle, activity, briefing, beta with explanatory copy | `WindowsProductSettings.zig` exposes native backend/model/Claude/Copilot/Codex selectors, routing/activity/briefing/beta controls, macOS-equivalent consequence copy, and Save/Cancel. Focused tests cover settings preservation and selector copy; `GRAPHCODE_UIA_GATE` mutation 15 opens the real window against an isolated settings file, verifies every required visible control/explanation, proves control-targeted Return saves while preserving unknown fields, and proves Escape cancels byte-for-byte | Validated | -| Infrastructure diagnostics | If retained, separate advanced surface | Daemon pipe/support-directory overrides are now explicitly labeled “Advanced Connection Settings...” while the normal Settings command opens product settings | Validated | -| Project Settings sheet | Resolve policy radio rows, safety explanation, size/count thresholds, immediate save | Valid threshold/radio edits persist immediately, while empty/partial threshold input preserves the last valid persisted value; Done remains dismiss-only. The shared dark native form is covered by focused and Windows-shell tests. The UIA gate uses the gate-only hook to open it and verifies policy, threshold-unit, and worktree content before cancelling | Validated | -| Worktree sweep sheet | Safe/look/in-use grouping, size summaries, default selections, reveal, inline destructive confirmation, recovery note | Sweep storage supports 256 rows, real directory sizes and aggregate totals, reveal, and a second destructive confirmation for dirty rows. Focused and Windows-shell coverage pass. The UIA gate opens the real sheet through the gate-only hook and verifies safe/look group rendering and Remove Selected. The fixture proves sheet rendering and interaction affordances, not Git worktree discovery | Validated | -| Worktree notice chip | Threshold-driven titlebar and lane notice | Explicit inspection installs compact value observations on each exact `GraphSummary` owner; replacing the full inspection, selecting another project, or reordering lanes does not transfer its counts. Overview chips share one presentation/geometry source for paint, hit testing, and path-keyed UIA data, reuse the existing Worktrees action, and show uninspected, incomplete-size, policy-unavailable, stale, and failed-refresh states without fabricated zero/freshness. Checked policy outcomes distinguish missing/legacy defaults from unreadable or malformed configuration. Failed policy writes also reconcile the captured owner's checked readback while preserving the original write error. The shared inclusive count-or-binary-GiB evaluator uses exact known bytes; partial measurements are approximate, and unknown policy or stale observations cannot claim a configured breach. Relevant binding/state/connection/mutation signals stale observations; nested comparisons reuse the existing decoder and scoped IDs under depth, work, and scratch-memory bounds, and comparison uncertainty remains explicitly stale. Title/position-only edits and supported reordering do not manufacture a refresh. Existing owner close/restore eviction removes observations. An exact-current-owner raw-inspection accessor keeps correct-owner rows visible below threshold or when stale/policy-unknown, while excluding foreign current-root rows without changing retained modal state. Pure production-helper/mapper tests cover boundaries, atomic replacement, ownership, nested changes/limits, injected partial-write reconciliation, error states, old action rectangles, path resolution, and literal 96/144/192-DPI UIA-data bounds; the sizing, rounding-label, raw-inspection, nested-binding, and failed-write regressions have RED/GREEN evidence. These are synthetic-data/helper results, not actual filesystem-failure, Git-inspection, native modal, keyboard/click, or COM/UIA walkthrough evidence. Windows still counts all inspection rows including primary/prunable and sums logical file bytes; macOS excludes the opened checkout and sums non-prunable allocated usage. **Still partial:** automatic discovery, global titlebar aggregation, remote/project-canvas band parity, and authentic multi-lane/boundary review-action proof remain outstanding | Partial | - -**Bounded worktree subprocess reliability (2026-09-26):** local job-bounded -`WorktreeGitProcess.Tests.ps1` calls the actual production helper. The baseline -with only `create_no_window` launch instrumentation reproduced upper/mixed-case -repository redirection, outside index/object writes, and an unread real-Git -stderr hang. Child-only environment scoping, concurrent bounded pipe collection, -and owned-child/result cleanup now cover those cases. Real selected/forced -removals and synthetic nonempty output from all three mutation paths exercise -allocation ownership; synthetic auth/config preservation does not use secrets. -The normal WindowsShell runner invokes this suite. No production wall-clock -deadline, OS wait/kill fault injection, live UI/provider walkthrough, or full -parity is claimed. The pre-existing direct `reclaim` command's self-removal -failure on the tested Windows Git remains unchanged; the synthetic direct-call -case proves ownership only. All existing Partial rows remain Partial. - -## Updates and dialogs - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| Available update alert | Install, Release Notes, Later | The native offer now conditions Install on whether the real feed check resolved a Windows asset URL (`WindowsUpdates.zig`/`App.showAvailableUpdate`): when one exists Install is enabled and triggers the real download/verify/extract/upgrade pipeline; when none exists (the real, currently-live state — see Install progress) Install stays visibly disabled with an honest reason, replacing the old permanent "not implemented" label. `UpdateOfferDialog.zig`'s `buttonsFor(installable)` is unit-tested for both states, and a real, live `feed-check` run against the actual `scgopi/GraphCode` releases API (`Tools/windows/Tests/WindowsUpdateInstall.Live.Tests.ps1`) confirms the disabled path is genuine, not simulated, for the current release. What is **not** live-witnessed: clicking Install through a real enabled offer end-to-end in the running UI, because no Windows release asset currently exists to enable it against — that gap is honestly disclosed rather than faked | Partial | -| Install progress | In-window progress indicator | `WindowsUpdateInstall.zig` now implements the full download → SHA-256 checksum verify → extract → `GraphCode-Setup.ps1 -Command Upgrade` pipeline, reusing the existing packaging verification/rollback logic rather than a second copy, and reports phase/fraction progress through a `ProgressFn` callback. `UpdateInstallDialog.zig` renders that progress in a native window (download %, verifying, extracting, installing) plus a failure state; both are unit-tested (14 tests total across the two files, part of the 42-file/273-test hermetic `WindowsShell.Tests.ps1` suite). Real, non-simulated live evidence (`WindowsUpdateInstall.Live.Tests.ps1`, invoked directly — not part of the hermetic suite since it makes real network calls): a real HTTPS download of a real multi-megabyte GitHub release asset streams genuine progress (100+ real progress reports, 0→100%) and its SHA-256 is verified against the asset's real published digest before extraction is attempted; a deliberately wrong digest is rejected with `ChecksumMismatch` strictly before extraction, proving the checksum gate is not vacuous. Honestly out of scope and **not** provable right now: extracting and upgrading a real Windows ZIP asset end-to-end, because the last recorded and just-reconfirmed-live asset check found only macOS DMGs published — there is no real Windows asset to extract. Also unproven: whether `Move-InstallDirectory`'s rename succeeds while `graphcode-windows.exe` is the actual running, self-updating process (the existing `Packaging.RealLifecycle.Tests.ps1` proves the *opposite* guarantee — that a locked file blocks and rolls back — not this scenario) | Partial | -| Relaunch prompt | Relaunch Now/Later and session continuity explanation | `UpdateInstallDialog.zig` presents Relaunch Now / Later with session-continuity copy after a successful install, and `App.runInstall`/`relaunchAfterUpdate` wire the outcome: Relaunch Now respawns the executable (same `CreateProcessW` pattern as `launchWorkspace`) and then quits the current process; Later leaves the update staged and shows an honest status message. Pure logic (`relaunch_message`, outcome handling) is unit-tested; the real Win32 window/thread code compiles and runs but is not live-driven by UI automation in this PR. This row cannot move past `Partial` genuinely: there is no real Windows release asset to drive a real install to completion today, so the actual relaunch — and whether zmx-backed terminal sessions survive an Upgrade-triggered restart specifically, as opposed to the differently-scoped scenario `DaemonHandoff.Live.Tests.ps1` already covers — remains unproven live. Faking that would violate this fleet's evidence policy, so the row is left honestly `Partial` rather than asserted `Validated` | Partial | -| Install failure | Download in Browser/Cancel with reason | The Windows flow exposes Release Notes/Later and a disabled Install action explaining that in-app installation is not implemented, then hands off through the verified browser release page when notes are requested. This remains Partial until download/install failure handling exists | Partial | -| Loop rename | Title field, Return submits, explanatory text | The dedicated single-title modal explains where the title appears, prepopulates the current value, trims and validates submission, and re-resolves the stable loop ID after the modal. The populated UIA gate edits the native field and verifies Return submits and closes the dialog | Validated | -| Loop delete | Named loop and full consequence message | Names the loop, explains graph-connection removal, and defaults to cancellation | Validated | -| Chat rename/delete | Dedicated prompts | Dedicated single-title rename modal and named fail-closed deletion warning are wired from card actions and shortcuts | Validated | -| Project delete loops | Dedicated confirmation | Sidebar project menus expose Delete All Loops through one fail-closed implementation with graph and filesystem consequence copy, safe cancellation default, and the dedicated daemon command. The live UIA gate verifies the native confirmation and cancellation path | Validated | -| Project remove/trash | Distinct reversible remove and filesystem Trash choices | Remove from GraphCode is distinct, confirmed, and explicitly preserves files. Local project menus now add a separate confirmed Move Folder to Recycle Bin action using the Windows undo-capable shell operation; remote projects retain only the GraphCode removal action | Validated | - -## Accessibility, input, and visual behavior - -| macOS surface | Required visible behavior | Windows evidence | Status | -|---|---|---|---| -| UI Automation tree | Names, roles, selection, invoke/toggle, focus, live status for every visible surface | The synchronized live C++ provider exposes stable project rows, loop rows, project/overview/Quick Chat cards, worktree rows, destinations, canvas primary action, zoom controls, policy actions, focus, selection-change events, and status. The live gate uses explicitly in-process deterministic fixtures to validate populated RawView/ControlView navigation, real bounds, observable Quick Chat/workspace invocation effects, tagged-command isolation, identity-preserving reorder/removal, events, concurrency, and teardown. The Workspace menu's New, Rename, and Delete lifecycle commands and dynamic workspace-switch rows are now part of that native provider tree instead of existing only in `Accessibility.zig`; the Zig contract was reduced to the native fixed table, and a pinned-Zig executable test now fails when any contract id lacks an exact native fixed-table id. The live gate now also traverses the Workspace menu in RawView and ControlView, checks its fixed lifecycle names and InvokePattern exposure, and verifies at least one `workspace-switch-*` row under the same parent. `TerminalSurface.zig` retains reported selection and cell metrics, but callback metadata does not prove applied terminal selection. Exact-pin Winghostty `f5abc059` source already creates an embedded child-HWND Text/Text2 provider and routes `WM_GETOBJECT` to it; the earlier missing-provider premise was inaccurate. GraphCode now feeds that provider owned UTF-8 from its unchanged rendered-cell grid with independent UTF-16 length/cursor offsets, rather than a rolling raw VT byte tail. Focused producer tests exercise overwritten text, chunk splits, reset/rollover, Unicode scalar representation, bounds, allocation/lifetime and injected publication failures without native APIs. This is fixed current-grid content, not transcript/scrollback or full Unicode terminal rendering. Render/text publication remains best-effort: failures are reported and the provider may retain its last successful, no-longer-current snapshot. Native terminal text results, applied selection, visible caret/geometry, range/HRESULT conformance, retained-generation behavior, atomicity, daemon-to-model UIA integration, and remaining dialogs still need separate evidence or implementation; keyboard discovery and HelpText residuals are not closed | Partial | -| Reproducible DPI/geometry regression coverage | Control metrics for GraphCode-owned chrome scale correctly and predictably across 100/125/150/200% DPI | `Tools/windows/visual-baseline.ps1` previously only checked that each DPI variant's `scale`/`viewport` were present and positive. It now reimplements `Dpi.zig`'s exact `scale()` rounding formula, self-checked against `Dpi.zig`'s own fixed-point unit-test cases, reads the real base pixel values straight out of `DesignTokens.zig` (not a copy baked into the manifest), and asserts the scaled geometry for `sidebar_width`, `tab_bar_height`, `pane_header_height`, and `loop_bar_height` at the real Windows per-monitor DPI values (96/120/144/192) is monotonic and matches the 96-DPI base exactly at 100%. Every `regionGeometry` entry is required to target a `deterministicScreenshotRegions` (GraphCode-owned) region, never a Winghostty-owned one, keeping third-party terminal pixels structurally out of scope. **This check performs static manifest/geometry metadata validation, not rendered-output comparison: it never launches the app, captures a window, or rasterizes a bitmap.** It re-derives expected numeric geometry from source-of-truth code and checks the manifest against that math, which is materially stronger than the prior presence-only checks and does catch real drift, but it is not a screenshot diff and should not be read as one; this repo/CI has no deterministic way to rasterize a live Win32 window. Manually re-verified that corrupting either a DPI value or a `DesignTokens.zig` constant makes the script fail | Validated | -| Keyboard discovery | Every shortcut represented by a menu item or visible hint where practical | Restored File, Loop, Terminal, View, and Help menus expose the primary project, graph, terminal, workspace, settings, update, and zoom commands with shortcut labels. The shared node popup no longer advertises Enter for Open Terminal or Ctrl+E for Edit Details: root Enter has no standalone open action, and Ctrl+E renames a selected loop or edits a selected edge. Two executable contracts failed on the old captions and pass on the corrected ones through the actual menu builder and `GetMenuStringW`, preserving command IDs, mappings, order, and enabled states across ordinary/resolved/composite/unwired targets. They inspect unattached menu handles without creating windows or displaying popups; mapper checks preserve toolbar Enter activation and existing Ctrl+E/F2 routes. The Windows README now distinguishes root, terminal, and dialog contexts and corrects toggle/chat-delete bindings. Pure mapper regressions cover Ctrl+Shift+OEM/legacy ASCII comma selecting product Settings, plain Ctrl+comma retaining Advanced Connection Settings, and unmodified/unrelated keys. An unattached native accelerator-table test checks the matching documented product binding and the unchanged original 15 bindings/order; this is not physical keyboard, focus, or dialog-opening evidence. Some context-only actions and canvas gestures still lack visible hints. No live keyboard or macOS runtime walkthrough was performed; remaining context-only/gesture discovery, other caption/routing discrepancies, and keyboard popup access still need evidence | Partial | -| IME/dead keys/layouts | Native composition in forms and terminal | Winghostty gate covers terminal IME; generic EDIT controls cover forms | Partial | -| Clipboard/selection | Terminal copy/paste and mouse selection | Terminal-context Ctrl+Shift+C copies the active surface's reported accessibility selection range through Winghostty into Windows `CF_UNICODETEXT`; Ctrl+Shift+V reads only `CF_UNICODETEXT`, converts UTF-16 to UTF-8, and calls Winghostty's paste validator and paste entry point with `allow_unsafe=0`. Clipboard conversion tests preserve Unicode, CRLF, LF, and empty text; app routing tests prove the shortcuts are terminal-context-only and preserve the existing global Ctrl+Shift+C Clone Repository route. Unsafe multiline/control-containing pastes are rejected with a status message rather than forced through, and no confirmation UI is provided. Clipboard Win32 calls, mouse-driven selection, actual rendered selection extraction, provider callback notifications, and end-to-end paste/copy on a live desktop were not exercised here; this row remains Partial | Partial | -| Per-monitor DPI | Layout and controls scale correctly across monitors | The process now declares real per-monitor-v2 DPI awareness at startup (`Win32.enablePerMonitorDpiAwareness()`, called before any window is created) instead of relying on system-DPI bitmap stretching; without this, Windows never delivers real per-monitor `WM_DPICHANGED` data to a DPI-unaware process. `App.zig` seeds the real startup DPI via `GetDpiForWindow` immediately after window creation (rather than assuming 96 DPI/100% until the first monitor move) and forwards every live `WM_DPICHANGED` to `TerminalWorkspace.Workspace.setDpi()`. Previously, `TerminalSurface.zig`'s `onDpiChanged` callback silently discarded the `dpi`/`scale` winghostty reported, and every terminal surface was created with `font_scale` hardcoded to `1.0`, so terminal text never actually rescaled on a DPI change or on a monitor with non-100% DPI at launch. `Workspace.setDpi()` now propagates the real runtime DPI to every live surface via winghostty's own `winghostty_surface_notify_dpi_changed` + `winghostty_surface_set_font_scale` (the two operations the provider actually exposes for this), and `surfaceOptions()` seeds new surfaces' `font_scale` from the workspace's last-known DPI instead of a fixed `1.0`. Deliberately does not also pre-scale `options.input.cell_width`/`cell_height` (kept at their 96-DPI logical baseline) so the DPI ratio is applied exactly once, through `font_scale`, avoiding double scaling. `onMetricsChanged`/`onAccessibilitySelection`, previously also fully discarded, now record the host's reported cell metrics and terminal text-selection range per surface instead of losing them. Verified with `zig build` (full app, pinned Zig 0.15.2 against the exact pinned Winghostty provider) and `zig test src/TerminalSurface.zig` (new `Dpi.fontScale` unit test plus all 13 pre-existing tests, 14/14). No live multi-monitor walkthrough was recorded (this environment has no interactive multi-DPI desktop), so this remains Partial pending that end-to-end evidence | Partial | -| Dark visual language | Dark canvas/cards/sheets and legible state hierarchy | Existing `DesignTokens`, repository dialogs and `NativeForms` supply the dark native palette and teaching tiles. [Actual production-renderer captures](visual-baseline/rendered-windows/README.md) now preserve canvas/sidebar, Product Settings and workspace clients at native 96 DPI. The real PNG comparator verifies exact opaque canvas/card/sidebar/dialog samples; setup uses a synthetic disconnected fixture, UIA invocation and native WM_COMMAND, not keyboard-menu proof. Settings retains native light buttons; current macOS Settings uses a native grouped form, so these were not speculatively darkened. Other sheets/state combinations, legibility on every surface and a compatible current macOS capture remain unverified | Partial | -| Font rendering quality | Legible, ClearType-quality text on every surface, matching macOS's default anti-aliased text | The shared `AppFont.zig` cache requests Segoe UI at `CLEARTYPE_QUALITY`, with per-DPI native controls and logical-size selection for buffered canvas painting. [Actual 96-DPI glyph samples](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) preserve card/sidebar/Settings-title pixels with 27/31/55 distinct colors; the native button sample has two. These counts and recorded system font-smoothing settings are observations, not legibility thresholds or actual font-face certification. Native control LOGFONT metadata, every-surface/multi-DPI review, terminal-text readability and matched current macOS evidence remain unavailable. No speculative font fix was made | Partial | -| Line/shape anti-aliasing | Smooth, anti-aliased lines/curves/rounded corners matching macOS's Core Graphics default | Existing `GdiplusAA` draws solid Beziers, rounded cards and metric segments with GDI fallbacks; axis-aligned grid lines and dashed/preview paths remain plain GDI. [Real app captures](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) now use production GDI+ startup, with both disabling automation hooks unset, rather than the ordinary UIA gate or a standalone drawing surrogate. The sampled selected-card corner has 31 colors and metric sparkline 30; original pixels and source/UIA-mapped regions are preserved. Other colors are not automatically antialias coverage, and there is no invented quality threshold. All edge styles/DPI states and equality with macOS Core Graphics remain unproven | Partial | - -| Color palette fidelity | Windows tones/gradients match macOS `Theme.swift` 1:1 (not just "generically dark") | [Immutable before/after app captures and replay](visual-baseline/rendered-windows/README.md) prove a bounded COLORREF bug: the same 96-DPI workspace focus-strip ROI [500,130,64,2] changed from 128/128 orange RGB255,132,10 pixels to 128/128 blue RGB10,132,255 after only `pane_focus_tint` changed from `0x000A84FF` to `0x00FF840A`. The production comparator independently derives current Theme.paneFocusTint, decodes Windows BGR and checks actual pixels; old-orange/channel-swap/delta-1 controls fail. Canvas/grid/card/sidebar/dialog/selected-tab opaque samples also match exact source colors. Existing two-stop `GdiGradient` chrome is measured, not equated with macOS three-stop/material compositing. All tones/states and a compatible current macOS rendered comparison remain unverified; historical manifest and separate two-token currentThemeContract are unchanged | Partial | - -**Known out-of-scope CI gap surfaced while validating the row above (PR #398):** the live `windows-shell` UIA gate's "New Loop" assertion invoked via the sidebar's `project-new-loop-*` element (`Tools/windows/uia-live-gate.ps1`, "project-row New Loop did not open the node form") fails intermittently/deterministically across unrelated branches (reproduced on `coneilen-microsoft-canvas-workspace-detail-parity` and `coneilen-microsoft-updates-dialogs-quick-chats-parity` as well, with no relation to dialog rendering code). This is pre-existing test-infrastructure flakiness, not a visual-polish regression; it is out of this pass's scope and is flagged here for a dedicated follow-up. - -**Known shared-environment gate instability surfaced while validating the Window toolbar/Update command rows above:** with the local shell toolchain unblocked (PR #433), multiple parity sessions now build and run `graphcode-windows.exe`/`zmx.exe` concurrently on the same interactive desktop. The pre-existing worktree reorder/removal focus-retention stress block in `Tools/windows/uia-live-gate.ps1` (`Retain-FocusWithRetry`, its `Start-Job` concurrent-UIA-read stress, and the plain `Get-DirectChildren` tree walks around it) repeatedly hit raw, uncaught COM exceptions (`GetFirstChild`/`GetNextSibling` "Could not open the process token"/"Unrecognized error.") at different, unrelated call sites across many local runs, and a separate run was independently derailed by another desktop application (Chrome) stealing the foreground window during a modal-dialog wait. None of this reproduced from this branch's own changes — a minimal, standalone re-run that skips straight to the Update command assertions using the same shell process, native menu, and gate helpers passed cleanly and repeatably. This matches flakiness independently reported by sibling parity sessions and is a pre-existing, shared test-infrastructure limitation, not a product regression; it blocked getting one single uninterrupted top-to-bottom `uia-live-gate.ps1` run this session and is flagged here for follow-up (likely hardening `Get-DirectChildren`/`Retain-FocusWithRetry` against concurrent-desktop contention). - -## Audit conclusion - -The Windows branch has substantial protocol, lifecycle, persistence, terminal, graph -mutation, tray, and packaging behavior, but it does **not** currently have complete UI -or screen parity. The previous parity statement conflated backend reachability with -user-visible parity. The largest corrective work is: - -1. Restore and complete the application menu and navigation state model. -2. Implement the sidebar, global graph, Quick Chats canvas, project canvas chrome, and - loop workspace as distinct application-owned surfaces. -3. Replace raw protocol forms with structured node, edge, settings, repository, and - worktree screens. -4. Implement the missing update, project-management, rename/delete, empty, and - connection-info states. -5. Expand UI Automation and live walkthrough coverage to every row above before any - complete-parity claim. +# Windows UI parity ledger + +This is a source-derived completion ledger, not a requirements sketch. A row is +`Validated` only when the Windows implementation exposes the same user-visible +information and actions as macOS and has runtime evidence. Platform-native chrome may +differ, but hiding a feature behind an undocumented shortcut or replacing a structured +screen with raw protocol fields is not parity. + +Statuses: + +- `Validated`: source mapping, automated coverage, and live walkthrough agree. +- `Partial`: some behavior exists, but visible controls, state, or interaction is absent + or materially different. +- `Missing`: no equivalent reachable Windows surface. +- `Blocked`: requires a deliberate platform decision or unavailable dependency. +- `Divergent`: Windows exposes a different product concept in the place where the macOS + surface belongs; it must be separated or redesigned before parity. + +## Application shell and navigation + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Main split view | Persistent sidebar; detail switches among welcome, global graph, project canvas, Quick Chats canvas, and loop workspace | Explicit destinations exist; the earlier stub walkthrough covered project → overview → workspace → Show in Graph with sidebar retention. The shared header now follows destination identity, distinguishes a Quick Chat workspace from a previously selected project, and keeps detail-panel toggling inside the loop workspace. Production-state and layout tests cover the destinations, narrow widths, and sidebar/panel independence. The current live attempt stopped at foreground acquisition before UIA root access: no new live UIA SetFocus, F6, Jump, pixels, or sidebar-effect proof was obtained, and provider-backed workspace/panel behavior remains unverified | Partial | +| Window toolbar | Needs-you chip, worktree notice, jump field, contextual loop-panel toggle | Render, hit-test, UIA exposure, and focus order share header visibility/layout. Needs-you opens the selected attention target; cached local worktree notices honor the owning project and count/size threshold. Jump opens the existing palette even with no projects. The detail toggle is limited to loop workspaces with supported edge/metric content; workspace/project identity is no longer overpainted. F6 entry/exit, scoped traversal/activation, focus rendering, and marshaled UIA button focus are implemented, while ordinary Tab/Shift+Tab/Ctrl+Tab contracts remain. The header paints inside the buffered logical pass; literal 96/144/192-DPI bounds and hidden native client tests cover logical layout/input and exactly-once physical UIA conversion without rescaling terminal tabs. Production-helper tests, a hidden native Tab-to-command test, and a hidden empty-palette create/cancel test pass. Live focus/event-deadlock, keyboard activation, rendered pixels, sidebar effects, and provider-backed panel proof are still absent because foreground acquisition failed. Cross-project worktree-notice discovery/aggregation, summary/board/mailroom panel content, and macOS visual parity remain separate residuals; private live tooling is not CI coverage | Partial | +| Jump palette | Search field, ranked cross-project results, type/state/project context, mouse and keyboard selection | Ctrl+P and Ctrl+J open a native modal palette with live exact-ID, exact-title, title-prefix, and substring ranking across projects. Results visibly include project, loop type, and state; Up/Down, Return, Escape, and mouse double-click are supported. The deterministic UIA gate verifies a visible search field, contextual cross-project results, and keyboard navigation changing the selected loop. | Validated | +| File/Loop/Terminal menus | Discoverable project, worktree, navigation, workspace, update, settings, and help commands with state-aware enablement | Startup menu replacement and UTF-16 corruption are fixed and the five readable runtime groups were probed. Reclaim/Reveal require a selected row (`worktreeRowSelected()` retains dedicated unit coverage); Save requires the Worktrees dialog open. Loop commands now gray out when they have no actionable target: Jump requires a loaded loop anywhere, Next/Previous require at least two loops or one unselected loop in the active graph, Create Edge requires two active-graph loops, and Stop requires a selected loop. These distinct thresholds follow their different production handlers and are intentional. Terminal Next/Previous Tab and Focus Next/Previous Pane require multiple tabs or panes in the selected tab. A hidden-window test calls `MainWindow.updateMenu` and verifies the real native HMENU `MF_GRAYED` bits in both unavailable and available states; this is unit/runtime Win32-menu evidence, not a live application walkthrough. The Worktrees-dialog UIA gate evidence remains outstanding: obtaining it requires launching the shell in a foreground desktop session, and no application executable is built in this worktree; provider builds/provisioning and foreground-gated UIA automation were not performed. Broader project-management parity remains incomplete | Partial | +| Workspace lifecycle | List/switch, create, rename, delete with fail-closed confirmation, and keyboard/UIA reachability | Windows discovers `Default` plus `.graphcode-*` siblings and exposes New/Rename/Delete and Ctrl+Alt paging. Manage now has an owned row model/native list, Default/creation-time/name-tie order, identity-deduplicated current-outside-home inclusion, full-path detail, current/default/open/uncertain-window refusals, and captured-identity Open/New/Rename handoff after modal teardown. An App-owned single joinable reader supplies bounded **saved top-level** graph-header counts, not live/descendant totals; current live content, missing/unreadable/invalid/duplicate/limited/unsupported-location data remain explicitly unavailable, not partial zero. Fixed local/non-reparse reads use existing project JSON and the documented mailroom-array exception, no daemon/backend or window activation. Done/Escape cancels; pending actions wait for reader cancellation/join while the modal pumps, and shutdown drains before allocator teardown (a stalled local disk can delay shutdown). Manager Delete is disabled pending recoverable deletion/teardown; existing menu deletion and ordinary menu ordering/Open/New semantics are unchanged. Next/Previous now reread the manager-ordered list, include validated current outside home, filter identified running windows, wrap both ways, recheck the target, and use a restore-only API with no launcher. Closed/disappeared targets never cold-open; any unidentified flag or lookup/restore failure refuses with status. Existing SID/session/class/published-metadata checks are preserved, not executable-path attestation. Menu capability counts known rows plus implicit current, without periodic window polling. Injected production-helper RED/GREEN, allocation-failure/owned-refresh, ordering/dedup/offset/race/refusal tests and pure menu-capability tests execute in the existing App/MainWindow roots; full native roots compile/link without execution and the ReleaseSafe app builds without launch. Manager coverage remains pure owned-data/explicit disposable fixtures, controlled memory-only joined threads, and filtered form/App data seams; no new shown manager/native-control/window-lookup/UIA/runtime proof is claimed. Existing lifecycle helper and never-shown native-control regressions cover accepted text lifetime, allocation cleanup, normalized naming/collisions, lexical identity, exact-window and one-launch routing, Default/current/open refusals, child-only support/daemon-pipe isolation, canonical plus legacy reservations, unidentified-window refusal, fail-closed Settings rebinding, and disposable confirmed mutation preservation. Lexical identity is not junction equivalence. Shown-dialog accessibility/keyboard/layout, full multi-instance/save-reload behavior, reliable live totals, real running-cycle keyboard/window proof, and recoverable deletion with session/daemon teardown remain residuals. Shared-host menu failures still have an unproven cause; no full lifecycle parity or host-only diagnosis is claimed | Partial | +| Help menu | GraphCode Basics and normal About entry | The live Help menu exposes GraphCode Basics, which reopens onboarding, and About GraphCode, which opens a native versioned product dialog. The populated UIA gate verifies the dialog identity, version text, and close behavior | Validated | +| Update command | Check for Updates, disabled while checking/installing | Reachable from the Help menu's native menu bar, immediately reports "Checking for updates..." status and disables the command the instant a check is invoked, and — after fixing a real bug where the background check's completion never refreshed the menu bit because the only refresh path was gated on unrelated daemon-connectivity state — now reliably re-enables the command once the check settles. A dedicated live UIA gate assertion (reading the real native `HMENU` bit via `GetMenuState`, not the fragile UIA tree) and a Win32-window-backed unit test both confirm the disable/re-enable cycle. When the settled result is an available update, the gate additionally waits for the real offer, dismisses it via Later, verifies the modal disappears, and requires the shell owner to be enabled again before continuing; failure/no-update outcomes are required not to present an offer. In-app installation is now implemented as a separate scope (see Install progress/Relaunch prompt, Partial) | Validated | +| Tray lifecycle | Restore and exit without foreground daemon window | `TrayLive.Tests.ps1` exercises the physical icon, Open, close-to-hide, single-instance restore, Explorer recovery, popup contents, and visible Exit activation | Validated | +| Connection failure presentation | Explicit visible failure without replacing normal navigation | A persistent inline canvas banner now reports daemon unavailability while leaving sidebar and destination navigation intact; ingress errors take precedence when present. The live UIA gate forces the disconnected state and verifies the dedicated banner text and bounds | Validated | + +Running-cycle follow-up evidence: the final lookup actually used for activation +now refuses mixed identified/unidentified results before activating that same +target; ordinary Open keeps its target-first policy. The real accelerator +descriptor and eligible pretranslation/top-level fallback now wire Ctrl+Alt +paging to the same cycle action before terminal-child dispatch. Pure injected +final-lookup, descriptor/modifier, and message-data tests cover these paths and +preserve Ctrl-only tabs/F6/F10 routing. The corrected native menu regression is +compiled, not locally executed. This is not an atomic global-window snapshot or +native keyboard/accelerator/window proof; the workspace row remains Partial. + +## First-run and empty states + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Four-page onboarding | Visual terminology tour, Skip/Back/Continue/Get Started, backend selection, reopen, persisted seen state | Custom rounded Win32 onboarding; all pages exercised and persistence verified | Validated | +| No-project Welcome detail | Graph icon, pitch, explanatory copy, Open Folder action, inline error | Windows matches the centered Graph identity, pitch, explanatory copy, and single Open Folder action. Persistent project-ingress failures now also render as a bounded, wrapped inline canvas alert without replacing navigation; focused geometry coverage and the populated UIA gate verify the alert text and live bounds | Validated | +| Empty global graph | “Nothing running yet”, explanatory copy, Open Folder action, New Loop action | The dedicated overview empty state exposes both bounded Open Folder and New Loop actions; New Loop targets the daemon's `graphcode://global` project. The live UIA gate switches to an empty model, verifies both visible native controls, invokes New Loop, and observes the node form | Validated | +| Empty project canvas | Project-specific empty message and New Loop action | The dedicated “No loops yet” project state exposes its visible New Loop action. The live UIA gate installs an empty local project, invokes that exact command, and observes the project-scoped node form | Validated | +| Empty Quick Chats canvas | Explanation of Quick Chats and New Chat action | Live walkthrough verified the dedicated explanation and New Chat action with corrected non-overlapping layout | Validated | + +## Sidebar + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Graph row | Pinned global graph row with graph glyph | The sidebar now keeps a dedicated Graph destination visible even with no open project, labels it with a graph identity glyph, and routes it through the existing global-overview hit target and UIA destination | Validated | +| Quick Chats group | Selectable header, hover New Chat, disclosure, child rows | The native header remains selectable, reveals a hover-only New Chat action and disclosure, and exposes stable selectable child rows with Rename/Delete context actions. Focused menu tests cover stable chat identity; the live UIA gate invokes New Chat, collapses and restores children, and verifies child runtime identity survives. | Validated | +| Local/remote sections | Group labels, independent collapse, folder/network glyphs | LOCAL and REMOTE retain local/folder and remote/network identity and now toggle independently as native section actions. Focused layout coverage validates mixed ordering, and the live UIA gate collapses LOCAL while proving the REMOTE row and its stable automation identity remain present before restoring LOCAL. | Validated | +| Project rows | Selection, folder type, hover New Loop, disclosure | Open project rows retain selection and local/remote glyphs, reveal hover-only New Loop and disclosure controls, and collapse/restore their own loop tree without changing row identity. The live UIA gate invokes the project-row New Loop action into the real native node form and exercises project collapse/expand through stable UIA actions. | Validated | +| Nested loop tree | Edge-derived hierarchy, persisted expansion, drag reorder of roots | Handoff edges derive a cycle-safe root/descendant tree; nested rows disclose and collapse by stable node ID, expanded IDs persist atomically in the GraphCode support directory, and root rows now reorder through live pointer drag backed by the existing transactional `root` records. Focused Sidebar/Wire coverage and the deterministic UIA gate verify observable reorder plus emission of the new `sidebarNodesReordered` daemon command for server-side persistence parity. | Validated | +| Loop row presentation | Type stripe, title, elapsed time, state indicator | Rows now show a loop-type stripe, title, compact state indicator, and a compact elapsed value derived from `createdAt`. `Sidebar.elapsedText` now has focused boundary coverage for every unit rollover (seconds/minutes/hours/days) and its invalid-input guards (`created_at<=0`, `now<=created_at`), reverified via `zig test` and the full `WindowsShell.Tests.ps1` suite. This is still text painted directly onto the sidebar's `HDC` with no UIA identity of its own (the same limitation `Sidebar.updateBannerAt` had before this change), so a live assertion that reads the *rendered pixels* of the elapsed column was not captured this session; only the formatting logic and the row's overall live-rendering-without-crashing are executable evidence today | Partial | +| Project context menu | Move, worktrees, settings, Explorer, remote info, close, remove, delete loops/project | Project rows expose the lifecycle actions plus the Windows Recycle Bin path for local folders. Move is deliberately **not** an Explorer `/select` alias: `GraphContextMenu.moveProjectMenuItem()` appends "Move Project... (unavailable: daemon support required)" with `MF_GRAYED` while `Wire.supportsProjectRelocation()` is false, and the stale command path surfaces that explicit reason instead of opening Explorer. The live UIA gate now drives the real `TrackPopupMenu` popup (`MainWindow.wm_uia_context_menu` -> the same `GraphContextMenu.show()` the mouse path calls) and asserts the live menu's ordered items, that Move is command 5149 with that exact text and a disabled state, that a remote project's menu omits Move/Recycle Bin/Explorer entirely, and that the popup dismisses without wedging the shell. Note the observation channel: a popup menu appears in the UIA tree only as an empty Pane with no `MenuItem` children, so item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, but not UIA-tree evidence. | Validated | +| Loop context menu | Open, composite actions, rename, stop, delete | Sidebar and canvas loop rows share stable-ID Open, Rename, Stop, and Delete actions. Composite cards expose Open Group, Pilot Once, and Arm Schedule; the drilled-in canvas addresses mutations through the parent composite. The live UIA gate now opens and reads all three `wm_uia_context_menu` loop variants: target 3 (a plain wired loop) asserts Open/Rename/Stop/Delete are present and that every composite-only and unwired-only command is absent; target 6 (a composite, not-yet-piloted loop) asserts Open Group/Pilot Once/Arm Schedule are present with Arm Schedule rendered `MF_GRAYED` (not piloted), and that unwired-only commands are absent; target 7 (an unwired loop node, added live via the sidebar-reorder fixture mutation) asserts Wire it up/Mark as entry are present and composite-only commands are absent. As with the project context menu, item identity/text/enabled state are read through `MN_GETHMENU` and the Win32 menu API against the HMENU the shell handed to `TrackPopupMenu` — live evidence of what the shell actually renders, not UIA-tree evidence | Validated | +| Recent projects | Reachable from Add Folder menu | Recent and currently-open projects are now exposed as distinct sidebar rows, with unopened recents remaining under the LOCAL/REMOTE sections while open workspaces use separate `open-project` identities. The deterministic UIA gate verifies the split presentation and section behavior. The live gate now also walks the Recent Folders submenu reachable from Add Folder end-to-end: it sends a real `WM_INITMENUPOPUP` (the message `App.zig` uses to refresh `recent_folders` from the live model before a popup shows, so the read reflects the fixture's recent projects rather than pre-fixture placeholder state), reads the submenu's live items in fixture order with their stable `recent_folder_command_base`-derived command IDs, and invokes the second entry through the real `WM_COMMAND` route, confirming the shell routes it without crashing | Validated | +| Add Folder menu | Open Folder, Clone, Add Remote, recents | File now groups Open Folder, Clone Repository, Add Remote Repository, and Add Codespace under Add Folder and adds a dedicated Recent Folders submenu with its own command range; that submenu is now located rather than positionally indexed, so a new ingress entry can no longer silently retarget the rebuild. Focused MainWindow coverage validates the native menu structure and recent-folder command wiring. The live UIA gate now reads the Add Folder submenu directly off the live `HMENU` (`GetMenu`/`GetSubMenu`, not `TrackPopupMenu`, since this is the persistent menu bar) and asserts all four action labels plus the Recent Folders submenu; see the Recent projects row above for the live Recent Folders walkthrough this shares | Validated | +| Sidebar update banner | Available version and click-to-install action | A persistent footer banner now shows the retained offered version and reopens the native update offer when clicked. The live UIA gate now drives this through the real click path rather than the `GRAPHCODE_UIA_SHOW_UPDATE` bypass: since `Sidebar.updateBannerRect` has no UIA identity of its own, the gate computes the banner's live pixel geometry from the shell's real client height (matching the same viewport-bottom formula the paint code uses) and posts a genuine synthetic `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at that point, then verifies the real `GraphCode Update Available` dialog opens with the offered `GraphCode 9.9.9-test` version text, dismisses it via the live Later command, and confirms the shell survives. **In-app install remains deliberately Blocked** — the offer still hands installation off to the verified release page, and this row's evidence does not claim otherwise | Validated | +| Sidebar error footer | Persistent, scoped project-ingress error | Folder, clone, remote, and daemon-open failures now persist in a dedicated red sidebar footer independently of transient status. Successful project ingress clears it, wrapped layout preserves long messages, and the deterministic UIA gate verifies the dedicated footer identity plus multi-line bounds below the update offer. | Validated | +| Needs-you section | Navigable list with reason/project and Stop action | Up to four entries now expose selection, explicit reason copy, stable UIA identities, click/UIA navigation, and a dedicated Stop action. Focused routing coverage plus the deterministic UIA gate verify Stop targets the populated entry's real project path and loop ID. | Validated | +| Activity strip | Optional bottom strip, summary, attention-only filter, horizontally scrolling actionable events | Activity events retain project/node identity and timestamps, render timestamped cards, expose stable UIA rows plus scroll controls, and now keep a real horizontal viewport with an attention-only filter. Focused Sidebar coverage and the deterministic UIA gate verify scroll-state changes, attention-only filtering, and card navigation into the selected loop workspace. | Validated | + +## Graph overview and project canvas + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Cross-project global graph | Every open folder as a lane on one canvas | Existing lanes stack vertically, as on macOS. Production geometry tests now use four Alpha loops and two Beta loops, literal accumulated lane/card bounds, both distinct Open/Worktrees targets, transformed hit testing, a recent-only exclusion, and an empty ordinary-folder lane. `App.applyOverviewLaneAction` is the existing lane dispatch extracted without changing its ordering or selection semantics; the real overview callback consumes it before the unchanged loop/pan paths. Never-shown native tests exercise both projects' Open actions and emitted project/card selection and UIA bounds at 96/144/192 DPI. Interleaved graph refresh retains the other project's selected loop and updates stable card identity/geometry. Real scoped Worktrees actions start with no inspection, inspect two independent disposable Git roots, and require exact inspection/dialog paths and emitted primary rows, non-reclaimable primary safety, and preserved sentinel bytes; pre-seeded rows or an Invoke return cannot satisfy them. Deliberate wrong-identity/action/geometry controls are rejected before dispatch, not claimed as disabled-production-dispatcher or historical bug evidence. These are production-helper/model/UIA-data results, not shown rendering, OS input, COM invocation, mounted terminal/focus, or macOS runtime proof. The shared live overview segment still covers one project's two cards; simultaneous two-project capture and loop navigation remain unproved. macOS topology/START furniture, richer lane captions/chips, global-lane filtering, and remote/all-project worktree binding behavior also remain outside this slice | Partial | +| Folder lanes/bands | Project caption, worktree chip, open/close and folder actions | Overview lanes render distinct Open and Worktrees actions beside the project caption; click routing selects the project or opens scoped worktree inspection. Focused geometry/input coverage passes, and the local Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` posts real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` messages at the lane's Open and Worktrees hit-test rects against the live executable and verifies Open routes to the project canvas (synchronized cards render at a new position) and Worktrees opens the scoped inspection view, both confirmed passing across many consecutive live runs. This also uncovered and fixed two real accessibility bugs along the way: `App.zig`'s `.overview` mouse-click switch arm and its `.cycle_attention` action handler were both missing the `syncAccessibility()` call that keeps the live UIA tree in sync with what is rendered, so a lane's Open/Worktrees click previously had no observable effect through the accessibility tree even though the underlying surface did change | Validated | +| Notebook grid | Grid pans and zooms with canvas | `GraphCanvas.drawGrid` derives its cell size and offset from the exact same `CanvasState.zoom`/`pan_x`/`pan_y` fields consumed by `overviewCardBounds`, `overviewLaneBounds`, and the loop-card geometry, so the same focused pan/zoom coverage (`GraphCanvas.zig`: "canvas hit testing follows pan and zoom", "overview and quick chat geometry applies pan and zoom consistently") indirectly proves the grid cannot desynchronize from the content it underlays. The Windows shell toolchain blocker is resolved, but the grid itself is a 1px `0x00161815` GDI line pattern with no UIA surface of its own, and this session did not add a live pixel-scan assertion (the connector-handle and attention-rail blocks already show this pattern is feasible) to directly confirm grid line spacing changes with zoom in the running executable. Left Partial rather than claim live evidence that was not actually captured | Partial | +| Pan and anchored zoom | Pan, pointer-centered wheel/pinch zoom | Mouse pan and pointer-centered wheel zoom remain intact and unchanged, with the same focused regression coverage as before (`GraphCanvas.zig`: "canvas zoom keeps the graph point beneath the cursor stable", "canvas wheel zoom scales high-resolution trackpad deltas"). Native touchscreen pinch-zoom is now implemented and routed through the main window: `MainWindow.zig` registers only `GID_ZOOM` via `SetGestureConfig`, leaving every other gesture class (`GID_PAN`/`GID_ROTATE`/`GID_TWOFINGERTAP`/`GID_PRESSANDTAP`) at its existing OS default rather than explicitly blocking gestures this app has no opinion on, with a real registration test against a genuine `HWND` plus two independent negative controls — a malformed native `SetGestureConfig` call and a genuinely invalid `HWND` passed straight through the production `registerCanvasGestureConfig` helper itself — proving the helper's own `GetLastError()` capture path actually fires, not just the raw Win32 API. `App.zig`'s `WM_GESTURE` case decodes `GID_ZOOM` via a pure `CanvasInput.classifyGesture` decision table (including a distinct outcome for a gesture delivered as a single combined begin+end message, so it can never reuse a stale prior gesture's baseline), requires the active surface to actually render the graph canvas (not just the wheel-region rectangle, which the terminal workspace surface shares), and applies `GraphCanvas.zig`'s `beginPinchZoom`/`continuePinchZoom`/`endPinchZoom` against a per-gesture identity hash of surface+project so a same-region destination change mid-gesture (surface switch, or project switch while still graph-capable) resets the baseline instead of silently continuing to scale the wrong canvas; a failed `GetClientRect` is guarded and treated as unhandled rather than classified against an undefined rect, and the gesture handle is closed before any call that could re-enter the message loop. This is source-mapped, automated routing/unit evidence, not live hardware-input evidence: this session held no live UIA capture slot this pass, so pinch is proven by code mapping and a full deterministic test suite (non-compounding/clamp/zero-distance/lifecycle/context-mismatch/routing matrix, verified with genuine temporary-regression RED/GREEN passes against the production helpers, not GREEN-only), not an actual touchscreen or Precision Touchpad device. Per Microsoft's documented default, Precision Touchpad pinch on a classic Win32 window is emulated as synthetic Ctrl+`WM_MOUSEWHEEL`, not delivered as `WM_GESTURE`, so this implementation targets true touchscreen digitizers specifically; Precision Touchpad pinch behavior is not separately implemented or verified here. Touch-driven pan (`GID_PAN`) remains a genuine unimplemented gap: this app forwards it unhandled rather than half-handling it, but does not explicitly block it either. Left Partial: touchscreen pinch has real source and automated-test coverage but no live device evidence, and touch pan is still unimplemented | Partial | +| Zoom controls | Zoom out, actual size, zoom in, fit with shortcuts/help | Visible bottom-right controls provide zoom out, percentage/actual size, zoom in, and fit. A visible shortcut/help line accompanies the controls; Ctrl+-, Ctrl+0, Ctrl+=, and Ctrl+9 remain represented in the View menu. The Windows shell toolchain blocker is resolved and `Tools\windows\uia-live-gate.ps1` now runs against the live executable: it locates the `zoom-out`, `actual-size`, `zoom-in`, and `fit-canvas` UIA fragments, requires non-empty bounds, resolves each `InvokePattern`, and then actually invokes zoom-in, actual-size, zoom-out, and fit-canvas in sequence against the running shell, all of which completed without error across many consecutive live runs | Validated | +| New Loop canvas button | Visible top-right add action | A live-validated top-right New Loop button is now present on non-empty project canvases and remains centered in the empty state | Validated | +| Composite breadcrumb | Current group, project back action, loop count | Open Group swaps the project canvas to the authoritative nested graph, renders its cards and edges through the normal interactive canvas, and exposes a clickable `Project > Group` breadcrumb with loop count that restores and reselects the parent. Nested graph selection survives daemon refreshes, and the populated live UIA gate invokes Open Group, verifies both nested cards, and invokes the bounded Back breadcrumb to restore the parent canvas | Validated | +| Canvas attention rail | Count/oldest context and Review action | The rail exposes a clickable Review target and uses `createdAt` from the daemon model when present to show a true `oldest ` label alongside the oldest attention item title. Focused hit testing passes, and the Windows shell toolchain blocker is now resolved: the rail has no dedicated UIA element of its own (it is a full-width band GDI hit-test region), so `Tools\windows\uia-live-gate.ps1` posts a real `WM_LBUTTONDOWN`/`WM_LBUTTONUP` at the rail's exact screen rect against the live executable and verifies the click drives `App.zig`'s `.review_attention` -> `selectNextAttention()` routing by observing the resulting `SelectionItemPattern` selection actually move from one card to the NEEDS YOU card, passing across many consecutive live runs | Validated | +| Node positioning | Persisted positions and direct card movement where supported | Project cards can be dragged directly, with movement transformed correctly at non-default zoom, shared geometry/hit testing updated during the drag, and capture-loss cancellation restoring the prior position. Offsets are keyed to stable node identity, remapped across daemon reorder, and atomically persisted under the configured GraphCode support directory. Focused reorder/reload regressions and a real physical drag capture validate the complete flow | Validated | +| Connector handles | Hover handles and drag-to-connect | The right-edge connector tracks hover, paints a visible handle and plus affordance, and preserves the drag-to-connect path. Focused rendering/input coverage passes, and the Windows shell toolchain blocker is now resolved: `Tools\windows\uia-live-gate.ps1` synthesizes real `WM_MOUSEMOVE` hover messages at the source card's outgoing connector position and confirms the live `0x7ACDFF` hover handle pixel actually appears on screen (`Test-ScreenPixelNear`), then drives a full `WM_LBUTTONDOWN`/`WM_MOUSEMOVE`/`WM_LBUTTONUP` drag from that connector onto a second card and confirms the resulting native "Create or edit edge" dialog locks its From/To fields to the exact dragged source and dropped target loop IDs, all passing across many consecutive live runs | Validated | +| Loop card identity | Loop-type stripe, title, state pill, entry/cycle role | Project and overview cards now use loop-type-colored stripes while retaining lifecycle state text, START, UNWIRED, and attention labels. Focused color regression coverage passes; live evidence remains blocked | Partial | +| Loop card live detail | Goal/prompt/check line, progress, metric change, elapsed/backend/model/worktree metadata | Cards prioritize goal, trigger, or check detail, retain current activity, and add metric pass/change text, elapsed age, backend identity, token usage, model tier, and worktree/branch metadata from the same decoded daemon fields used by the workspace loop bar. Focused card metadata tests (`GraphCanvas.zig`: "loop card metadata includes backend elapsed and token usage when reported") pass, and the `windows-shell` CI job's live UIA gate exercises the populated card fixture end to end (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, passing, merged as PR #399) | Validated | +| Loop card attention | Reason-aware amber presentation and primary action | NEEDS YOU cards render a card-level reason-specific primary button: `Reply` for reported awaiting-input sessions and `Inspect` for other attention reasons, both routed through the normal loop-opening path. Focused action-label tests (`GraphCanvas.zig`: "attention cards expose reason-specific primary actions") pass, and the live UIA gate's `attention-action-*` assertion for the deterministic awaiting-input card passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | +| Unwired card recovery | Explanation, Wire it up, Mark as entry | Cards with no inbound or outbound edge now show an explicit UNWIRED warning and recovery explanation. Their native context menu exposes Wire it up, which enters the existing drag-to-connect flow, and Mark as entry, which changes the card to START for the session. Focused role/action tests plus live menu and post-action captures validate the flow | Validated | +| Worktree reclaim offer | Reclaim and Keep actions on resolved card | Safe resolved cards with a matching landed, clean, pushed worktree expose separate Reclaim and Keep targets. Reclaim revalidates safety and Keep suppresses the offer for the session. Canvas Reclaim/Keep descendants are now emitted through the UIA provider and invoke the same fail-closed paths. Focused geometry/safety coverage passes, and `Tools\windows\uia-live-gate.ps1` now asserts the live Reclaim/Keep descendants under the Graph fragment (name, non-empty bounds, InvokePattern, and correct RawView/ControlView sibling linkage) via the `windows-shell` CI job (PR #385, run 35422364203, passing) | Validated | +| Composite card actions | Open Group, Pilot Once, Arm Schedule | Canvas and sidebar composite menus expose all three actions. Open Group is live-validated; nested creates, edits, deletes, edge changes, pilot, and arm commands use the daemon's authoritative `subGraphCommand` envelope; and Arm Schedule is disabled unless the decoded pilot state is exactly `piloted` | Validated | +| Edge presentation | Kind style, fired state, cycle label | Windows retains typed optional cycle guards and derives fired state from authoritative `fireCount`, with missing/null legacy fallback. Raw snapshot JSON reaches the production label formatter for maximum/until/flat-pass and empty-guard summaries; signed/null/malformed inputs, Unicode/long text, owned copies, refresh/composite transitions, and allocation-failure cleanup have executable coverage. Kind styling, selected emphasis, fixed 148-by-20 label bounds, ellipsis, and collision placement are retained. This is exact-string and static-geometry evidence only: full visible wording, the macOS context-menu summary, rendered pixels, and live label evidence remain unverified | Partial | +| Edge creation sheet | Kind/condition/transform/cycle controls with conditional validation | The guided native form provides endpoint selectors, conditional fields, validation, keyboard traversal, scrolling, and recap. Menu and connector creation now share the full draft sender, retaining condition, payload transform, all optional cycle guards, and spawn target. Project/composite identity and endpoint IDs/titles are owned across the modal; stale model/selection/client subgraph addressing and missing or changed endpoint choices are explicitly rejected without retargeting. The update subscription can still observe another cached project or refresh independently: outgoing creation explicitly addresses the captured project. Registered pure production-path tests inspect the real client's outgoing queue, including cached-project selection, Unicode/quotes, callback-induced graph changes, cancellation, and allocation failures. The reduced connector sender fails the same accepted-draft fixture that the full sender passes. Earlier live evidence only opened the form and checked endpoints/recap before cancellation; this fix has no new live modal/keyboard, daemon acceptance, persistence, or cross-platform parity evidence | Partial | +| Custody child creation | New Child Node on unresolved parents, inherited editable backend, daemon-owned custody | Project-canvas/sidebar node menus consume the tested unresolved-only item plan (5119). Owned popup target/settings/child-only exact-project worktree snapshots feed the existing guarded node form and template/attachment continuation. Original popup context is checked before normal initial selection; final guards never reselect, reject project/composite/address drift or deleted/resolved/type/backend-changed parents, and allow rename/reorder/unresolved progress. Tests exercise the production initializer/transfer boundary and real data-only client queue, including cached B while observing A, root-versus-composite same IDs, UUID/null wire fields, cancellation and allocation failures. `createdBy` produces one create command: GraphStore owns the fired handoff/report-back memo and normal startup. No new protocol or parent/session mutation. Native display/action results, overview right-click, daemon acceptance/persistence, and inherited downstream-send failure handling remain unverified. The merged ordinary-creation path now retains the owned worktree snapshots whose evidence is recorded in Node creation sheet, while custody retains its pre-popup owned snapshot; template backend settings remain a separate residual | Partial | +| Edge editing | Preserve identity/configuration and refuse stale edits | Windows uses an owned modal snapshot and one checked `updateEdge`, retaining endpoints, current fireCount, transform/spawn settings and unchanged optional legacy guards. Root/direct-composite scope and current cache are rechecked without treating observation subscription as authorization. Existing serialized daemon commands, blocking, parent roll-up and root publication remain authoritative; deeper new edit wrappers are refused. Production-helper regressions exercise actual Wire JSON/unstarted queues, foreign scopes, refresh, cancellation and allocation cleanup. Edit-only live/submit capture reads complete Unicode text and blocks acceptance on per-field read/allocation errors; injected-reader and initializer tests show no controls. In-memory Swift store tests cover CAS, kind/blocking rules, root publication, preview refusal and serialized legacy-child writeback followed by an edit that retains runtime progress. This is not native editing, daemon acceptance/persistence or macOS runtime evidence; macOS sources expose creation-time configuration and delete, not an existing edit UI | Partial | +| Node creation sheet | Loop-type teaching tiles, conditional fields, backend/model/branch pickers, recap, validation reason | A guided native form provides loop-type/backend/model choices, type-specific fields, explanatory copy, accessible checkboxes, inline validation, keyboard traversal, scrolling, and a live-updating recap. The native Branch picker offers inspection choices plus “This folder”; existing tests cover its empty state and selected binding, and earlier gate evidence covers the recap, Branch picker, and distinct Attach control. Staging now reserves a uniquely named file exclusively: the production append/removal regression first demonstrated A/B/C → remove B → add D overwriting C's bytes, then passed with C's original path and bytes preserved. Offline in-file coverage also checks template-restored/legacy files, preexisting destination collisions, partial-write rollback, allocation failures without orphan copies, invalid inputs, exactly eight files and the ninth rejection, and the exact 10 MiB/+1-byte boundary. Existing attachment DTO/wire tests remain passing; these are no-window helper/filesystem results, not OS input or daemon persistence evidence. Creation choices now own an exact-project snapshot of inspected path/branch strings across cache/model replacement; baseline aliasing and foreign-cache regressions fail before the fix and pass afterward. Noninteractive production-projection tests cover snapshot lifetime, projection without a loaded graph, and allocation-failure cleanup. Separate pure initializer/builder-to-Wire tests verify exact non-default repository/id/path/branch fields, initial/current/template-restored selection, and “This folder” clearing with available choices; legacy empty-choice hidden binding remains unchanged. This does not establish actual native non-default selection, a Git-inspection-to-creation flow, or daemon save/reload. **Still partial:** actual inspected nondefault binding and project-scoped ownership across pumped modals remain unproved; New branch and remote/global branch visibility differ from macOS; authentic OS picker acceptance/cancellation, clipboard paste/drop, and native-input → request → daemon save/reload evidence remain outstanding. Per-file removed-file reclamation and legacy unguarded cleanup remain separate residuals. Creation now retains the original project/composite context and validates after modal teardown, before attachment transfer, across normal, template-load-error, empty-library, and template-loop submissions without switching selection or client scope. Bounded offline production-boundary RED/GREEN and owned temporary-file tests cover stale-scope refusal, loaded-project closure, same-path refresh/promotion, path-only starts, valid empty composites, template continuation, allocation unwinding, and checked cleanup failures. Guarded staging reserves its attachment-directory leaf exclusively; cancellation/refusal and zero-reference acceptance abandon only that owned leaf, while nonzero accepted attachment bytes survive. Zero-reference drafts preserve existing empty-ID/wire behavior. These are helper/model/unstarted-client-queue proofs, not live modal, OS picker, or daemon-persistence evidence; path-only external disappearance and downstream send failure remain limitations | Partial | +| Node update/rename | Dedicated rename prompt and safe typed updates | Rename retains its dedicated safe prompt. Edit Details uses `NativeForms.update` and `sendUpdateNodeForm`; its project/node identity is now owned across the modal message loop and re-resolved afterward, and changed strings are compared against the form's owned initial snapshot rather than potentially freed graph strings. Original numeric scalars and clear-versus-unchanged semantics are preserved. Production-helper tests cover source mutation, unchanged/changed typed fields, cancellation, clearing, and allocation failures. The lifetime test rejects the original borrowed baseline. Live attempts stopped at background-menu activation before reaching editor or rename actions, so app-level dispatch, cancellation, and returned model results remain unverified in this work | Partial | +| Delete confirmations | Named object, consequences, safe default | Loop deletion names the loop and explains graph-connection removal. Edge deletion now names both endpoint loops and the connection kind, explains that the loops remain, re-resolves the stable edge after confirmation, and defaults to cancellation | Validated | +| Canvas context menu | Folder actions on background; complete node/edge actions | Project backgrounds now expose existing Worktrees, Project Settings, and Explorer routes with captured project context; unavailable non-filesystem actions are disabled and global scope omits them. Create Edge remains present but disabled with fewer than two loops. Resolved node menus omit Stop, matching macOS; composite/template/unwired actions and the absence of Message/Memo are preserved. Tests inspect the same native HMENU builder used by `TrackPopupMenu`, including an exact disabled-item assertion that fails when Create Edge is removed. The existing gate already inspects plain/composite/unwired popup contents; its succeeded-node Stop assertion now requires absence. Focused live attempts read the real background popup but did not achieve an action/result, and a minimal native control failed foreground acquisition before opening its menu; the cause remains unproven. Live node/edge/background action results, New Child live proof, and import/export remain deferred; custody child creation and sketch promotion retain their separate Partial evidence rows | Partial | +| Sketch promotion | Promote a sketch to Goal, Turn, or Timed without replacing its identity or session | Canvas and sidebar context menus use one production submenu plan and focused native forms. Owned popup/form snapshots reject stale scope, selection, type, and deletion; initial selection of a different cached project remains supported independently of the observation subscription. Filtered pure tests exercise the actual App selection adapter, typed native-form builder, and unstarted DaemonClient outbound queue, including composite addressing and allocation failures. Three emitted fixtures decode as existing Swift `promoteNode` variants, and the full Windows application builds ReleaseSafe. A native Win32 test attaches the production node popup to a hidden HWND and checks the Goal/Turn/Timed submenu's presence, IDs, labels, and enabled states through the actual HMENU; it does not show or select the popup. Local command construction/queueing and this native menu state are proven, but native keyboard interaction, UIA, app launch, and real-daemon acceptance/persistence remain unexercised | Partial | + +## Quick Chats + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Quick Chats canvas | Band, cards, pan/zoom, add button, empty state | The real executable was exercised with two deterministic chats. The band, transformed cards, top-right New Chat action, bottom-right zoom controls, and 100% → 110% zoom transition were captured live. Context actions are wired, and the populated live UIA gate validates named, bounded, invokable Quick Chat cards plus the populated-canvas New Chat action | Validated | +| Quick Chat cards | Title, chat identity, optional backend badge, open/rename/delete menu | A populated live fixture verified title/default-chat identity/optional backend rendering and direct opening (`openQuickChat` was observed by the protocol stub). Cards now expose Open Chat, Rename, and Delete Chat context actions | Validated | +| Create chat | Visible New Chat controls | Empty and populated Quick Chats canvases expose the New Chat action in the macOS placements. The live UIA gate invokes the populated canvas action and the sidebar/header action; the existing empty-state walkthrough invokes the centered empty-state action | Validated | +| Rename chat | Single title prompt from row/card | Uses a dedicated single-title modal from the card/keyboard action, trims input, and rejects empty titles | Validated | +| Delete chat | Named confirmation explaining session/scrollback deletion | Uses a named warning that explains terminal-session and scrollback removal, defaults to cancellation, and only sends deletion after confirmation | Validated | +| Chat workspace | Opens a persistent terminal workspace | Opening a Quick Chat now exposes a bounded, selected `Quick Chat terminal workspace` UIA surface while the existing terminal panel remains persistent. The full native UIA walkthrough verifies the card invocation and workspace transition without duplicating loop-workspace implementation | Validated | + +## Loop terminal workspace + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| 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. Production is still 120x40 without PTY resize negotiation; 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 | +| 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 | +| Mounted background tabs | Switching preserves live terminal surfaces | Workspace implementation tests still cover the topology, and the live UIA gate now creates a second mounted tab, switches between the original and background tab, and asserts both tab automation identities survive the round trip without shell exit/reconnection | Partial | +| Right loop panel | Minimap, upstream/downstream, fired conditions, metric sparkline, branch/start/usage footer | The full workspace right rail includes the selected-loop map, upstream/downstream cards, fired-edge coloring, edge conditions, branch/worktree identity, metric/goal detail, model tier, a metric-history sparkline from decoded samples, start-time/usage/backend footer text, a collapse/expand control that no longer reserves rail width while hidden, and dedicated UIA children for sparkline/start/usage/toggle (`workspace-detail-sparkline-*`, start, usage, and toggle automation IDs). The live UIA gate asserts those children and toggles collapse/expand, and that gate passed on `windows-shell` (run https://github.com/scgopi/GraphCode/actions/runs/35638849754, merged as PR #399) | Validated | +| Show in Graph | Visible loop-bar and menu action | The live gate invokes the real `workspace-show-graph-*` loop-bar action through `InvokePattern`, requires the exact selected `UIA loop A` `canvas-card-*` identity and selected state with strict workspace-chrome absence, then returns through the exact supported `loop-row-*` identity and requires the same project toolbar, selected-loop bar, and Show in Graph identities. A focused isolated pair at `.graphcode-evidence/show-in-graph-20260924-151455` used source gate SHA-256 `AE4A2C71...`, focused harness `78E6821E...`, and shell `8E0AA9E7...`: GREEN invoked the action and completed the exact round trip (stdout `E94DCA9D...`); RED disabled only that invocation and failed the unchanged chrome-absence assertion (stderr `3198914D...`). This confirms the route without a product fix; graph cards remain intentionally non-invokable. The focused early return did not exercise later downstream-provider rebinding. A full integrated gate pass and live native Loop-menu invocation remain unverified. | Partial | + +**Live-gate infrastructure fix (this session):** the `windows-shell` CI job's `uia-live-gate.ps1` step was, until now, never actually exercising any of the workspace chrome above: `App.init()` unconditionally skipped `Workspace.init()` under `GRAPHCODE_UIA_GATE=1` regardless of whether a real `zmx` executable was supplied (a pre-existing guard predating this workstream), so every "Partial" row above had never been run against a real workspace at all. Fixed in `App.zig` to build the real workspace under the gate whenever `GRAPHCODE_ZMX` is present. That surfaced a second, genuine regression: the newly-real terminal surface competed for native Win32 keyboard focus with the rest of the UI after navigating away from the workspace (`App.openGlobalOverview()` and friends). Root-caused to `Workspace.poll()` (driven by the main window's 100ms `WM_TIMER`) unconditionally draining terminal output and calling `winghostty_surface_notify_accessibility_text()` regardless of workspace visibility, which kept re-asserting UI Automation focus on the terminal no matter what Win32-level focus fixes were made. Fixed by adding `Workspace.collapse()`/`Workspace.collapsed`, skipping `resize()`/`syncTopology()`'s pane refocus and terminal-output polling entirely while the workspace is hidden, plus a `WM_ACTIVATE` handler that reasserts the app's own focus policy after `DefWindowProc`'s default child-focus restoration on window reactivation. All of this is now covered by the passing `windows-shell` CI job (commits `a31813b`..`cba010f`, run https://github.com/scgopi/GraphCode/actions/runs/35415967793). Note: the separate `windows-spikes`/`windows-hardening` jobs (`validate.ps1 -Task all`) run the identical gate script but under much heavier CI load and still intermittently hit this same assertion's 15-second retry window; this has been confirmed as pre-existing, cross-branch flakiness unrelated to this workstream (an unrelated sibling branch, `coneilen-microsoft-repository-settings-parity`, shows both a pass and an unrelated failure on the same job across consecutive runs), not a regression introduced here. + +## Repository ingress + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Open Folder | Native picker from Welcome and Add Folder menu | Welcome and File menu commands use the Windows folder-only File Open dialog with filesystem/path validation. The live UIA gate invokes the empty-state action, verifies the titled native picker, and cancels it safely | Validated | +| Clone Repository sheet | Repository, location picker, derived folder, branch, depth, progress, inline failure, cancel | Clone runs behind a progress-capable native operation sheet with live output, cancellation, terminal status, and shared dark painting. Windows-shell contracts pass. The UIA gate opens the real sheet, verifies URL/destination/branch/depth fields, submits it empty to verify the inline URL error, and cancels it | Validated | +| Add Remote Repository sheet | Server/user/port/path, explanation, validation progress, inline selectable error | SSH validation runs on a worker while a validation sheet remains open and Connect is unavailable until completion. It shares Clone’s dark themed native dialog class. Windows-shell contracts pass. The UIA gate opens the real “Add SSH Repository” sheet, verifies host/user/port/path fields, submits it empty to verify inline validation, and cancels it | Validated | +| Remote Connection info | Read-only selectable connection sheet | Remote project context menus expose a dedicated read-only connection-information dialog with the encoded remote project identity and management guidance. The live UIA gate opens the native sheet, verifies both pieces of content, and closes it | Validated | +| Add Codespace sheet | Authenticated codespace discovery, selection, workspace path, validation progress, empty/error/retry/cancel, opens the remote project | `Codespaces.zig` + `WindowsCodespaceDialog.zig` provide discovery, failure remediation, validation, submit gating, and `codespace://` project opening. Deterministic Codespaces/dialog/windows-shell coverage remains green. **Still partial:** the available token lacks `codespace` scope; no real discovery-success, selection, validated dial, or rendered-sheet UIA walkthrough was possible. Only the 403/remediation path was exercised against real `gh` | Partial | + +## Settings and worktrees + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Product Settings window | Backend, three permission pickers, model picker/auto toggle, activity, briefing, beta with explanatory copy | `WindowsProductSettings.zig` exposes native backend/model/Claude/Copilot/Codex selectors, routing/activity/briefing/beta controls, macOS-equivalent consequence copy, and Save/Cancel. Focused tests cover settings preservation and selector copy; `GRAPHCODE_UIA_GATE` mutation 15 opens the real window against an isolated settings file, verifies every required visible control/explanation, proves control-targeted Return saves while preserving unknown fields, and proves Escape cancels byte-for-byte | Validated | +| Infrastructure diagnostics | If retained, separate advanced surface | Daemon pipe/support-directory overrides are now explicitly labeled “Advanced Connection Settings...” while the normal Settings command opens product settings | Validated | +| Project Settings sheet | Resolve policy radio rows, safety explanation, size/count thresholds, immediate save | Valid threshold/radio edits persist immediately, while empty/partial threshold input preserves the last valid persisted value; Done remains dismiss-only. The shared dark native form is covered by focused and Windows-shell tests. The UIA gate uses the gate-only hook to open it and verifies policy, threshold-unit, and worktree content before cancelling | Validated | +| Worktree sweep sheet | Safe/look/in-use grouping, size summaries, default selections, reveal, inline destructive confirmation, recovery note | Sweep storage supports 256 rows, real directory sizes and aggregate totals, reveal, and a second destructive confirmation for dirty rows. Focused and Windows-shell coverage pass. The UIA gate opens the real sheet through the gate-only hook and verifies safe/look group rendering and Remove Selected. The fixture proves sheet rendering and interaction affordances, not Git worktree discovery | Validated | +| Worktree notice chip | Threshold-driven titlebar and lane notice | Explicit inspection installs compact value observations on each exact `GraphSummary` owner; replacing the full inspection, selecting another project, or reordering lanes does not transfer its counts. Overview chips share one presentation/geometry source for paint, hit testing, and path-keyed UIA data, reuse the existing Worktrees action, and show uninspected, incomplete-size, policy-unavailable, stale, and failed-refresh states without fabricated zero/freshness. Checked policy outcomes distinguish missing/legacy defaults from unreadable or malformed configuration. Failed policy writes also reconcile the captured owner's checked readback while preserving the original write error. The shared inclusive count-or-binary-GiB evaluator uses exact known bytes; partial measurements are approximate, and unknown policy or stale observations cannot claim a configured breach. Relevant binding/state/connection/mutation signals stale observations; nested comparisons reuse the existing decoder and scoped IDs under depth, work, and scratch-memory bounds, and comparison uncertainty remains explicitly stale. Title/position-only edits and supported reordering do not manufacture a refresh. Existing owner close/restore eviction removes observations. An exact-current-owner raw-inspection accessor keeps correct-owner rows visible below threshold or when stale/policy-unknown, while excluding foreign current-root rows without changing retained modal state. Pure production-helper/mapper tests cover boundaries, atomic replacement, ownership, nested changes/limits, injected partial-write reconciliation, error states, old action rectangles, path resolution, and literal 96/144/192-DPI UIA-data bounds; the sizing, rounding-label, raw-inspection, nested-binding, and failed-write regressions have RED/GREEN evidence. These are synthetic-data/helper results, not actual filesystem-failure, Git-inspection, native modal, keyboard/click, or COM/UIA walkthrough evidence. Windows still counts all inspection rows including primary/prunable and sums logical file bytes; macOS excludes the opened checkout and sums non-prunable allocated usage. **Still partial:** automatic discovery, global titlebar aggregation, remote/project-canvas band parity, and authentic multi-lane/boundary review-action proof remain outstanding | Partial | + +**Bounded worktree subprocess reliability (2026-09-26):** local job-bounded +`WorktreeGitProcess.Tests.ps1` calls the actual production helper. The baseline +with only `create_no_window` launch instrumentation reproduced upper/mixed-case +repository redirection, outside index/object writes, and an unread real-Git +stderr hang. Child-only environment scoping, concurrent bounded pipe collection, +and owned-child/result cleanup now cover those cases. Real selected/forced +removals and synthetic nonempty output from all three mutation paths exercise +allocation ownership; synthetic auth/config preservation does not use secrets. +The normal WindowsShell runner invokes this suite. No production wall-clock +deadline, OS wait/kill fault injection, live UI/provider walkthrough, or full +parity is claimed. The pre-existing direct `reclaim` command's self-removal +failure on the tested Windows Git remains unchanged; the synthetic direct-call +case proves ownership only. All existing Partial rows remain Partial. + +## Updates and dialogs + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| Available update alert | Install, Release Notes, Later | The native offer now conditions Install on whether the real feed check resolved a Windows asset URL (`WindowsUpdates.zig`/`App.showAvailableUpdate`): when one exists Install is enabled and triggers the real download/verify/extract/upgrade pipeline; when none exists (the real, currently-live state — see Install progress) Install stays visibly disabled with an honest reason, replacing the old permanent "not implemented" label. `UpdateOfferDialog.zig`'s `buttonsFor(installable)` is unit-tested for both states, and a real, live `feed-check` run against the actual `scgopi/GraphCode` releases API (`Tools/windows/Tests/WindowsUpdateInstall.Live.Tests.ps1`) confirms the disabled path is genuine, not simulated, for the current release. What is **not** live-witnessed: clicking Install through a real enabled offer end-to-end in the running UI, because no Windows release asset currently exists to enable it against — that gap is honestly disclosed rather than faked | Partial | +| Install progress | In-window progress indicator | `WindowsUpdateInstall.zig` now implements the full download → SHA-256 checksum verify → extract → `GraphCode-Setup.ps1 -Command Upgrade` pipeline, reusing the existing packaging verification/rollback logic rather than a second copy, and reports phase/fraction progress through a `ProgressFn` callback. `UpdateInstallDialog.zig` renders that progress in a native window (download %, verifying, extracting, installing) plus a failure state; both are unit-tested (14 tests total across the two files, part of the 42-file/273-test hermetic `WindowsShell.Tests.ps1` suite). Real, non-simulated live evidence (`WindowsUpdateInstall.Live.Tests.ps1`, invoked directly — not part of the hermetic suite since it makes real network calls): a real HTTPS download of a real multi-megabyte GitHub release asset streams genuine progress (100+ real progress reports, 0→100%) and its SHA-256 is verified against the asset's real published digest before extraction is attempted; a deliberately wrong digest is rejected with `ChecksumMismatch` strictly before extraction, proving the checksum gate is not vacuous. Honestly out of scope and **not** provable right now: extracting and upgrading a real Windows ZIP asset end-to-end, because the last recorded and just-reconfirmed-live asset check found only macOS DMGs published — there is no real Windows asset to extract. Also unproven: whether `Move-InstallDirectory`'s rename succeeds while `graphcode-windows.exe` is the actual running, self-updating process (the existing `Packaging.RealLifecycle.Tests.ps1` proves the *opposite* guarantee — that a locked file blocks and rolls back — not this scenario) | Partial | +| Relaunch prompt | Relaunch Now/Later and session continuity explanation | `UpdateInstallDialog.zig` presents Relaunch Now / Later with session-continuity copy after a successful install, and `App.runInstall`/`relaunchAfterUpdate` wire the outcome: Relaunch Now respawns the executable (same `CreateProcessW` pattern as `launchWorkspace`) and then quits the current process; Later leaves the update staged and shows an honest status message. Pure logic (`relaunch_message`, outcome handling) is unit-tested; the real Win32 window/thread code compiles and runs but is not live-driven by UI automation in this PR. This row cannot move past `Partial` genuinely: there is no real Windows release asset to drive a real install to completion today, so the actual relaunch — and whether zmx-backed terminal sessions survive an Upgrade-triggered restart specifically, as opposed to the differently-scoped scenario `DaemonHandoff.Live.Tests.ps1` already covers — remains unproven live. Faking that would violate this fleet's evidence policy, so the row is left honestly `Partial` rather than asserted `Validated` | Partial | +| Install failure | Download in Browser/Cancel with reason | The Windows flow exposes Release Notes/Later and a disabled Install action explaining that in-app installation is not implemented, then hands off through the verified browser release page when notes are requested. This remains Partial until download/install failure handling exists | Partial | +| Loop rename | Title field, Return submits, explanatory text | The dedicated single-title modal explains where the title appears, prepopulates the current value, trims and validates submission, and re-resolves the stable loop ID after the modal. The populated UIA gate edits the native field and verifies Return submits and closes the dialog | Validated | +| Loop delete | Named loop and full consequence message | Names the loop, explains graph-connection removal, and defaults to cancellation | Validated | +| Chat rename/delete | Dedicated prompts | Dedicated single-title rename modal and named fail-closed deletion warning are wired from card actions and shortcuts | Validated | +| Project delete loops | Dedicated confirmation | Sidebar project menus expose Delete All Loops through one fail-closed implementation with graph and filesystem consequence copy, safe cancellation default, and the dedicated daemon command. The live UIA gate verifies the native confirmation and cancellation path | Validated | +| Project remove/trash | Distinct reversible remove and filesystem Trash choices | Remove from GraphCode is distinct, confirmed, and explicitly preserves files. Local project menus now add a separate confirmed Move Folder to Recycle Bin action using the Windows undo-capable shell operation; remote projects retain only the GraphCode removal action | Validated | + +## Accessibility, input, and visual behavior + +| macOS surface | Required visible behavior | Windows evidence | Status | +|---|---|---|---| +| UI Automation tree | Names, roles, selection, invoke/toggle, focus, live status for every visible surface | The synchronized live C++ provider exposes stable project rows, loop rows, project/overview/Quick Chat cards, worktree rows, destinations, canvas primary action, zoom controls, policy actions, focus, selection-change events, and status. The live gate uses explicitly in-process deterministic fixtures to validate populated RawView/ControlView navigation, real bounds, observable Quick Chat/workspace invocation effects, tagged-command isolation, identity-preserving reorder/removal, events, concurrency, and teardown. The Workspace menu's New, Rename, and Delete lifecycle commands and dynamic workspace-switch rows are now part of that native provider tree instead of existing only in `Accessibility.zig`; the Zig contract was reduced to the native fixed table, and a pinned-Zig executable test now fails when any contract id lacks an exact native fixed-table id. The live gate now also traverses the Workspace menu in RawView and ControlView, checks its fixed lifecycle names and InvokePattern exposure, and verifies at least one `workspace-switch-*` row under the same parent. `TerminalSurface.zig` retains reported selection and cell metrics, but callback metadata does not prove applied terminal selection. Exact-pin Winghostty `f5abc059` source already creates an embedded child-HWND Text/Text2 provider and routes `WM_GETOBJECT` to it; the earlier missing-provider premise was inaccurate. GraphCode now feeds that provider owned UTF-8 from its unchanged rendered-cell grid with independent UTF-16 length/cursor offsets, rather than a rolling raw VT byte tail. Focused producer tests exercise overwritten text, chunk splits, reset/rollover, Unicode scalar representation, bounds, allocation/lifetime and injected publication failures without native APIs. This is fixed current-grid content, not transcript/scrollback or full Unicode terminal rendering. Render/text publication remains best-effort: failures are reported and the provider may retain its last successful, no-longer-current snapshot. Native terminal text results, applied selection, visible caret/geometry, range/HRESULT conformance, retained-generation behavior, atomicity, daemon-to-model UIA integration, and remaining dialogs still need separate evidence or implementation; keyboard discovery and HelpText residuals are not closed | Partial | +| Reproducible DPI/geometry regression coverage | Control metrics for GraphCode-owned chrome scale correctly and predictably across 100/125/150/200% DPI | `Tools/windows/visual-baseline.ps1` previously only checked that each DPI variant's `scale`/`viewport` were present and positive. It now reimplements `Dpi.zig`'s exact `scale()` rounding formula, self-checked against `Dpi.zig`'s own fixed-point unit-test cases, reads the real base pixel values straight out of `DesignTokens.zig` (not a copy baked into the manifest), and asserts the scaled geometry for `sidebar_width`, `tab_bar_height`, `pane_header_height`, and `loop_bar_height` at the real Windows per-monitor DPI values (96/120/144/192) is monotonic and matches the 96-DPI base exactly at 100%. Every `regionGeometry` entry is required to target a `deterministicScreenshotRegions` (GraphCode-owned) region, never a Winghostty-owned one, keeping third-party terminal pixels structurally out of scope. **This check performs static manifest/geometry metadata validation, not rendered-output comparison: it never launches the app, captures a window, or rasterizes a bitmap.** It re-derives expected numeric geometry from source-of-truth code and checks the manifest against that math, which is materially stronger than the prior presence-only checks and does catch real drift, but it is not a screenshot diff and should not be read as one; this repo/CI has no deterministic way to rasterize a live Win32 window. Manually re-verified that corrupting either a DPI value or a `DesignTokens.zig` constant makes the script fail | Validated | +| Keyboard discovery | Every shortcut represented by a menu item or visible hint where practical | Restored File, Loop, Terminal, View, and Help menus expose the primary project, graph, terminal, workspace, settings, update, and zoom commands with shortcut labels. The shared node popup no longer advertises Enter for Open Terminal or Ctrl+E for Edit Details: root Enter has no standalone open action, and Ctrl+E renames a selected loop or edits a selected edge. Two executable contracts failed on the old captions and pass on the corrected ones through the actual menu builder and `GetMenuStringW`, preserving command IDs, mappings, order, and enabled states across ordinary/resolved/composite/unwired targets. They inspect unattached menu handles without creating windows or displaying popups; mapper checks preserve toolbar Enter activation and existing Ctrl+E/F2 routes. The Windows README now distinguishes root, terminal, and dialog contexts and corrects toggle/chat-delete bindings. Pure mapper regressions cover Ctrl+Shift+OEM/legacy ASCII comma selecting product Settings, plain Ctrl+comma retaining Advanced Connection Settings, and unmodified/unrelated keys. An unattached native accelerator-table test checks the matching documented product binding and the unchanged original 15 bindings/order; this is not physical keyboard, focus, or dialog-opening evidence. Some context-only actions and canvas gestures still lack visible hints. No live keyboard or macOS runtime walkthrough was performed; remaining context-only/gesture discovery, other caption/routing discrepancies, and keyboard popup access still need evidence | Partial | +| IME/dead keys/layouts | Native composition in forms and terminal | Winghostty gate covers terminal IME; generic EDIT controls cover forms | Partial | +| Clipboard/selection | Terminal copy/paste and mouse selection | Terminal-context Ctrl+Shift+C copies the active surface's reported accessibility selection range through Winghostty into Windows `CF_UNICODETEXT`; Ctrl+Shift+V reads only `CF_UNICODETEXT`, converts UTF-16 to UTF-8, and calls Winghostty's paste validator and paste entry point with `allow_unsafe=0`. Clipboard conversion tests preserve Unicode, CRLF, LF, and empty text; app routing tests prove the shortcuts are terminal-context-only and preserve the existing global Ctrl+Shift+C Clone Repository route. Unsafe multiline/control-containing pastes are rejected with a status message rather than forced through, and no confirmation UI is provided. Clipboard Win32 calls, mouse-driven selection, actual rendered selection extraction, provider callback notifications, and end-to-end paste/copy on a live desktop were not exercised here; this row remains Partial | Partial | +| Per-monitor DPI | Layout and controls scale correctly across monitors | The process now declares real per-monitor-v2 DPI awareness at startup (`Win32.enablePerMonitorDpiAwareness()`, called before any window is created) instead of relying on system-DPI bitmap stretching; without this, Windows never delivers real per-monitor `WM_DPICHANGED` data to a DPI-unaware process. `App.zig` seeds the real startup DPI via `GetDpiForWindow` immediately after window creation (rather than assuming 96 DPI/100% until the first monitor move) and forwards every live `WM_DPICHANGED` to `TerminalWorkspace.Workspace.setDpi()`. Previously, `TerminalSurface.zig`'s `onDpiChanged` callback silently discarded the `dpi`/`scale` winghostty reported, and every terminal surface was created with `font_scale` hardcoded to `1.0`, so terminal text never actually rescaled on a DPI change or on a monitor with non-100% DPI at launch. `Workspace.setDpi()` now propagates the real runtime DPI to every live surface via winghostty's own `winghostty_surface_notify_dpi_changed` + `winghostty_surface_set_font_scale` (the two operations the provider actually exposes for this), and `surfaceOptions()` seeds new surfaces' `font_scale` from the workspace's last-known DPI instead of a fixed `1.0`. Deliberately does not also pre-scale `options.input.cell_width`/`cell_height` (kept at their 96-DPI logical baseline) so the DPI ratio is applied exactly once, through `font_scale`, avoiding double scaling. `onMetricsChanged`/`onAccessibilitySelection`, previously also fully discarded, now record the host's reported cell metrics and terminal text-selection range per surface instead of losing them. Verified with `zig build` (full app, pinned Zig 0.15.2 against the exact pinned Winghostty provider) and `zig test src/TerminalSurface.zig` (new `Dpi.fontScale` unit test plus all 13 pre-existing tests, 14/14). No live multi-monitor walkthrough was recorded (this environment has no interactive multi-DPI desktop), so this remains Partial pending that end-to-end evidence | Partial | +| Dark visual language | Dark canvas/cards/sheets and legible state hierarchy | Existing `DesignTokens`, repository dialogs and `NativeForms` supply the dark native palette and teaching tiles. [Actual production-renderer captures](visual-baseline/rendered-windows/README.md) now preserve canvas/sidebar, Product Settings and workspace clients at native 96 DPI. The real PNG comparator verifies exact opaque canvas/card/sidebar/dialog samples; setup uses a synthetic disconnected fixture, UIA invocation and native WM_COMMAND, not keyboard-menu proof. Settings retains native light buttons; current macOS Settings uses a native grouped form, so these were not speculatively darkened. Other sheets/state combinations, legibility on every surface and a compatible current macOS capture remain unverified | Partial | +| Font rendering quality | Legible, ClearType-quality text on every surface, matching macOS's default anti-aliased text | The shared `AppFont.zig` cache requests Segoe UI at `CLEARTYPE_QUALITY`, with per-DPI native controls and logical-size selection for buffered canvas painting. [Actual 96-DPI glyph samples](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) preserve card/sidebar/Settings-title pixels with 27/31/55 distinct colors; the native button sample has two. These counts and recorded system font-smoothing settings are observations, not legibility thresholds or actual font-face certification. Native control LOGFONT metadata, every-surface/multi-DPI review, terminal-text readability and matched current macOS evidence remain unavailable. No speculative font fix was made | Partial | +| Line/shape anti-aliasing | Smooth, anti-aliased lines/curves/rounded corners matching macOS's Core Graphics default | Existing `GdiplusAA` draws solid Beziers, rounded cards and metric segments with GDI fallbacks; axis-aligned grid lines and dashed/preview paths remain plain GDI. [Real app captures](visual-baseline/rendered-windows/README.md#what-the-pixels-establish) now use production GDI+ startup, with both disabling automation hooks unset, rather than the ordinary UIA gate or a standalone drawing surrogate. The sampled selected-card corner has 31 colors and metric sparkline 30; original pixels and source/UIA-mapped regions are preserved. Other colors are not automatically antialias coverage, and there is no invented quality threshold. All edge styles/DPI states and equality with macOS Core Graphics remain unproven | Partial | + +| Color palette fidelity | Windows tones/gradients match macOS `Theme.swift` 1:1 (not just "generically dark") | [Immutable before/after app captures and replay](visual-baseline/rendered-windows/README.md) prove a bounded COLORREF bug: the same 96-DPI workspace focus-strip ROI [500,130,64,2] changed from 128/128 orange RGB255,132,10 pixels to 128/128 blue RGB10,132,255 after only `pane_focus_tint` changed from `0x000A84FF` to `0x00FF840A`. The production comparator independently derives current Theme.paneFocusTint, decodes Windows BGR and checks actual pixels; old-orange/channel-swap/delta-1 controls fail. Canvas/grid/card/sidebar/dialog/selected-tab opaque samples also match exact source colors. Existing two-stop `GdiGradient` chrome is measured, not equated with macOS three-stop/material compositing. All tones/states and a compatible current macOS rendered comparison remain unverified; historical manifest and separate two-token currentThemeContract are unchanged | Partial | + +**Known out-of-scope CI gap surfaced while validating the row above (PR #398):** the live `windows-shell` UIA gate's "New Loop" assertion invoked via the sidebar's `project-new-loop-*` element (`Tools/windows/uia-live-gate.ps1`, "project-row New Loop did not open the node form") fails intermittently/deterministically across unrelated branches (reproduced on `coneilen-microsoft-canvas-workspace-detail-parity` and `coneilen-microsoft-updates-dialogs-quick-chats-parity` as well, with no relation to dialog rendering code). This is pre-existing test-infrastructure flakiness, not a visual-polish regression; it is out of this pass's scope and is flagged here for a dedicated follow-up. + +**Known shared-environment gate instability surfaced while validating the Window toolbar/Update command rows above:** with the local shell toolchain unblocked (PR #433), multiple parity sessions now build and run `graphcode-windows.exe`/`zmx.exe` concurrently on the same interactive desktop. The pre-existing worktree reorder/removal focus-retention stress block in `Tools/windows/uia-live-gate.ps1` (`Retain-FocusWithRetry`, its `Start-Job` concurrent-UIA-read stress, and the plain `Get-DirectChildren` tree walks around it) repeatedly hit raw, uncaught COM exceptions (`GetFirstChild`/`GetNextSibling` "Could not open the process token"/"Unrecognized error.") at different, unrelated call sites across many local runs, and a separate run was independently derailed by another desktop application (Chrome) stealing the foreground window during a modal-dialog wait. None of this reproduced from this branch's own changes — a minimal, standalone re-run that skips straight to the Update command assertions using the same shell process, native menu, and gate helpers passed cleanly and repeatably. This matches flakiness independently reported by sibling parity sessions and is a pre-existing, shared test-infrastructure limitation, not a product regression; it blocked getting one single uninterrupted top-to-bottom `uia-live-gate.ps1` run this session and is flagged here for follow-up (likely hardening `Get-DirectChildren`/`Retain-FocusWithRetry` against concurrent-desktop contention). + +## Audit conclusion + +The Windows branch has substantial protocol, lifecycle, persistence, terminal, graph +mutation, tray, and packaging behavior, but it does **not** currently have complete UI +or screen parity. The previous parity statement conflated backend reachability with +user-visible parity. The largest corrective work is: + +1. Restore and complete the application menu and navigation state model. +2. Implement the sidebar, global graph, Quick Chats canvas, project canvas chrome, and + loop workspace as distinct application-owned surfaces. +3. Replace raw protocol forms with structured node, edge, settings, repository, and + worktree screens. +4. Implement the missing update, project-management, rename/delete, empty, and + connection-info states. +5. Expand UI Automation and live walkthrough coverage to every row above before any + complete-parity claim.