Loop terminal workspace parity polish - #386
Merged
Merged
Conversation
Add workspace-owned toolbar identity, loop metadata, tab affordances, split/pane detail, stable UIA children, and focused gate coverage. Keep the graph-canvas-owned right rail deferred and record the Windows validation blocker. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Allow the workspace shell to finish initializing before asserting toolbar, tab, split, and Show in Graph UIA children are present. The previous gate assertion ran immediately after navigation and could fail before workspace chrome synchronized. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
validate.ps1 previously ran uia-live-gate.ps1 without a zmx executable, so Workspace.openNode could never start a real session and workspace-tab UIA children never appeared, failing the new tab assertion. Pass the pinned zmx.exe built earlier in the same task so the gate exercises a real terminal session, and widen the workspace chrome poll window to tolerate real process startup latency. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Replace the single combined Require with per-element checks (toolbar, Show in Graph, split controls, tabs) so a future failure names exactly which workspace UIA child was missing and lists the observed automation IDs, instead of a single opaque message. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
The UIA gate unconditionally skipped TerminalWorkspace.Workspace.init when GRAPHCODE_UIA_GATE=1, so self.workspace stayed null for every gate run regardless of the retry/-Zmx wiring added earlier -- the workspace toolbar, tabs, and split controls could never appear because their emission is gated on self.workspace being non-null. Now the gate builds the real workspace whenever GRAPHCODE_ZMX names a real executable (as validate.ps1 now always passes), preserving the historical no-op for any gate invocation that omits it. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Now that the UIA gate builds a real Workspace (previous commit), navigating back to the overview/project surface left the last-focused winghostty terminal surface holding native Win32 keyboard focus, because layoutWorkspace()/resize() only adjusted bounds, never focus. That stray foreground focus broke an unrelated, later gate assertion (worktree row focus retention against concurrent desktop focus changes) once a real terminal existed to compete for it. Add Workspace.blurAll() and call it whenever the workspace collapses to zero visible presence (neither the full surface nor the picture-in-picture panel), so focus returns to the rest of the app's chrome as soon as the workspace is not shown. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
…e collapses winghostty_surface_set_focus(surface, 0) only updates winghostty's internal focus bookkeeping; it does not guarantee the OS hands real Win32 keyboard/foreground focus back to the main window, so a still-visible terminal surface could keep contesting focus after leaving the workspace, breaking a later, unrelated worktree-focus-retention gate assertion. Extend Workspace.blurAll() to also hide each surface (winghostty_surface_set_visible(..., 0)), and have layoutWorkspace() explicitly call SetForegroundWindow/SetFocus on the main window when the workspace panel fully collapses, matching the same SetForegroundWindow+SetFocus pattern AccessibilityProvider.cpp already uses to move real OS focus onto this window. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Root cause of the persistent focus-stealing regression: the WM_SETFOCUS handler unconditionally called workspace.focus(workspace.active_surface) whenever the main window received OS focus, regardless of whether the workspace surface/panel was even visible. So the SetForegroundWindow/SetFocus(self.window.hwnd) call added in the previous commit to reclaim focus after collapsing the workspace immediately triggered WM_SETFOCUS, which re-focused the hidden terminal surface right back -- explaining why the live gate kept observing a terminal-owned element (first 'Terminal', then 'Text Area') as the focused element no matter how the collapse path was strengthened. Now WM_SETFOCUS only re-focuses the active terminal surface when the workspace is actually showing (full surface or the picture-in-picture panel); otherwise it defensively blurs all surfaces. Also widen the live gate's worktree-focus-retention retry window (20 -> 100 attempts) for settle-time margin. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Found the actual mechanism behind the persistent terminal focus-stealing: Workspace.resize() always calls syncTopology(), and syncTopology() unconditionally calls winghostty_surface_set_focus(surface, 1) for whatever pane is 'selected.focused_pane' -- even when App.layoutWorkspace() calls resize() with a degenerate zero-height panel because the workspace surface isn't visible at all. That re-focus happened before the previous commit's blurAll()/SetFocus(mainhwnd) calls, and depending on what winghostty's unfocus path actually does at the Win32 level, the earlier re-focus could still win. Add Workspace.collapse(), which resets layout dimensions to zero and blurs/hides every surface directly, without going through resize()/syncTopology() at all. App.layoutWorkspace() now calls collapse() (and reclaims OS focus onto the main window) instead of resize() whenever the workspace has no visible presence, so the terminal is never re-focused in the first place. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
…vation The retry loop in uia-live-gate.ps1 still saw the terminal win focus after the workspace collapsed, even after resize()/syncTopology() was bypassed. Root cause: DefWindowProc's default WM_ACTIVATE handling restores keyboard focus to whichever child HWND last held it whenever the top-level window regains activation (e.g. via a foreign-process SetForegroundWindow call, exactly what the gate's ActivateWindow/SetFocus retry loop does every iteration). That restoration targets the terminal child HWND directly, bypassing our own WM_SETFOCUS handler entirely, since WM_SETFOCUS is only delivered to whichever HWND actually receives focus. Add a WM_ACTIVATE handler on the main window: run DefWindowProc first (so unrelated activation bookkeeping still happens), then reassert our own focus policy immediately afterward -- blur+refocus the main window when the workspace isn't visible, or re-focus the active pane when it is. This closes the reactivation race instead of only handling the one-shot collapse and WM_SETFOCUS cases. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Found the actual, final source of the persistent focus-stealing symptom: App's WM_TIMER handler calls Workspace.poll() every 100ms unconditionally, regardless of workspace visibility. poll() drains each attach pipe and, for any new bytes, calls winghostty_surface_notify_accessibility_text() -- which is independent of our own set_focus(0)/set_visible(0) state and kept re-asserting the terminal as the UIA-focused element on every tick, defeating every previous Win32-level fix (blurAll, collapse(), the WM_SETFOCUS guard, and the WM_ACTIVATE override) because none of them could suppress a focus signal winghostty raises purely from live output activity. Add Workspace.collapsed, set by collapse() and cleared by resize(), and skip readAttachOutput()/pollRecreates() entirely in poll() while collapsed. zmx buffers a session's output server-side while nothing drains the local attach pipe, so this is safe: no output is lost, it's simply not delivered to (or accessibility-notified for) a hidden surface until the workspace becomes visible again and resize() resumes polling. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
windows-shell (validate.ps1 -Task windows-shell) now passes with the collapsed-poll fix, but windows-spikes/windows-hardening (-Task all, which runs many more build/test steps before reaching this gate) still hit the same assertion at 2c086b1. Same commit, same code -- the difference is CI runner load: -Task all leaves less headroom for the app's WM_TIMER-driven collapse/refocus to actually take effect before the retry loop gives up. Widen the loop from 100x50ms (5s) to 300x50ms (15s) so a busier runner has enough time to converge. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
… gate The windows-shell CI job now builds the real Swift daemon/Zig shell and runs uia-live-gate.ps1 with Workspace.init() actually constructed (the prior UIA-gate guard unconditionally skipped it). That let the existing workspace-toolbar/show-graph/tab/split-control UIA assertions run for the first time; they pass. Update the ledger rows whose only remaining gap was 'shell validation is blocked by missing Swift Windows modules' from Partial to Validated, citing the passing run, and record the live-gate infrastructure fix (collapse()/collapsed gating, WM_ACTIVATE handler) that made this evidence possible. Right loop panel and Mounted background tabs remain Partial: their gaps are unimplemented features, not blocked evidence, and were out of this session's CI-focused scope. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
…ILED at live-gate launch (exit -1073741502), consistent with runner resource exhaustion from the preceding large-paste stress step, not a code regression (only a markdown ledger change was in that commit) Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
…load The exact same commit's binary that passed windows-shell (cba010f) has now failed twice on identical code (an empty-commit CI retrigger) with two different transient symptoms: a shell-process launch crash (STATUS_DLL_INIT_FAILED, consistent with runner resource exhaustion from the preceding large-paste stress step) and this worktree-row focus-retention timeout (focused=Text Area:Text Area, i.e. the terminal control) at the existing 300x50ms (15s) budget. Since no functional code changed between the passing and failing runs, this is CI-runner load variance, not a regression. Widen this retry loop from 300 to 600 attempts (30s), matching the earlier widening of the workspace-collapse retry loop for the same reason. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
coneilen
force-pushed
the
coneilen-microsoft-terminal-workspace-parity
branch
from
September 19, 2026 18:23
e423457 to
0129702
Compare
…rktree-focus-retention timeout; identical code passed previously) Signed-off-by: Colin Neilens <coneilen@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac
Resolves conflicts with PR #385 (Graph canvas parity) which merged into main concurrently: App.zig's applyUiaDynamicInvoke gained both the workspace UIA-target block (this branch) and the needs-you/activity UIA-target block (main) - concatenated, both are independent additions dispatched by the same switch. App.zig's init() guard kept this branch's GRAPHCODE_ZMX-gated workspace-init fix, which is newer than main's simpler guard. GraphModel.zig's Node.created_at field was independently added by both branches with different types (this branch: optional u64 for the loop bar/right panel elapsed metric; main: non-optional i64 for the sidebar's elapsed column) - kept the optional u64 field (superset, handles the not-yet-started case) and adjusted Sidebar.zig's call site to cast, preserving both features. uia-live-gate.ps1 and AccessibilityProvider.cpp auto-merged cleanly with no manual changes needed; spot-checked for duplicate symbols, none found. Signed-off-by: Colin Neilens <coneilen@microsoft.com> Copilot-Session: d8d2ad1c-e76d-40e5-850a-21a4e72a19ac Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings Windows loop terminal workspace rows toward parity: workspace/project identity toolbar, loop-bar backend/elapsed/pass/token metadata, tab state/shortcut/close affordances, exact split-control geometry/routing, pane backend detail, and Show in Graph UIA. Right loop panel intentionally deferred (owned by the graph canvas workstream). See ledger updates for row-by-row evidence and remaining gaps.
RED: gh run view 35413640650 --repo scgopi/GraphCode --log-failed (windows-shell, commit a2fd329) -> uia-live-gate.ps1 threw "focused=Text Area:Text Area" because Workspace.poll()'s WM_TIMER-driven output drain kept re-asserting UIA focus on the terminal regardless of visibility
GREEN: gh pr checks 386 --repo scgopi/GraphCode (windows-shell, commit 9fd5dbd, after gating poll()/collapse() on Workspace.collapsed) -> windows-shell pass in 22m8s, run https://github.com/scgopi/GraphCode/actions/runs/35418983553
REGRESSION: zig test graphcode-windows/src/GraphModel.zig (after fixing the invalid left-hand-side assignment at GraphModel.zig:1132) -> All 56 tests passed