Skip to content

step between open windows - #591

Merged
umputun merged 2 commits into
masterfrom
window-cycle
Sep 11, 2026
Merged

step between open windows#591
umputun merged 2 commits into
masterfrom
window-cycle

Conversation

@umputun

@umputun umputun commented Sep 11, 2026

Copy link
Copy Markdown
Owner

a user asked whether the missing cycle-between-windows action was deliberate. It was not, so here it is.

agterm had prev/next for sessions, attention sessions, workspaces and split panes. Window was the one level of the hierarchy with nothing, which left ⌘(a macOS shortcut you can turn off and cannot rebind per app) or a script overagtermctl window list`.

What is new

  • previous_window / next_window keymap builtins, both keyless, so map cmd+opt+[ previous_window is yours to write
  • Navigate ▸ Previous Window / Next Window, plus the two matching palette rows
  • agtermctl window go --to next|prev

How the step works

One host-free WindowLibrary.navigateWindow backs all three, so menu, palette and CLI cannot drift. It walks the OPEN windows in library order and wraps. A closed bundle is not a stop on the way round: the cycle is as long as what is on screen, and window select stays the verb that opens a closed one. Below two open windows the GUI items disable through PaletteContext.canStepWindows and window.go answers no other open window to navigate to.

Two things worth a second look

Both surfaces raise through WindowRegistry.raise rather than the AppActions.openWindow hub. On a failed raise that hub enqueues a claim and opens a fresh scene, and the failure case for a step is an OPEN window whose NSWindow has not attached yet, so one store would end up with two scenes and closing either would tear down surfaces the other is showing. enqueueClaim dedups pending claims only, so a claim already popped does not protect it. AppActionsTests.testWindowStepNeverOpensASceneForAnUnattachedTarget pins it.

Both also publish frontmost themselves. WindowAccessor.reportFrontmost rides didBecomeKey, which AppKit does not deliver while the app is inactive, and the key monitor fires a bound step from the quick terminal with agterm in the background. Without the explicit publish the id stays stale, every later step recomputes from the same origin and picks the same window, and an auto-hidden sidebar stays collapsed on the window just raised.

Also in here

AppActions.swift, CommandsTests.swift and ControlProtocolTests.swift were all sitting exactly at the swiftlint file limit, so they are split at existing seams: AppActions+Navigation.swift, WindowCommandsTests.swift, ControlWindowProtocolTests.swift. The moved code is verbatim.

One unrelated fix rides along, in its own commit. SessionHostClientTests.Fixture.hostPID() read the session-host pidfile once, while the host writes it asynchronously, so ControlServerZmxTests.testRealClientPaneChangesFromSupervisorToOrphanedWhileBarePaneReadsApp failed with ENOENT under full-suite load and passed on its own. It polls now.

Docs updated: site/docs.html, site/commands.html, the bundled skill, and the windows/control-api/menu-actions/keymap rules.

`hostPID()` read `session-host.pid` on the first try, but the host writes it from its own process after the client that started it returns. `waitForLeaders` proves the zmx daemons are up, which is a different event. Unloaded the file was already there, so the race only showed in a full-suite run, where `ControlServerZmxTests.testRealClientPaneChangesFromSupervisorToOrphanedWhileBarePaneReadsApp` failed with ENOENT while passing on its own.
new `previous_window`/`next_window` builtins, Navigate ▸ Previous/Next Window, two palette rows, and `window.go --to next|prev`. Window was the only level of the hierarchy with no prev/next: sessions, attention sessions, workspaces and panes all had one. Both builtins ship keyless, like the workspace pair, so nothing takes a chord from anyone.

host-free `WindowLibrary.navigateWindow` is the one step every surface calls. It walks the OPEN windows in library order, wrapping. A closed bundle is not a stop on the way round, so the cycle length matches what is on screen rather than what the library holds, and `window select` stays the verb that opens one.

both surfaces raise through `WindowRegistry.raise` and publish frontmost themselves. Going through `AppActions.openWindow` instead would be wrong twice over: on a failed raise that hub enqueues a claim and opens a fresh scene, and the failure case for a step is an open window still attaching, so one store would get two scenes. `enqueueClaim` dedups pending claims only, so a popped claim does not protect it. And `WindowAccessor.reportFrontmost` rides `didBecomeKey`, which never arrives while agterm is inactive, the state a step from the quick terminal raises in, leaving the id stale so every later step recomputes from the same origin.

`AppActions.swift` and two test files were at the swiftlint limit, so the navigation block moved to `AppActions+Navigation.swift` and the window cases to `WindowCommandsTests.swift` and `ControlWindowProtocolTests.swift`.
Copilot AI lite review requested due to automatic review settings September 11, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@umputun
umputun merged commit 99c8c50 into master Sep 11, 2026
8 checks passed
@umputun
umputun deleted the window-cycle branch September 11, 2026 17:38
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.

2 participants