Skip to content

Loop terminal workspace parity polish - #386

Merged
coneilen merged 19 commits into
mainfrom
coneilen-microsoft-terminal-workspace-parity
Sep 19, 2026
Merged

coneilen merged 19 commits into
mainfrom
coneilen-microsoft-terminal-workspace-parity

Conversation

@coneilen

@coneilen coneilen commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

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

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
coneilen force-pushed the coneilen-microsoft-terminal-workspace-parity branch from e423457 to 0129702 Compare September 19, 2026 18:23
coneilen and others added 2 commits September 19, 2026 11:38
…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>
@coneilen
coneilen merged commit de1d85e into main Sep 19, 2026
10 of 16 checks passed
@coneilen
coneilen deleted the coneilen-microsoft-terminal-workspace-parity branch September 19, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant