From d878245b5e5e97631599e375edb8b14d8e11f517 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 8 Oct 2026 14:51:49 -0700 Subject: [PATCH] Fix Windows node ids and text dialog initial focus Dev Box qualification of 0.1.78-windows.beta17 found client-chosen node ids were a 48-bit timestamp behind a constant 00000000-0000-4000-8000- prefix, and the shared GraphCodeWindowsDialog (Rename Loop) opened with keyboard focus on its frame instead of the Title edit. Node ids are now random RFC 4122 v4 UUIDs in Swift's uppercase form for both draft and send-time generation. The dialog selects its first field on creation and routes WM_ACTIVATE/WM_SETFOCUS to the remembered control. The empty-title New Loop finding is intended macOS parity (NodeDraft isValid has no title requirement except composites; NewNode fallback), recorded in the parity ledger rather than changed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- graphcode-windows/src/DaemonClient.zig | 32 ++++-- graphcode-windows/src/Forms.zig | 62 +++++++---- .../src/WindowsNativeDialogs.zig | 102 +++++++++++++++--- investigation/ui-parity-matrix.md | 4 +- 4 files changed, 151 insertions(+), 49 deletions(-) diff --git a/graphcode-windows/src/DaemonClient.zig b/graphcode-windows/src/DaemonClient.zig index df2f7e12..3c3e4e9b 100644 --- a/graphcode-windows/src/DaemonClient.zig +++ b/graphcode-windows/src/DaemonClient.zig @@ -125,6 +125,25 @@ test "sketch promotion test client has a fixed synthetic endpoint and inactive t try std.testing.expectEqual(@as(usize, 0), client.inbound_count); } +test "client-generated node ids on the create-node wire are random version-4 UUIDs" { + const allocator = std.testing.allocator; + var client = try DaemonClient.initUnstartedForTest(allocator); + defer client.deinit(); + client.sendCreateNodeConfigured("C:\\work\\graph", "Loop", "claudeCode", null); + client.sendCreateNodeDraft("C:\\work\\graph", .{ .title = "" }); + try std.testing.expectEqual(@as(usize, 2), client.outbound_count); + var ids: [2][]const u8 = undefined; + for (client.outbound[0..2], &ids) |command, *id| { + const marker = "\"createNode\":{\"_0\":{\"id\":\""; + const start = (std.mem.indexOf(u8, command, marker) orelse return error.MissingNodeId) + marker.len; + id.* = command[start .. start + 36]; + try std.testing.expect(Forms.isUuid(id.*)); + try std.testing.expectEqual(@as(u8, '4'), id.*[14]); + try std.testing.expect(!std.mem.startsWith(u8, id.*, "00000000-0000-4000-8000-")); + } + try std.testing.expect(!std.mem.eql(u8, ids[0], ids[1])); +} + pub const EventCallback = *const fn ( context: ?*anyopaque, frame: [*]const u8, @@ -167,7 +186,6 @@ pub const DaemonClient = struct { last_error: []const u8 = "", resume_from: u64 = 0, next_request: u64 = 1, - next_draft: u64 = 1, pending_request_ids: [64][36]u8 = undefined, pending_request_count: usize = 0, v1_pending_count: usize = 0, @@ -485,11 +503,7 @@ pub const DaemonClient = struct { model_tier: ?[]const u8, ) void { var node_id: [36]u8 = undefined; - self.mutex.lock(); - const sequence = self.next_draft; - self.next_draft +%= 1; - self.mutex.unlock(); - makeRequestID(&node_id, sequence); + Forms.generateDraftId(&node_id); const command = Wire.commandGraphCreateNodeConfigured( self.allocator, project_path, @@ -513,11 +527,7 @@ pub const DaemonClient = struct { // creates has to carry the same one. Everything else keeps the historical // generate-at-send-time id. const node_id: []const u8 = if (draft.node_id.len != 0) draft.node_id else blk: { - self.mutex.lock(); - const sequence = self.next_draft; - self.next_draft +%= 1; - self.mutex.unlock(); - makeRequestID(&generated_id, sequence); + Forms.generateDraftId(&generated_id); break :blk &generated_id; }; const command = Wire.commandGraphCreateNodeFull(self.allocator, project_path, node_id, draft) catch { diff --git a/graphcode-windows/src/Forms.zig b/graphcode-windows/src/Forms.zig index 1814ea27..e8755539 100644 --- a/graphcode-windows/src/Forms.zig +++ b/graphcode-windows/src/Forms.zig @@ -65,27 +65,28 @@ pub const NodeDraft = struct { } }; -/// A `[[0-9a-f]{8}-...]` version-4-shaped id, generated the same way -/// `DaemonClient.zig`'s own `makeRequestID` does — a nanosecond timestamp rather than a -/// cryptographic random source, because these ids only ever need to be unique within one -/// running client, never unguessable. Exposed here (rather than kept private to -/// `DaemonClient.zig`) so a draft's id can be chosen before its dialog opens, which is -/// what lets an attachment picked mid-dialog be filed under the id the node will -/// actually carry. -/// -/// Mixed with a process-lifetime counter, not the timestamp alone: two calls close -/// enough together can land on the same nanosecond reading on lower-resolution clocks, -/// which would hand two different attachment directories the same name. -var draft_id_sequence = std.atomic.Value(u64).init(0); - +/// A random RFC 4122 version-4 id, formatted like Swift `UUID().uuidString` (uppercase), +/// which is also how the daemon echoes node ids back. Every node id the client chooses +/// comes from here — `DaemonClient` uses it for create-node commands too — so Windows +/// ids carry the same 122 random bits as macOS ones instead of a timestamp behind a +/// zero-filled prefix. Exposed here (rather than kept private to `DaemonClient.zig`) so a +/// draft's id can be chosen before its dialog opens, which is what lets an attachment +/// picked mid-dialog be filed under the id the node will actually carry. pub fn generateDraftId(buffer: *[36]u8) void { - const timestamp: u64 = @intCast(std.time.nanoTimestamp()); - const sequence = draft_id_sequence.fetchAdd(1, .monotonic); - _ = std.fmt.bufPrint( - buffer, - "00000000-0000-4000-8000-{x:0>12}", - .{(timestamp ^ sequence) & 0xffffffffffff}, - ) catch unreachable; + var bytes: [16]u8 = undefined; + std.crypto.random.bytes(&bytes); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = std.fmt.bytesToHex(bytes, .upper); + @memcpy(buffer[0..8], hex[0..8]); + buffer[8] = '-'; + @memcpy(buffer[9..13], hex[8..12]); + buffer[13] = '-'; + @memcpy(buffer[14..18], hex[12..16]); + buffer[18] = '-'; + @memcpy(buffer[19..23], hex[16..20]); + buffer[23] = '-'; + @memcpy(buffer[24..36], hex[20..32]); } pub const EdgeDraft = struct { @@ -867,6 +868,27 @@ test "generateDraftId produces a version-4-shaped, distinct id each call" { try std.testing.expect(!std.mem.eql(u8, &first, &second)); } +test "generateDraftId produces random RFC 4122 version-4 ids, not a zero-filled prefix" { + // Dev Box beta17 created a loop whose id was `00000000-0000-4000-8000-AA395289111D`: + // 74 of 122 random bits were constant, leaving only a timestamp to tell ids apart. + const samples = 32; + var ids: [samples][36]u8 = undefined; + for (&ids) |*id| generateDraftId(id); + for (ids, 0..) |id, index| { + try std.testing.expect(isUuid(&id)); + try std.testing.expectEqual(@as(u8, '4'), id[14]); + try std.testing.expect(std.mem.indexOfScalar(u8, "89ABab", id[19]) != null); + // Same case as Swift `UUID().uuidString`, which is what the daemon echoes back. + for (id) |byte| try std.testing.expect(!std.ascii.isLower(byte)); + try std.testing.expect(!std.mem.startsWith(u8, &id, "00000000-0000-4000-8000-")); + for (ids[0..index]) |earlier| { + // The leading 32 bits alone must already differ between ids; a collision + // among 32 random samples has probability below 2^-22. + try std.testing.expect(!std.mem.eql(u8, earlier[0..8], id[0..8])); + } + } +} + test "node updates preserve unchanged fields and allow stall clear sentinel" { try validateNodeUpdate(.{ .stall_after_seconds = 0 }); try std.testing.expectError(error.InvalidGoal, validateNodeUpdate(.{ .poll_interval_seconds = 0 })); diff --git a/graphcode-windows/src/WindowsNativeDialogs.zig b/graphcode-windows/src/WindowsNativeDialogs.zig index 913aa031..919f76d0 100644 --- a/graphcode-windows/src/WindowsNativeDialogs.zig +++ b/graphcode-windows/src/WindowsNativeDialogs.zig @@ -29,6 +29,9 @@ const State = struct { closed: bool = false, failure: ?anyerror = null, button_y: i32 = 565, + /// The control that owns keyboard focus whenever the dialog is active: the first + /// field on open, then whichever child the user left focused when it deactivated. + focus: c.HWND = null, }; const class_name = std.unicode.utf8ToUtf16LeStringLiteral("GraphCodeWindowsDialog"); @@ -89,27 +92,13 @@ pub fn textWithDescription( active_state.closed = false; active_state.accepted = false; active = true; - const hwnd = c.CreateWindowExW( - c.WS_EX_DLGMODALFRAME | c.WS_EX_CONTROLPARENT, - class_name.ptr, - wide_title.ptr, - c.WS_OVERLAPPED | c.WS_CAPTION | c.WS_SYSMENU | c.WS_VSCROLL, - c.CW_USEDEFAULT, - c.CW_USEDEFAULT, - 600, - window_height, - parent, - null, - c.GetModuleHandleW(null), - null, - ) orelse { + const hwnd = createDialogWindow(parent, wide_title, window_height) orelse { freeStateValues(&active_state); active = false; return error.DialogCreationFailed; }; _ = c.EnableWindow(parent, 0); - _ = c.ShowWindow(hwnd, c.SW_SHOW); - _ = c.SetForegroundWindow(hwnd); + presentDialog(hwnd); var message: c.MSG = undefined; while (!active_state.closed) { const code = c.GetMessageW(&message, null, 0, 0); @@ -126,6 +115,29 @@ pub fn textWithDescription( return finishText(&active_state); } +fn createDialogWindow(parent: c.HWND, wide_title: []const u16, window_height: i32) c.HWND { + return c.CreateWindowExW( + c.WS_EX_DLGMODALFRAME | c.WS_EX_CONTROLPARENT, + class_name.ptr, + wide_title.ptr, + c.WS_OVERLAPPED | c.WS_CAPTION | c.WS_SYSMENU | c.WS_VSCROLL, + c.CW_USEDEFAULT, + c.CW_USEDEFAULT, + 600, + window_height, + parent, + null, + c.GetModuleHandleW(null), + null, + ); +} + +fn presentDialog(hwnd: c.HWND) void { + _ = c.ShowWindow(hwnd, c.SW_SHOW); + _ = c.SetForegroundWindow(hwnd); + if (active_state.focus) |target| _ = c.SetFocus(target); +} + fn finishText(state: *State) !?Result { if (state.failure) |err| { freeStateValues(state); @@ -184,6 +196,26 @@ fn windowProc(hwnd: c.HWND, message: c.UINT, wparam: c.WPARAM, lparam: c.LPARAM) } createButton(hwnd, "OK", ok_id, 490, active_state.button_y); createButton(hwnd, "Cancel", cancel_id, 400, active_state.button_y); + if (active_state.edits[0]) |first| { + _ = c.SendMessageW(first, c.EM_SETSEL, 0, -1); + active_state.focus = first; + } + return 0; + }, + // This is a plain window, not a dialog-manager dialog, so nothing hands focus to + // a control on its own: DefWindowProc's activation leaves it on the frame, where + // typing goes nowhere. Route it to the remembered control instead. + c.WM_ACTIVATE => { + if ((wparam & 0xffff) == c.WA_INACTIVE) { + const current = c.GetFocus(); + if (current != null and c.IsChild(hwnd, current) != 0) active_state.focus = current; + } else if (active_state.focus) |target| { + _ = c.SetFocus(target); + } + return 0; + }, + c.WM_SETFOCUS => { + if (active_state.focus) |target| _ = c.SetFocus(target); return 0; }, c.WM_VSCROLL => { @@ -449,3 +481,41 @@ test "workspace text capture and transfer release every partial allocation" { }; try std.testing.checkAllAllocationFailures(std.testing.allocator, Probe.run, .{edit}); } + +test "text dialog opens with keyboard focus in its first field, text selected, and keeps it across activation" { + // Dev Box beta17: Rename Loop opened with focus on the dialog frame, so typing did + // nothing until the Title edit was clicked. + const allocator = std.testing.allocator; + const previous_active = active; + active_state = State{ .allocator = allocator, .parent = null, .count = 1, .description = "Shown on the loop card." }; + active_state.labels[0] = "Title"; + active_state.values[0] = try allocator.dupe(u8, "GCQCrud17"); + active = true; + defer { + freeStateValues(&active_state); + active = previous_active; + } + try registerClass(); + const wide_title = try wideZ(allocator, "Rename Loop"); + defer allocator.free(wide_title); + const hwnd = createDialogWindow(null, wide_title, 220) orelse return error.TestWindowCreationFailed; + defer _ = c.DestroyWindow(hwnd); + const title_edit = active_state.edits[0] orelse return error.TestWindowCreationFailed; + try std.testing.expectEqual(@as(isize, 9904), c.GetDlgCtrlID(title_edit)); + + presentDialog(hwnd); + try std.testing.expectEqual(title_edit, c.GetFocus()); + var start: c.DWORD = 0; + var end: c.DWORD = 0; + _ = c.SendMessageW(title_edit, c.EM_GETSEL, @intFromPtr(&start), @bitCast(@intFromPtr(&end))); + try std.testing.expectEqual(@as(c.DWORD, 0), start); + try std.testing.expectEqual(@as(c.DWORD, "GCQCrud17".len), end); + + // Activation hands focus to the frame (DefWindowProc's WM_ACTIVATE does exactly + // that); the frame must pass it straight back to the field the user was in. + _ = c.SendMessageW(hwnd, c.WM_ACTIVATE, c.WA_INACTIVE, 0); + _ = c.SetFocus(hwnd); + try std.testing.expectEqual(title_edit, c.GetFocus()); + _ = c.SendMessageW(hwnd, c.WM_ACTIVATE, c.WA_ACTIVE, 0); + try std.testing.expectEqual(title_edit, c.GetFocus()); +} diff --git a/investigation/ui-parity-matrix.md b/investigation/ui-parity-matrix.md index a6aab6b6..2e530538 100644 --- a/investigation/ui-parity-matrix.md +++ b/investigation/ui-parity-matrix.md @@ -156,7 +156,7 @@ native keyboard/accelerator/window proof; the workspace row remains 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; at that point, daemon acceptance and persistence were also unproven. The added headless harness starts the real `graphcoded.exe` with isolated support state; production `sendCreateEdgeDraft` receives a correlated V2 `graphChanged` response, and daemon save/reload preserves edge identity, endpoints, condition, transform, spawn target, and cycle guard. The hosted `windows-shell integration` job 109978958907 (run 36742113814, merged as #540) then drove the real native sheet end to end against the connected protocol stub: Create Edge from the live canvas popup, the source picker carrying the seeded daemon node ID, native keyboard selection of kind/condition/transform, real `SendInput` text entry into payload and both cycle-guard fields with injected-event counts matching exactly (clear 2/2; text 32/32, 24/24, 2/2) and stable `WM_GETTEXT` readback, a rejected empty-template submission that showed the exact reason `Enter the template or script that should carry context.` while leaving the daemon command-log bytes and graph-command count unchanged, and an accepted submission whose `createEdge` wire matched every native choice and was answered by a correlated reply. The preceding intentional-failure run 36739940627 (job 109971468469) recorded the same full evidence before its deliberate late assertion. This is stub-daemon evidence, not `graphcoded` persistence; the accessibility provider still exposes no edge fragment, so rendering is corroborated only by republished graph state and the live Edit Edge hit test; native macOS parity evidence remains absent. | 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 does not establish native editing or macOS runtime evidence; macOS sources expose creation-time configuration and delete, not an existing edit UI. The added headless real-daemon test confirms production `sendUpdateEdge` receives a correlated V2 `graphChanged` response and a fresh daemon reload preserves edge ID, endpoints, `fireCount=2`, transform, spawn target, condition, and cycle guard. The hosted `windows-shell integration` job 109978958907 (run 36742113814, merged as #540) added native editing evidence against the connected protocol stub: the republished edge hit-tests to exactly `Edit Edge...`/`Delete Edge`, the reopened native editor prefills every endpoint, kind, condition, transform, payload and cycle-guard field from the created edge, and changing only the condition emits one `updateEdge` whose `expectedSpec.condition` is the old `onFailure`, whose `spec` differs from it in that single field, and which the stub applies once to the same edge ID with the count unchanged at one. Cancelling a changed reopen left the command-log bytes, applied-update count and stored condition untouched, and a third reopen still showed the committed `onSuccess`. This is stub-daemon evidence, not `graphcoded` persistence; there is still no edge UIA fragment, and macOS exposes creation-time configuration and delete rather than an equivalent edit UI, so this row cannot be promoted on parity grounds. | 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 or a Git-inspection-to-creation flow. **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 and clipboard paste/drop remain outstanding; native-input evidence remains 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.. A headless real-daemon round-trip sends production `sendCreateNodeDraft`, receives a correlated V2 `graphChanged` response, and confirms node identity/configuration in persisted graph state and after a fresh daemon restart. The hosted `windows-shell integration` job 109912042628 (run 36722807278, merged as #538) then exercised the real native sheet with OS input: four loop-type teaching tiles were selected by verified native mouse points, two invalid submissions each kept the modal open, showed the exact validation reason and dispatched no create command with the daemon command count unchanged, the reason cleared on type change, and an accepted submission dispatched `timeBased`/`copilotCLI`/`capable` with the exact trigger prompt, was answered by the daemon stub, and rendered as exactly one sidebar row and one canvas card. Two accessibility defects were recorded rather than papered over: the pre-submit recap is not refreshed when a tile is clicked, and on both invalid submissions the modal stayed natively visible with the correct title while the desktop UIA child census never recovered after ten retries. Native OS picker acceptance, clipboard paste/drop, physical keyboard entry and macOS parity remain unproven, so this row stays `Partial`. Separately, #539 (merged `d0cce79e`) clamps the form's content controls to the scrollable viewport and sizes the dialog to the monitor work area, so a tall sheet no longer paints outside its frame on a small display; the hosted UIA gate caught a regression in that work where fully offscreen controls were hidden outright and therefore vanished from the automation tree, and the landed fix keeps such controls shown with an empty clip region. That is unit plus hosted-gate evidence over 254 `NativeForms.zig` tests, not a live small-display walkthrough. | 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 or a Git-inspection-to-creation flow. **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 and clipboard paste/drop remain outstanding; native-input evidence remains 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.. A headless real-daemon round-trip sends production `sendCreateNodeDraft`, receives a correlated V2 `graphChanged` response, and confirms node identity/configuration in persisted graph state and after a fresh daemon restart. The hosted `windows-shell integration` job 109912042628 (run 36722807278, merged as #538) then exercised the real native sheet with OS input: four loop-type teaching tiles were selected by verified native mouse points, two invalid submissions each kept the modal open, showed the exact validation reason and dispatched no create command with the daemon command count unchanged, the reason cleared on type change, and an accepted submission dispatched `timeBased`/`copilotCLI`/`capable` with the exact trigger prompt, was answered by the daemon stub, and rendered as exactly one sidebar row and one canvas card. Two accessibility defects were recorded rather than papered over: the pre-submit recap is not refreshed when a tile is clicked, and on both invalid submissions the modal stayed natively visible with the correct title while the desktop UIA child census never recovered after ten retries. Native OS picker acceptance, clipboard paste/drop, physical keyboard entry and macOS parity remain unproven, so this row stays `Partial`. Separately, #539 (merged `d0cce79e`) clamps the form's content controls to the scrollable viewport and sizes the dialog to the monitor work area, so a tall sheet no longer paints outside its frame on a small display; the hosted UIA gate caught a regression in that work where fully offscreen controls were hidden outright and therefore vanished from the automation tree, and the landed fix keeps such controls shown with an empty clip region. That is unit plus hosted-gate evidence over 254 `NativeForms.zig` tests, not a live small-display walkthrough. A blank title is accepted for every type except composite and the daemon names the loop `NewNode`, matching macOS `NodeDraft.isValid`/`untitledFallback` (macOS then asks the loop's backend for a title via `TitleSuggestionClient`, which Windows does not yet do). Client-chosen node ids are now random RFC 4122 version-4 UUIDs in Swift's uppercase form; before the beta17 Dev Box finding they were a 48-bit timestamp behind a constant `00000000-0000-4000-8000-` prefix. | Partial | | Node update/rename | Dedicated rename prompt and safe typed updates | Rename retains its dedicated safe prompt. Edit Details uses `NativeForms.update` and `sendUpdateNodeForm`; project/node identity is owned across the modal loop and re-resolved afterward, changed strings are compared with an owned initial snapshot, and numeric plus clear-versus-unchanged semantics are preserved. Production-helper tests cover mutation, unchanged/changed typed fields, cancellation, clearing, allocation failures, and the former borrowed-baseline lifetime bug. The green Windows shell CI run 36489223062 opened the live Rename Loop dialog, verified its explanation, Title label, and prefilled current title, typed a replacement title and submitted it with Return, and observed the dialog close. The same run found Rename and Edit Details in live plain, composite, and unwired node popups. It did not verify the renamed title in the graph/sidebar/model after submission and did not open, cancel, or submit Edit Details. macOS runtime evidence did verify a root rename reaching overview/sidebar, but reproduced a nested Rename action that showed no dialog and did not exercise typed retype. Presence plus dialog closure is not end-to-end update parity, so app-level rename result and Edit Details behavior remain residuals and the row stays `Partial`. Separately, the headless real-daemon test exercises production `sendUpdateNodeForm` and `sendRenameNode`; correlated V2 `graphChanged` replies confirm acceptance, and a fresh daemon reload retains the node ID with updated check description and renamed title. This covers daemon acceptance/persistence only, not the live UI update result; native editor/rename interaction, cancellation, UIA, and returned app-level dispatch remain unverified. The green Windows shell CI run 36559162178 adds the returned result the earlier gate could not observe: a separate connected-daemon phase runs its own shell with `GRAPHCODE_UIA_CONNECTION_FAILURE` unset against `Tools/windows/Stub-Daemon.ps1`, which speaks the length-prefixed v2 protocol over a real named pipe. It logged `UIA_CONNECTED_DAEMON_MODEL sidebar='Daemon loop A' identity=loop-row-1575646491273972180` (nothing was seeded locally, so the rendered row came from a daemon `graphChanged`), then drove the same live Rename Loop dialog and logged `UIA_CONNECTED_RENAME_PROPAGATION nodeId=11111111-1111-4111-8111-111111111111 graphIdentity=canvas-card-1510499067760483540 graph='Daemon renamed loop' sidebarIdentity=loop-row-1575646491273972180 sidebar='Daemon renamed loop' expected='Daemon renamed loop' connection=live-stub-daemon`, with `UIA_CONNECTED_RENAME_STUB` reporting `appliedRenames:["11111111-1111-4111-8111-111111111111=Daemon renamed loop"]`. Both AutomationIds are unchanged across the rename, so a title-only change preserves graph-card and sidebar-row identity. The same run still shows the untouched disconnected assertions `UIA_RENAME_DISPATCH ... connectionFailure=forced` and `UIA_RENAME_OUTCOME graph='UIA loop A' sidebar='UIA loop A' ... reason=gate-forces-daemon-connection-failure`. The connected peer is a protocol-level stub, not `graphcoded`, so this would establish that a daemon-returned model reaches the Windows graph card and sidebar row after a live UI-driven rename, not that the production daemon computes that model; however, this result is not reliably reproducible. Two subsequent PR #510 CI runs on the same connected-daemon phase (windows-shell run 36563493110 and, after widening the propagation read window and adding a pre-rename stub-connection-settle wait, run 36567907468) both failed the identical `UIA_CONNECTED_RENAME_PROPAGATION` assertion, reading the pre-rename title (`graph='Daemon loop A'`) after the full wait even though each run's own retained `rename-stub.json` confirmed the stub correctly applied and republished the rename (`appliedRenames:["11111111-1111-4111-8111-111111111111=Daemon renamed loop"]`). Both failing runs also show a mid-phase daemon reconnection (`connectionCount=2`) that the passing run also had, but at a different point in the request sequence; gate-only mitigations (a wider read window, then a pre-rename wait for the stub's `connectionCount` to hold steady) did not make propagation succeed again, and root-causing further would require instrumenting or changing `App.zig`/`GraphModel.zig`'s project-selection and graph-refresh path, which is outside this row's gate-only scope. So the one successful run above is evidence the mechanism can work, not that it reliably does; live connected-daemon propagation is an open, intermittent gap, not proven parity. Edit Details still has no live open/cancel/submit evidence, and macOS still reproduced a nested Rename action that showed no dialog. The row stays `Partial`. | 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 expose 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; composite/template/unwired actions and the absence of Message/Memo are preserved. Tests inspect the same native HMENU builder used by `TrackPopupMenu`. The green Windows shell CI run 36489223062 opened and read the real native plain, composite, and unwired loop popups, required their state-specific labels and absences, verified disabled Arm Schedule for an unpiloted composite, and dismissed each menu without losing the UIA tree. It also read local and remote project popups, including disabled unavailable Move Project and omission of local-only actions for a remote project. macOS runtime evidence sampled Composite, Main, and background menus but did not open its edge menu. Neither runtime drove a destructive confirmation or edge popup, and Windows CI did not invoke node/background menu results, New Child, import/export, custody creation, or promotion. Those actions are part of this row's explicit node/edge contract, so popup presence and enablement alone do not justify promotion. The green Windows shell run 36691624270, integration job 109810355220, then measured all three canvas targets from live canvas geometry after restoring Actual Size, and its final summary JSON recorded the blank-canvas menu (Worktrees..., Project Settings..., Show in Explorer, separator, disabled-aware Create Edge), the node-card menu (Open Terminal, Edit Details..., Save as Template..., Rename... F2, Delete Loop... Delete) and the edge menu (Edit Edge..., Delete Edge), each item enabled and unchecked and each menu dismissed. It also invoked Edit Edge... on a real edge with a physical cursor move and click, observed the edge editor open and cancel, and confirmed the daemon command log was unchanged. That closes measurement and one invoked edge action, but not the destructive confirmations, New Child, import/export, custody creation, or promotion this row still requires | Partial | @@ -232,7 +232,7 @@ case proves ownership only. All existing Partial rows remain 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 | `UpdateInstallDialog.zig` maps every `WindowsUpdateInstall.InstallError` (checksum unavailable/mismatch, download, extraction and its timeout, missing setup script, upgrade and its timeout, out of memory) to a distinct reason, switches the native progress window to that reason with a Close button, and `App.runInstall` surfaces it as status; cancelling during progress reports "Update install cancelled". The failure reason is carried by value (`FailureMessage`), so worker, out-of-memory, and window-closed failures all surface a reason without the caller freeing borrowed or static memory, and a worker thread that cannot start re-enables and reactivates the owner instead of leaving it disabled. Evidence is `UpdateInstallDialog.zig` unit tests of the outcome and startup-failure logic plus a compiling shell build, not a live walkthrough. Remains Partial: every terminal failure now renders a "Download in Browser" action beside Close, opening the release URL through an injected shell API that the tests substitute, so the mapping and invocation are covered by unit tests. The real `ShellExecuteW` handoff is never invoked by a test, and no published release has a Windows asset (v0.1.76 and 0.1.77-beta1 ship the macOS DMG only), so no real download/install failure and no real browser launch has been driven end to end | 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 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. Dev Box qualification of `0.1.78-windows.beta17` found the dialog opened with keyboard focus on its frame rather than the Title edit (9904), so typing did nothing until the field was clicked; the shared `GraphCodeWindowsDialog` now selects the first field's text on creation and routes `WM_ACTIVATE`/`WM_SETFOCUS` to the remembered control. A `WindowsNativeDialogs.zig` test drives the real window class through `ShowWindow` activation, `WM_SETFOCUS`, and deactivate/reactivate, and failed before the fix; that is in-process native-message evidence, and a Dev Box rerun of physical typing into the opened dialog is still pending | 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 |