Skip to content

Backlog sweep: thirteen items done, six dropped - #694

Merged
umputun merged 22 commits into
masterfrom
backlog-sweep
Oct 3, 2026
Merged

umputun merged 22 commits into
masterfrom
backlog-sweep

Conversation

@umputun

@umputun umputun commented Oct 3, 2026

Copy link
Copy Markdown
Owner

a sweep of docs/backlog/: thirteen items done, six dropped, three kept. Each done item has its own commit, followed by review fixes, so this merges with a merge commit.

New control surface

  • zmx screen NAME reads the screen of a daemon whose window is closed
  • keymap run NAME runs a custom command from keymap.conf, with --target and --window
  • session.selected event on selection changes, carrying the previous session
  • reconnect on a surface node: the retry streak and the last failed probe's reason. The pane shows the same reason while it waits
  • indexUnsaved on the tree while a failed windows.json write is outstanding

Fixes

  • a minimized, ordered-out or app-hidden window releases the swap chains of its panes and gets them back when shown
  • a failed windows.json write is retried after the next snapshot save that succeeds and at exit, ahead of arming a Live reset
  • a zmx replay no longer paints a styled run across cells the cursor skipped. This patches zmx's ghostty dependency, so the next scripts/setup.sh rebuilds zmx once
  • a Debug build launched with AGTERM_STATE_DIR unset or empty uses ~/Library/Application Support/agterm-debug for state and socket, and keeps the default config directory

Smaller ones: a window restored by a reopen shows its sidebar, the overlay wrapper starts as a non-login shell, scripts/symbolicate-frames.py names stripped libghostty frames, and docs for OpenCode 1.x, long session type lines and the skill description.

Kept in the backlog

Measured and not checked

  • one pane on an isolated Debug instance: IOSurface 34.4 MB, 560 KB 38 seconds after minimizing, 34.4 MB three seconds after restoring. Hiding the app dropped it to 11.8 MB with 0 resident after five seconds and unhide returned it
  • not checked by hand: pixels after un-minimize, covered or other-Space windows, the reconnect reason on a real lost link, a pane shell inheriting the adopted Debug state directory, a Live reset relaunch under it

umputun added 22 commits October 3, 2026 01:54
libghostty runs a surface command through `exec -l`, which puts a dash on argv[0]. The overlay wrapper was a bare `sh -c`, so a `/bin/sh` that is dash or zsh started as a login shell and ran `~/.profile` before the caller's command. The stock bash selection was unaffected.

the wrapper is now `/usr/bin/env /bin/sh -c`, the form the remote pane command already uses: `env` takes the dash and `sh` starts non-login.
`agterm-status.js` uses the 1.x plugin API and OpenCode 2 does not load it, so on a 2.x machine the installer reported success and no status ever appeared. The install dialog and both troubleshooting pages told the user to restart OpenCode, which does nothing there. They now name the major the plugin serves.

Related to #637
`revealRestoredWindow` published the frontmost id by hand and left the sidebar reconcile to `didBecomeKey`, which AppKit does not deliver while agterm is inactive. A `reopen_recent` fired from the quick terminal over another app could restore into a window other than the frontmost one, and with auto-hide of inactive sidebars on, that window was raised with its sidebar still collapsed.

it now calls `takeFrontmost`, the same publish block with the reconcile in it.
#679 paced only the final Return. One very long line can still be taken whole as a paste by the receiver: Claude Code ran a 2000-character `/rename` as a prompt. Pacing inside a line would block the main thread per piece on both routes, so the docs now say the caller sends shorter pieces and checks the result.
release builds keep only the exported `ghostty_*` symbols, so `atos` cannot identify the stripped Zig-internal functions. `scripts/symbolicate-frames.py BINARY ARCHIVE OFFSET...` matches the instructions before each return address against `libghostty-internal.a` with immediates masked, 24 instructions and then 12, the method that resolved #606. A frame is named only on a single match; an ambiguous match, no match, and an offset off an instruction boundary are reported as such and exit 1.
- accept-thread-waits-on-main-actor-without-bound: a design task that only keeps reads answering during a main-thread freeze, and does nothing for the freeze
- host-side-remote-client-events: no consumer asked for it
- no-draft-marker-independent-of-the-status-slot: no confirmed demand for a new persisted, rendered state
- wrapper-pane-foreground-capture-unverified: unresolved, with no installation left to reproduce it on
- edit-keymap-menu-hides-the-extension-mechanism: none of its ideas was clearly right
- claude-hook-table-changes-never-reach-existing-installs: `mergeClaudeSettings` already migrates the historical generated forms, and a generic refresh has no mapping change to deliver

also corrects the "fires once" sentence in the settings-reorder item, and rewords the `client.attached` line in `control-api.md` that pointed at a removed item.
a Live pane's daemon keeps its own scrollback. `session text` can read it through a realized pane, but resolves only open-window sessions. `agtermctl` had no direct read for a closed-window daemon.

`agtermctl zmx screen NAME [--all|--lines N]` (`zmx.screen`) prints one daemon's screen by the name `zmx list` shows. The default is the screen at its last leader's size, `--all` adds the retained scrollback, `--lines N` keeps the last N lines of that. It attaches nothing and changes no pane's size. A name with no readable daemon is an error.
the stream had no selection event, so a watcher reacting to a session losing the selection had to poll `tree` and diff the `active` flag.

`session.selected` carries the newly selected session and, as `previous`, the id of the one that lost it. It fires for every cause: a click, navigation, `session select`, a selected `session new` after its `session.created`, and the reselection after the active session closes. Re-selecting the selected session, a window coming forward and a store reloading its snapshot emit nothing.
a Debug build launched with the variable unset resolved the deployed app's directory, so it loaded the live windows, attached the live daemons and rewrote the live window files at quit. A Dock click on a lingering Debug tile is such a launch.

a Debug build now adopts `~/Library/Application Support/agterm-debug` when the variable is unset or empty and exports it before anything reads it, so its shells and their `agtermctl` resolve the same socket. A non-empty value wins, the live directory included. Release is unchanged.
`keymap list` shows the user's custom commands and no verb could run one, so the control API was the one surface that could not reach them.

`agtermctl keymap run NAME [--target T] [--window W]` (`keymap.run`) starts the command with that exact name against the addressed session, as the palette does for the active one. The reply means the process started: the command is detached, so its exit status is not reported, and a launch failure is an error carrying the reason.
the reconnect probe discarded its stderr, so a host-key or authentication failure that began during the wait had no fresh diagnostic: the pane said "reconnecting" and looked the same as an offline host.

each failed probe now keeps its last non-empty stderr line. A waiting pane shows it as one line along its bottom edge, and its surface node in `tree` carries `reconnect` with the retry streak and that reason. Both go when the pane is attached again or closed. The message is not classified and the retry schedule is unchanged, so a host that is only offline is still never given up on.
after a Live restore of an idle Claude Code session a black block sat between the logo and its title. On re-attach, zmx replays the daemon's screen through ghostty's formatter, which wrote cells a program skipped with a cursor move as spaces under the previous cell's style.

zmx now builds against a private checkout of the ghostty revision it pins, with a formatter patch that closes the style before a skipped cell; spaces a program wrote keep their style. `setup.sh` reads that revision from zmx's `build.zig.zon`, patches the checkout and points zmx's dependency at it, and the stamp covers both patch sets. `check.py` gains the two replay cases: the gap one fails on the unpatched build. A daemon already running keeps the old formatter until it is recreated.
a failed index write could leave a newly created window's snapshot missing from an otherwise valid index. A crash before another index write then restored the old index and silently skipped that window.

a failed write now sets `WindowLibrary.indexUnsaved`. While it stands, every window snapshot save that succeeds retries the index, and the exit flush writes snapshots and index together before a Live reset is armed. The tree reports `indexUnsaved` at its top level. A crash with no later write still leaves the stale index: this narrows the gap and exposes the failure in `tree`, and adds no orphan scan.

`saveIndex` and `loadIndex` move to `WindowLibrary+Index.swift` and the snapshot save block to `AppStore+Persistence.swift`, since both files sat at the length limit.
a pane counted as on screen whenever its view had a window, and a window keeps its views while it is minimized, ordered out or hidden with the app. Each such window held the swap chains of its selected session's panes, a shown split and a floating overlay included.

a pane is now on screen only while its window is visible. The view observes `isVisible` on its current window, since ordering a window out or in has no notification of its own and occlusion notifications can miss an order-in. Whole-window occlusion and other Spaces are not part of the test. A program using mode 2033 now gets a visibility report when its window is hidden this way.

measured on an isolated Debug instance with one pane: IOSurface 34.4 MB before, 11.8 MB with 0 resident four seconds after minimizing, 560 KB 38 seconds after minimizing, 34.4 MB three seconds after restoring. Hiding the app gave the same drop, to 11.8 MB with 0 resident after five seconds, and the same return on unhide. Hosted tests order a real window out and in, and attach a pane to a window before it is ordered in.

the accessibility visibility observers move to `GhosttySurfaceView+Accessibility.swift`, since the view file sat at the length limit.
…description

the bundled skill's description and trigger list are what an agent matches a request against, and the three new capabilities were only in the body. They are now named in both, so a request to run a custom command, read a closed window's session or watch the selection reaches the skill.
`RemoteReconnectBook.Entry.failures` and `reason` were public with no reader outside the package. The app reads the reconnect state through `readback(pane:)`.
the `PaneLeadCover` doc said the view draws only while the pane is covered. The reconnect note inside it also draws on an uncovered pane and takes no hits.
the exit flush writes every open store and the index through `saveAllChecked`. The `saveAllOpen` doc comment and the windows and settings rules still described `saveAllOpen` plus `saveIndex` as the quit-time flush.
`ExitFlushSteps` carried a checked and an unchecked save that both called `saveAllChecked`. It now has one save returning whether everything was written, and `exitFlush` reads that result only when a Live sessions reset is pending.
a hosted test miniaturizes a real window and restores it, waiting for the pending hide and for its cancel. Minimize was covered only by a manual measurement.

the accessibility observer becomes `observeAccessibilityExposure`, since `observeWindowVisibilityChanges` read like the renderer's `observeWindowVisibility`. Its comment no longer says nothing else reports `isVisible`.
a Debug build that adopted `agterm-debug` as its state directory also resolved its config to `agterm-debug/config`, so `make run` started without the user's keymap, ghostty config and hooks.

`DebugStateDirectory.configStateDirectory` returns no state directory for the adopted sibling, and the three config resolvers pass its answer to `ConfigPaths.configDirectory`. Any other `AGTERM_STATE_DIR` still redirects config to `<stateDir>/config`.
naming keymap run, the closed-window screen read and selection events pushed the bundled skill description past the spec limit, which `bundledSkillDescriptionFitsTheSpecLimit` checks. The description is reworded to 1023 characters with every trigger kept.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 08:52
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying agterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 96414bd
Status: ✅  Deploy successful!
Preview URL: https://2a4661fe.agterm.pages.dev
Branch Preview URL: https://backlog-sweep.agterm.pages.dev

View logs

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 review overview

🟡 Changes recommended

The symbolication matcher can falsely identify instruction sequences spanning unrelated archive functions.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Sweeps the backlog by implementing control APIs, persistence and rendering fixes, diagnostics, and documentation updates.

Changes:

  • Adds zmx screen, keymap run, selection events, reconnect read-back, and index-write status.
  • Fixes hidden-window rendering, persistence retries, Debug-state isolation, overlays, and zmx replay styling.
  • Adds broad test and documentation coverage while retiring completed or rejected backlog items.
File Description
site/​docs.html Documents events, reconnects, OpenCode, and zmx screen.
site/​commands.html Documents new commands and read-back fields.
scripts/​zmx-patches/​README.md Explains private Ghostty patching.
scripts/​zmx-patches/​ghostty/​0001-formatter-unstyled-blanks.patch Fixes styled replay gaps.
scripts/​zmx-patches/​check.py Tests replay formatting.
scripts/​zmx-patches/​0002-private-ghostty-dependency.patch Redirects zmx to patched Ghostty.
scripts/​symbolicate-frames.py Adds stripped-frame symbolication.
scripts/​setup.sh Builds zmx with private Ghostty patches.
project.yml Updates Debug isolation guidance.
plugins/​agterm/​skills/​agterm/​troubleshooting.md Notes OpenCode compatibility.
plugins/​agterm/​skills/​agterm/​SKILL.md Advertises new control capabilities.
plugins/​agterm/​skills/​agterm/​reference.md Documents new API contracts.
docs/​troubleshooting.md Clarifies OpenCode support.
docs/​backlog/​zmx-replay-paints-unwritten-cells-with-the-previous-style.md Removes completed backlog item.
docs/​backlog/​wrapper-pane-foreground-capture-unverified.md Removes dropped backlog item.
docs/​backlog/​stripped-libghostty-frames-need-a-symbolication-script.md Removes completed backlog item.
docs/​backlog/​stale-window-index-orphans-a-window-file-silently.md Removes completed backlog item.
docs/​backlog/​session-type-long-or-multi-line-input-reads-as-paste-in-an-agent-tui.md Removes documented backlog item.
docs/​backlog/​restored-window-reveal-skips-sidebar-reconcile.md Removes completed backlog item.
docs/​backlog/​reconnect-probe-hides-a-permanent-ssh-failure.md Removes completed backlog item.
docs/​backlog/​overlay-wrapper-reads-login-profile-under-non-bash-sh.md Removes completed backlog item.
docs/​backlog/​opencode-integration-does-not-say-it-is-v1-only.md Removes completed backlog item.
docs/​backlog/​no-draft-marker-independent-of-the-status-slot.md Removes dropped backlog item.
docs/​backlog/​no-control-verb-runs-a-custom-command.md Removes completed backlog item.
docs/​backlog/​no-cli-read-of-a-detached-daemon-scrollback.md Removes completed backlog item.
docs/​backlog/​host-side-remote-client-events.md Removes dropped backlog item.
docs/​backlog/​hidden-windows-keep-swap-chains.md Removes completed backlog item.
docs/​backlog/​events-stream-has-no-selection-change-event.md Removes completed backlog item.
docs/​backlog/​edit-keymap-menu-hides-the-extension-mechanism.md Removes dropped backlog item.
docs/​backlog/​debug-build-defaults-to-live-state.md Removes completed backlog item.
docs/​backlog/​claude-settings-merge-reorders-the-whole-file.md Records the retained decision.
docs/​backlog/​claude-hook-table-changes-never-reach-existing-installs.md Removes resolved backlog item.
docs/​backlog/​accept-thread-waits-on-main-actor-without-bound.md Removes dropped backlog item.
CLAUDE.md Updates build and Debug-state guidance.
ARCHITECTURE.md Documents hidden-window occlusion.
agtermTests/​LiveResetQuitTests.swift Tests checked exit persistence.
agtermTests/​GhosttySurfaceViewTrackingTests.swift Tests window visibility transitions.
agtermTests/​CustomCommandRunnerTests.swift Tests control-triggered custom commands.
agtermTests/​ControlServerZmxTests.swift Tests daemon screen reads.
agtermTests/​ControlServerRemoteReconnectTests.swift Tests reconnect read-back.
agtermTests/​AppActionsFocusTests.swift Tests sidebar reconciliation.
agtermCore/​Tests/​agtermctlKitTests/​ZmxCommandsTests.swift Tests zmx screen CLI parsing.
agtermCore/​Tests/​agtermctlKitTests/​MiscCommandsTests.swift Tests keymap run CLI parsing.
agtermCore/​Tests/​agtermctlKitTests/​EventCommandsTests.swift Tests selection event formatting.
agtermCore/​Tests/​agtermCoreTests/​WindowLibraryIndexTests.swift Tests index retry behavior.
agtermCore/​Tests/​agtermCoreTests/​RemoteReconnectTests.swift Tests reconnect reasons and streaks.
agtermCore/​Tests/​agtermCoreTests/​OverlayCaptureTests.swift Tests non-login overlay execution.
agtermCore/​Tests/​agtermCoreTests/​MockControlActions.swift Extends control mocks.
agtermCore/​Tests/​agtermCoreTests/​HtmlBridgeTests.swift Tests page-scoped keymap execution.
agtermCore/​Tests/​agtermCoreTests/​DebugStateDirectoryTests.swift Tests Debug-state adoption.
agtermCore/​Tests/​agtermCoreTests/​ControlEventProtocolTests.swift Tests selection event encoding.
agtermCore/​Tests/​agtermCoreTests/​ControlDispatcherZmxTests.swift Tests zmx screen dispatch.
agtermCore/​Tests/​agtermCoreTests/​ControlDispatcherTests.swift Tests keymap run dispatch.
agtermCore/​Tests/​agtermCoreTests/​AppStoreEventTests.swift Tests selection event emission.
agtermCore/​Sources/​agtermctlKit/​ZmxCommands.swift Adds zmx screen.
agtermCore/​Sources/​agtermctlKit/​MiscCommands.swift Adds keymap run.
agtermCore/​Sources/​agtermctlKit/​EventCommands.swift Formats selection events.
agtermCore/​Sources/​agtermCore/​WindowLibrary+Index.swift Implements checked index persistence.
agtermCore/​Sources/​agtermCore/​WindowLibrary.swift Integrates index retries.
agtermCore/​Sources/​agtermCore/​RemoteReconnect.swift Stores reconnect diagnostics.
agtermCore/​Sources/​agtermCore/​OverlayCapture.swift Defines a non-login wrapper.
agtermCore/​Sources/​agtermCore/​HtmlBridge.swift Targets keymap runs to page sessions.
agtermCore/​Sources/​agtermCore/​DebugStateDirectory.swift Resolves isolated Debug state.
agtermCore/​Sources/​agtermCore/​ControlProtocol.swift Adds new protocol commands.
agtermCore/​Sources/​agtermCore/​ControlProjection.swift Adds reconnect and index projections.
agtermCore/​Sources/​agtermCore/​ControlEvents.swift Adds selection events.
agtermCore/​Sources/​agtermCore/​ControlDispatcher+Zmx.swift Dispatches screen reads.
agtermCore/​Sources/​agtermCore/​ControlDispatcher.swift Dispatches custom commands.
agtermCore/​Sources/​agtermCore/​ControlActionsDefaults.swift Adds compatibility defaults.
agtermCore/​Sources/​agtermCore/​ConfigPaths.swift Documents Debug config resolution.
agtermCore/​Sources/​agtermCore/​AppStore+Persistence.swift Extracts persistence behavior.
agtermCore/​Sources/​agtermCore/​AppStore+Events.swift Emits selection events.
agtermCore/​Sources/​agtermCore/​AppStore.swift Integrates events, retries, and projections.
agterm/​Views/​SettingsView.swift Uses Debug-aware config resolution.
agterm/​Views/​PaneLeadCover.swift Displays reconnect reasons.
agterm/​SettingsModel.swift Uses Debug-aware config resolution.
agterm/​Ghostty/​GhosttySurfaceView+Visibility.swift Tracks window visibility for rendering.
agterm/​Ghostty/​GhosttySurfaceView+Accessibility.swift Separates accessibility notifications.
agterm/​Ghostty/​GhosttySurfaceView.swift Manages visibility observation lifecycle.
agterm/​Ghostty/​GhosttyApp.swift Preserves default Debug config lookup.
agterm/​Control/​ControlServer+Zmx.swift Reads daemon screens.
agterm/​Control/​ControlServer+RemoteReconnect.swift Captures probe errors.
agterm/​Control/​ControlServer+AppCommands.swift Runs named custom commands.
agterm/​Control/​ControlServer.swift Exposes new commands and index status.
agterm/​Commands/​CustomCommandRunner.swift Runs commands for addressed sessions.
agterm/​AppDelegate.swift Checks snapshots and index before reset.
agterm/​AppActions+Focus.swift Reuses frontmost-window reconciliation.
agterm/​agtermApp.swift Adopts Debug state and overlay wrapper.
agterm/​AgentHooksInstaller.swift Reports OpenCode 1.x support.
.github/​workflows/​ci.yml Includes nested patches in cache keys.
.claude/​rules/​windows.md Documents index retry semantics.
.claude/​rules/​settings.md Documents Debug config and exit saves.
.claude/​rules/​libghostty.md Documents occlusion and symbolication.
.claude/​rules/​control-api.md Documents new control contracts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +93 to +94
return [i for i, token in enumerate(self.tokens)
if token == last and i + 1 >= size and self.tokens[i + 1 - size:i + 1] == window]
@umputun
umputun merged commit 75a7c4d into master Oct 3, 2026
9 checks passed
@umputun
umputun deleted the backlog-sweep branch October 3, 2026 09:10
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