Mac app: nested Hera-tree sidebar mode + dual-pane detail view - #948
Merged
Conversation
Converged the TUI keymap into a per-key mac-app resolution (shortcuts, context menus, or deferred) via user brainstorm.
Converged with the user via investigation-grounded brainstorm: nested tree sidebar mode, dual-pane coordinator/agent detail view, and the REST nesting fields needed to drive it (finishing a pre-existing named follow-up in the rest-api base spec).
Mechanical rename in place ahead of extracting model.go into its own package: bridgeIndex, consumedSet, coordBridgeChildren, roleOrchID, roleByID, canonicalParents, bridgingRoleFor, formatCostUSD, bridgeTaskID, roleBridges, annotateRoles, needsInputRoleIDs, rollupNeedsInput, and the canonParent type (+ its OrchID/CoordSpawn fields) are called from rail.go/page.go/plan.go/details.go/ops.go/ adopt.go/eol.go and several _test.go files, so they can't stay unexported once they move to a separate package. rail.go's two needsInputOwn() call sites now go through the existing public ShowsNeedsInput() wrapper instead of needing a new export. No behavior change; full repo builds and internal/tui/hera tests pass.
Stage 2 (add-mac-hera-rail-toggle): moves RoleView/OrchView/Model/ Selection/HeraReader/BuildModel and the bridging helpers out of internal/tui/hera/model.go into a new internal/hera/model package with zero tview/tcell imports, so the REST API can share the TUI rail's nesting/bridging logic instead of reimplementing it. internal/tui/hera re-exports the five moved types as type aliases so the App-level heraactions.go/hera_tiering.go/etc. keep compiling unchanged; a method defined outside model.go (SubtreeArchivedWorkers) became a plain function since Go forbids methods on foreign types even via an alias. model_test.go's shared test builders (used by ~20 other hera-package test files) were duplicated rather than moved; its model-internals-only tests moved with model.go. Added direct unit tests for functions that lost their internal/hera/model-attributed coverage in the split. Stage 3: internal/api/hera.go's handleHera now calls heramodel.BuildModel (sourcing needsInput/sessionIdle/sessionRunning from the same Server.sessionStateMaps() that already backs GET /api/tasks and the SSE stream) instead of hand-rolling the orchestrator/role walk, and adds bridge_parent_orch_id/ bridge_parent_role_id (via new Model.BridgeParentOf), subtree_needs_input, and needs_input to the JSON envelope — additive only, every existing field's shape and meaning is unchanged. make pre-pr gates (build/vet/fmt-check/full race test suite/coverage gate) all pass.
Plain function, like its heraRoleJSONFrom sibling — it never touched s.
…hild Review finding: no test at the REST layer exercised the rest-api delta spec's "subtree_cost_usd does not recurse into a nested sub-coordinator" scenario — TestModel_SubtreeCostUSD_NestedSubCoordinatorCountedOnce covers the TUI-only recursive Model.SubtreeCostUSD, a different method than what buildHeraOrchJSON actually calls. Verified this test catches the regression by temporarily injecting a fake recursive sum (failed with got 102 want 2) before confirming it passes against the real code.
Adds a sidebar-local toggle between the flat task list and a nested,
kanban-grouped Hera tree, sourced from GET /api/hera's new nesting fields.
The old toolbar Hera roster (HeraTab/showingHera) stays untouched and
functional until Stage 6 retires it; Stage 5's dual-pane detail view gets
a minimal, clearly-marked placeholder for now.
New ArgusKit (pure, unit-tested) types:
- HeraOrchestrator gains kanbanStatus/bridgeParentOrchID/bridgeParentRoleID/
subtreeNeedsInput; HeraRole gains needsInput.
- HeraTreeBuilder.build(_:HeraRoster) -> HeraTree: groups top-level
orchestrators by kanban_status (canonical order, unrecognized statuses
sort last) and nests bridge-child orchestrators under their parent's
bridging role, recursing to any depth. A dangling bridge-parent reference
falls back to top-level rather than being silently dropped.
- HeraFoldState: explicit, id-keyed local fold/expand state (not SwiftUI's
implicit OutlineGroup state) so collapsing a node is a testable, AppState-
owned value that survives a roster refresh via the node's stable id.
New AppState surface (for Stage 5 to build on):
- SidebarMode { tasks, hera } + var sidebarMode
- var selectedHeraRoleID: Int64? (separate selection channel from
selectedTaskID, so toggling sidebarMode never disturbs either selection)
- var heraFoldState: HeraFoldState
- private(set) var currentHeraRoster: HeraRoster? + refreshHeraRoster()
(wraps heraRoster(), also caches the result)
- var activeHeraOrchestrator: HeraOrchestrator? (the orchestrator containing
the current role selection)
- func selectHeraRole(_ roleID: Int64)
New view: HeraTreeSidebar.swift (mirrors HeraTab's fetch+poll pattern rather
than sharing its private state, since HeraTab itself isn't touched this
stage). Sidebar.swift gained a segmented Picker and now branches between the
flat list and the tree. ContentView.swift's detail pane gained a TEMPORARY
placeholder branch for Hera-tree-mode role selection.
make mac-build and make mac-test (153 tests) both pass clean.
…gle) Mirrors the TUI's real geometry: the active orchestrator's coordinator pane plus the selected role's own pane, or a roster-list region when the selection is itself a coordinator. Pane selection is extracted into a pure, unit-tested HeraDetailPaneResolver in ArgusKit since ArgusKitTests has no SwiftUI/AppState harness to exercise the view directly.
The sidebar's nested Hera-tree mode and dual-pane detail view (Stages 4-5) fully subsume the old flat toolbar roster, so remove it: delete AppState.showingHera/selectHeraTask and HeraTab.swift's mount point, relocating its still-needed row components (HeraOrchestratorHeader, HeraRoleRow, KindBadge, RoleStatusIcon) into a new HeraRosterComponents.swift shared with HeraDetailView's roster pane. HeraRoleRow now calls selectHeraRole instead of selectHeraTask so clicking a role inside the dual-pane roster region stays in Hera mode and updates the right pane, rather than kicking the user out to the flat view; the row's disabled guard is dropped since selecting an unbound role is now meaningful (HeraDetailPaneResolver already renders it as "Unbound"). Also consolidated HeraTreeSidebar's duplicate HeraKindBadge onto the now-shared KindBadge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Add the REST-vs-TUI live-state-source distinction to gotchas/hera-view.md and a HeraTreeBuilder/HeraDetailPaneResolver pure-logic + single-poll-loop entry to gotchas/macos-app.md. Update the README's GET /api/hera reference row with kanban_status, bridge_parent_orch_id/bridge_parent_role_id, and subtree_needs_input/needs_input.
Merge the rest-api and macos-app delta specs into their base specs and move the change folder to openspec/changes/archive/2026-08-22-add-mac-hera-rail-toggle/ (openspec archive --yes; folder dated by the CLI's UTC clock, one day ahead of local time at archive time). Checked off tasks 7.1-7.4 in the archived tasks.md; all 30 tasks across all 7 stages are now [x]. openspec validate --all --strict: 61 passed, 0 failed.
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)
Merging this branch changes the coverage (2 decrease, 2 increase)
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
|
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
Replaces the macOS companion app's toolbar Hera toggle (a flat, all-orchestrators roster) with a real nested Hera rail in the sidebar, mirroring the TUI's native Hera view — plus a genuine coordinator/agent dual-pane detail view. Finishes a pre-existing, deliberately-deferred REST gap along the way.
Daemon (
internal/hera/model,internal/api/hera.go):internal/tui/hera/model.go's nesting/bridging logic (RoleView/OrchView/Model/BuildModel/bridging helpers) plusreader.go'sHeraReaderinterface into a new tview-free packageinternal/hera/model, importable by both the TUI and the daemon's REST API.internal/tui/herare-exports the moved types as Go type aliases so its ~30 existing files (including ~25 test files) kept compiling with only mechanical import/qualification edits — its full existing test suite passes unchanged.GET /api/heranow callsinternal/hera/model.BuildModeldirectly and gains additive fields:bridge_parent_orch_id/bridge_parent_role_id(null when top-level),subtree_needs_input(per orchestrator), andneeds_input(per role) — sourced from the same daemon-authoritative idle-detection signal (Runner.RunningAndIdle()/NeedsInputIDs(), viasessionStateMaps()) that already backsGET /api/tasksand the SSE events stream, not from any TUI-session-local state. Every existing field's shape and meaning is unchanged;subtree_cost_usd's non-recursive semantics are untouched (a separate, still-deferred follow-up).macOS app:
HeraTreeBuilderin ArgusKit (mirrors the existingTaskGrouping/FileTreeBuilderpattern) with local, unpersisted fold/expand state.HeraDetailView: the active orchestrator's coordinator pane + the selected role's agent pane side by side, swapping to a read-only roster-list region when the selection is itself a coordinator — driven by a pureHeraDetailPaneResolver, also unit-tested in ArgusKit.HeraTab.swiftnow that the sidebar mode fully subsumes it; its still-needed roster-row components were relocated toHeraRosterComponents.swift. Clicking a role inside the new roster region now stays in Hera mode (selectHeraRole) instead of kicking back to the flat view.Docs: gotcha entries in
gotchas/hera-view.mdandgotchas/macos-app.mdcovering the package extraction, the REST-vs-TUI live-state-source distinction, and the mac app's pure-decision-logic pattern; README's/api/herareference row updated with the new fields.Explicitly out of scope (named follow-ups, not silently dropped): the plan-DAG graph inside a coordinator's details region, any Hera mutation over REST (stays TUI-only), the pre-existing
subtree_cost_usdrecursive-rollup gap, and web SPA parity for the new REST fields.OpenSpec change
add-mac-hera-rail-togglearchived into base specs on this branch (openspec/specs/rest-api,openspec/specs/macos-app) before merge.Test plan
make test(full Go suite,-race): green, 88.7% filtered coverage (floor 88%)make vet,golangci-lint run --new-from-rev=origin/master: 0 issuesmake mac-build: cleanmake mac-test: 161/161 passing (includes newHeraTreeBuilder,HeraDetailPaneResolversuites)openspec validate --all --strict: 61 passed, 0 failed🤖 Generated with Claude Code