Mac app: keyboard-shortcut parity with the TUI - #946
Merged
Conversation
Converged the TUI keymap into a per-key mac-app resolution (shortcuts, context menus, or deferred) via user brainstorm.
…ng-parity) TDD red phase for the five ArgusKit pure-logic seams later stages implement: TaskStatus.advanced()/reverted(), NeedsInputNavigation.next(), TerminalChords' intercepted-chord allowlist, ArgusClient.setPinned (raw JSON round trip), and ArgusClient.pruneCompleted. None of the referenced APIs exist yet, so make mac-build/mac-test fail to compile by design, pinning the exact signatures Stages 2-6 implement against. Also extends MockURLProtocol with a request log so a test can assert GET-then-PUT ordering for the pin round trip. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…n endpoints
Implementation surfaced a real gap in design.md's "no daemon/REST API
changes" claim: internal/claudesession has no HTTP route, so the mac app
(a REST-only client) can't reach it for the Claude session switcher (D3).
Escalated to and approved by the hera coordinator. Adds a minimal
GET/POST /api/tasks/{id}/claude-session(s) delta under specs/rest-api/,
corrects design.md/proposal.md's Impact sections, and names the web SPA's
own use of these endpoints as an explicit follow-up per this repo's
Frontend Parity rule.
Task rail filter access, per openspec/changes/add-mac-keybinding-parity: - Sidebar.swift gains a filter bar with a case-insensitive substring filter over task name, and a persistent visible toggle for hera-managed task visibility (default on, matching current behavior). - Cmd+F focuses the filter field via a hidden button + @focusstate, the same "shortcut on the performing button" pattern as the existing Cmd+N/Cmd+R toolbar shortcuts. - AppState gains heraManagedTaskIDs, rebuilt from GET /api/hera (there is no such field on the ArgusKit.Task wire shape) alongside every task snapshot, mirroring how HeraTab already cross-references role bindings by task id. Documents both the hera-managed-derivation and the Cmd+F/SwiftTerm-focus open question in gotchas/macos-app.md. No new mac-build/mac-test failures: the full-package build is red on this branch by Stage 1's own design (pending Stages 2/3/5's ArgusKit symbols); verified the compile-error set is byte-identical before and after this change, and the Argus target alone builds clean in isolation.
…eybinding-parity)
Implements the mac app's chrome-level shortcuts and menu items:
- NeedsInputNavigation.next(): pure cycling logic for jump-to-next-
needs-input, making Stage 1's NeedsInputNavigationTests pass.
- ArgusClient.pruneCompleted() + PruneReport model: POSTs
/api/maintenance/prune-completed, mirroring internal/apiclient's
JSON shape; makes Stage 1's ClientRequestTests pruneCompleted case
pass.
- AppState: openRepo(_:), openPR(for:), jumpToNextNeedsInput(),
pruneCompleted() wrapper, isPresentingShortcutsHelp, and a new
.pruneCompleted case on PendingConfirmation.
- TaskActionMenuItems: Open Repo / Open PR buttons, plus keyboard
shortcuts on Fork/Delete/Open Repo/Open PR when showShortcuts is set
(mirrors the existing Rename pattern; no centralized dispatch table
per design.md D1).
- TaskDetailTabs: Cmd+1/2/3/4 tab-switch shortcuts.
- ContentView: Cmd+Shift+/ shortcuts-help sheet, Cmd+Shift+J jump-to-
needs-input, and a new toolbar overflow ("…") menu with "Prune Stale
Worktrees" (gated behind the existing confirmation-dialog mechanism).
- ShortcutsHelpSheet: new sheet listing the app's shortcuts (hardcoded
static list per D1, including other stages' chords for a single
future reference).
Chord table: Help ⇧⌘/, Destroy ⌘⌫, Fork ⇧⌘B, Open Repo ⇧⌘E, Open PR
⇧⌘U, Jump-to-needs-input ⇧⌘J — checked against the existing 4
shortcuts, this change's other fixed chords, and macOS HIG reserved
shortcuts (tasks.md 2.1).
Verified in isolation (Stage 3's setPinned/TaskStatus.advanced/
reverted and Stage 5's TerminalChords/KeyChord don't exist yet, so
the full make mac-build/mac-test can't go green until those stages
land): temporarily set aside TaskStatusTransitionTests.swift,
TerminalChordsTests.swift, and the setPinned cases in
ClientRequestTests.swift, confirmed clean build + all 152 remaining
tests pass (including every NeedsInputNavigation and pruneCompleted
case), then restored all three exactly via git checkout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
…hortcuts
Stage 3 of add-mac-keybinding-parity:
- ArgusKit.TaskStatus.advanced()/reverted(), mirroring internal/model/status.go's
Next()/Prev() clamp semantics; .other(_) is a no-op in both directions.
- ArgusClient.setPinned(id:pinned:), a GET/PUT round trip against
/api/tasks/{id}/raw so fields the lossy Task model can't decode
(session_id, result, ...) survive the write. Also mirrors the
pinned/archived mutual-exclusivity invariant client-side, since
handleUpdateTaskRaw's plain db.Update does not enforce it.
- TaskRow.swift: right-click context menu gains Advance/Revert Status,
disabled at the ladder's clamp points.
- TaskActions.swift: Archive/Unarchive gets Cmd+Shift+A; a new Pin/Unpin
action (net-new UI) gets Cmd+Shift+P.
- AppState: setStatus wrapper; pinnedTaskIDs client-side cache (lossy
/api/tasks omits `pinned` entirely) backfilled on-demand via TaskRow/
DetailView .task(id:) modifiers and kept current by optimistic updates
plus archive/delete event reconciliation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Review finding: Sidebar's filteredFolders was a private View method reading @State/@Environment directly, so the filter + hera-managed-toggle logic had no unit test (tasks.md 1.3/4.3, design.md's Prove-It Pattern). Move the actual predicate into a pure ArgusKit.TaskFiltering.filteredFolders free function over plain data (folders, filterText, showHeraManaged, heraManagedTaskIDs) -> [TaskFolder], mirroring Stage 1's other pure-logic seams (NeedsInputNavigation, TaskStatus.advanced/reverted). Sidebar now just calls it with its own state. Adds TaskFilteringTests.swift covering: empty/whitespace filter text as a no-op, a matching substring (case-insensitive), a non-matching substring dropping the folder, the hera-managed toggle hiding/showing a managed task, and the two filters combining (AND). Verified the exact same assertions pass against the real ArgusKit source via a standalone smoke harness (the in-repo ArgusKitTests executable still can't build — unrelated, pre-existing Stage 1 stub symbols pending Stages 2/3/5). make mac-build/mac-test: confirmed the compile-error set is still byte-identical to the pre-fix baseline; no new failures introduced.
ArgusKit gains a ClaudeSession model (Codable, decoding mod_time into a
real Date rather than the SDK's usual raw RFC3339 String) and two
ArgusClient methods, claudeSessions(taskID:) and
switchClaudeSession(taskID:sessionID:), against the fixed daemon
contract from specs/rest-api/spec.md's "Claude session switcher" delta.
ArgusError gains isBadRequest to let callers distinguish a 400
(non-Claude-backed task) from other failures.
The Argus app wires a toolbar button into the Terminal tab (shown only
when the task's backend isn't a known non-Claude backend) that fetches
sessions and opens a new picker sheet, ClaudeSessionPickerSheet,
listing them newest-first with the current one checkmarked. Selecting
a session POSTs the switch; both "switched" and "unchanged" dismiss
the sheet, trusting the terminal tab's existing SSE reconnect (the
same exit{"rerendering":true} path an ordinary resize kick-restart
already uses) to pick up the new session's output. A 500 or other
failure surfaces via the existing ActionErrorBanner instead of
crashing or leaving a broken sheet open.
Documents three non-obvious gotchas: ISO8601DateFormatter can't be
cached in a static let under this package's strict-concurrency check,
modTime is the only ArgusKit timestamp decoded to a Date, and the
mac-side "is this task Claude-backed" check is an exclude-list mirroring
the daemon's own guard, not an include-list.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Stage 3's review flagged that proposal.md/design.md/spec.md described pin as "the existing... pin action" when it never existed in the mac app before this change (confirmed: no pinned field on the lossy task wire shape, no dedicated REST endpoint). Cosmetic but real spec drift.
…rror Review flagged the highest-risk addition in Stage 3 (setPinned's if pinned, case .bool(true)? = obj["archived"] flip) as untested: the existing rawTaskFixture() never included an archived:true key, so the flip branch never actually ran under test. Added an archived parameter to the fixture and three new cases: - pinning an already-archived task flips archived to false in the PUT body while every other key, including pinned itself, round-trips - an already-false archived key is left alone (present, unchanged) - an omitted archived key is never inserted 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Adds GET /api/tasks/{id}/claude-sessions and POST /api/tasks/{id}/claude-session,
the daemon-side REST mirror of the TUI's ctrl+r Claude session switcher
(internal/claudesession + app.go's openSessionPickerModal/switchSession), so
the REST-only mac app can reach the same data and mechanism. Implements D3 of
openspec/changes/add-mac-keybinding-parity/design.md.
The switch endpoint stops/restarts a live session via the existing
Runner.KickRerender primitive (never hand-rolled Stop+Start, which would race
the runner's own exit-cleanup goroutine) and bridges KickRerender's async gap
with a bounded poll on HasPendingRestart/Get so the response can carry the
new pid synchronously.
…ding-parity # Conflicts: # macos/Sources/Argus/AppState.swift # macos/Sources/Argus/TaskActions.swift
…nding-parity # Conflicts: # context/knowledge/gotchas/macos-app.md # context/knowledge/index.md # macos/Sources/Argus/AppState.swift # macos/Sources/Argus/ContentView.swift
Adds the Terminal tab's fixed 10-chord intercepted allowlist (ArgusKit.TerminalChords/KeyChord) and wires it into a new local NSEvent .keyDown monitor in FocusTakingTerminalView, alongside the existing .leftMouseDown focus-reclaim monitor, so Cmd+Up/Down (task switch), Cmd+Left/Right (detail-tab cycle), Shift+Up/Down/PageUp/ PageDown/End (scrollback), and Cmd+Shift+C (copy visible output) never reach SwiftTerm's keyDown or POST /input. - ArgusKit: KeyChord/TerminalChords (Stage 1's pinned allowlist), KeyChordDecoding (pure virtual-keycode table), TaskNavigation (clamped adjacent-task stepping), CyclicSelection (generic wrap-around stepping for detail-tab cycling) - all with tests. - AppState: selectPreviousTask/selectNextTask, cycleDetailTab(forward:), task(withID:); terminalController(for:) wires the view's controller/appState back-references. - TerminalController: scrollLineUp/Down, scrollPageUp/Down, scrollToBottom (SwiftTerm scrollUp/scrollDown/scroll(toPosition:), never pageUp()/pageDown() which can leak bytes to the PTY in alt-screen mode), copyVisibleOutput (Terminal.getLine + translateToString, not select-all+getSelection). - A second, separate one-entry allowlist in the same monitor swallows Cmd+Shift+U as a terminal-focused fallback for the chrome-level Open PR shortcut, per Stage 2's review finding - kept structurally apart from TerminalChords.intercepted. - ChromeShortcutCollisionTests pins disjointness against every real chrome-level .keyboardShortcut (tasks.md 5.4). - Documents the mechanism in gotchas/macos-app.md. make mac-build && make mac-test: 244 tests / 19 suites, all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Stage 5's review flagged that specs/macos-app/spec.md, design.md, and proposal.md still described this as "pane focus" (a TUI agent-view concept that doesn't exist in the mac app, which has one terminal surface per task, not a split-pane view). The implementation itself was always correct (design.md's own D2 already required this exact resolution be made during implementation); only the doc wording had drifted. Non-blocking per the review, fixed before archive.
Documents the full new chord set in the macOS app's Reference section (no such table existed before this change). Gotcha-doc coverage (7.1) was already delivered by Stage 5; verified full mac-test green (7.3).
lint-pr flagged claudesessions.go's uint16(rows)/uint16(cols) as an unchecked integer-overflow conversion. Added clampToUint16, mirroring the existing pattern where /resize's rows/cols already arrive typed as uint16 from the request body and never needed this — PTYSize() returns plain ints here, so the conversion needs its own guard.
openspec archive add-mac-keybinding-parity: merges the macos-app and rest-api deltas into their base specs and moves the change folder to openspec/changes/archive/2026-08-21-add-mac-keybinding-parity/, per this repo's CLAUDE.md (archive lands in the same PR, before merge). Checked off 1.4/1.5/7.4 in the archived tasks.md — their work landed in Stages 5/6 respectively, just wasn't retroactively marked done. `openspec validate --all --strict`: 60/60 specs pass.
…g-parity # Conflicts: # context/knowledge/index.md
Merging this branch will decrease overall coverage
Coverage by fileChanged files (no unit tests)
Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code. Changed unit test files
|
anutron
added a commit
that referenced
this pull request
Aug 22, 2026
Resolves conflicts with the sibling add-mac-keybinding-parity change (#946/#947), which merged first and, via shared branch ancestry from a common batch-brainstorm starting point, both (a) genuinely overlapped this branch in Sidebar.swift and (b) accidentally carried forward this branch's already-archived openspec/changes/add-mac-hera-rail-toggle/ scaffolding as "new" from master's perspective (that squash-merge had no shared file history with this branch's own archival commit, so git resurrected the stale, all-unchecked pre-work copy instead of treating it as already-deleted). Conflict resolution: - openspec/changes/add-mac-hera-rail-toggle/*: removed — resurrected stale/unarchived duplicate; this branch's completed, checked-off, base-spec-merged archive at openspec/changes/archive/2026-08-22-add-mac-hera-rail-toggle/ stands. - macos/Sources/Argus/Sidebar.swift: real merge — kept this branch's sidebar-mode Picker (Tasks/Projects toggle) at the top level, nested master's filter bar + task-name filtering inside the `.tasks` case (filtering has no meaning in the Hera tree), adopted master's navigationTitle/onDeleteCommand placement at the outer VStack level since those apply regardless of mode. - openspec/specs/macos-app/spec.md, context/knowledge/gotchas/macos-app.md, context/knowledge/index.md: purely additive on both sides (independent new requirements/gotcha entries) — concatenated, index.md bullet count corrected to reflect the merged total. - openspec/specs/rest-api/spec.md: auto-merged cleanly, no manual changes. Verified post-merge: go build ./... clean, make mac-build clean, make mac-test 266/266 passing (161 from this branch + 105 from add-mac-keybinding-parity), full `go test -race` suite green at 88.7% coverage (floor 88%), openspec validate --all --strict 61/0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5 tasks
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.
Summary
Ports the TUI's useful keyboard actions into the macOS companion app so a user working entirely from the mac app never has to fall back to opening the TUI:
NSEventmonitor before they reach SwiftTerm, so they never leak into the agent's input stream. Every other keystroke, including all Ctrl chords, is forwarded unchanged (non-regression, verified both by a chord-collision test and a code-level trace of the monitor's two-path structure).Scope correction found during implementation: the Claude-session-switcher piece needed two new minimal daemon REST endpoints (
GET/POST /api/tasks/{id}/claude-session(s)) —internal/claudesessionhad no HTTP route, only in-process TUI access. This was escalated to and approved by the coordinator; seedesign.mdD3 in the archived change for the full rationale. The session-switch endpoint reuses the existingRunner.KickRerenderprimitive rather than a hand-rolled stop+start, avoiding a session-map race.Explicitly deferred (named follow-ups, not silently dropped): a global command palette, a global task/role switcher beyond direct sidebar selection, restore-rail, copy branch/path, manual refresh, Files-tab per-row actions, all Hera-rail mutation keys, and — newly — the web SPA's own use of the two new session-switcher endpoints (Frontend Parity note, matching the existing "hera mutations are TUI-only" precedent).
Process
Built via
/execute-planagainstopenspec/changes/add-mac-keybinding-parity(proposal/design/delta specs pre-approved): 7 stages, most run in parallel worktrees, each through a two-stage review (spec compliance, then code quality) before merging back. Every review-found issue was fixed and re-verified, not waved through — including an independently-traced concurrency claim in the daemon-side session switch, and a SwiftTerm API risk (pageUp()/pageDown()silently forward bytes to the PTY in alt-screen mode) caught by reading the vendored source rather than trusting the convenience API. Change archived in this same PR per this repo's CLAUDE.md.Test plan
make pre-pr— build/vet/fmt-check/lint-pr clean;vuln's failures are pre-existing stdlib-only CVEs (CI-advisory, continue-on-error);test-cover-gatepasses at 88.7% filtered coverage once the sandbox's ownARGUS_*env vars are excluded from theinternal/agentsubprocess (documented pre-existing hera-worker-sandbox artifact, confirmed unrelated to this change)make mac-build&&make mac-test— 244/244 tests, 19 suites, cleanopenspec validate --all --strict— 60/60 specs pass🤖 Generated with Claude Code