Skip to content

Mac app: nested Hera-tree sidebar mode + dual-pane detail view - #948

Merged
anutron merged 18 commits into
masterfrom
argus/mac-hera-rail-toggle
Aug 23, 2026
Merged

Mac app: nested Hera-tree sidebar mode + dual-pane detail view#948
anutron merged 18 commits into
masterfrom
argus/mac-hera-rail-toggle

Conversation

@anutron

@anutron anutron commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

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):

  • Extracted internal/tui/hera/model.go's nesting/bridging logic (RoleView/OrchView/Model/BuildModel/bridging helpers) plus reader.go's HeraReader interface into a new tview-free package internal/hera/model, importable by both the TUI and the daemon's REST API. internal/tui/hera re-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/hera now calls internal/hera/model.BuildModel directly and gains additive fields: bridge_parent_orch_id/bridge_parent_role_id (null when top-level), subtree_needs_input (per orchestrator), and needs_input (per role) — sourced from the same daemon-authoritative idle-detection signal (Runner.RunningAndIdle()/NeedsInputIDs(), via sessionStateMaps()) that already backs GET /api/tasks and 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:

  • New sidebar mode toggle switching between the flat task list and a nested Hera tree (kanban-grouped, bridge-nested), built via a pure HeraTreeBuilder in ArgusKit (mirrors the existing TaskGrouping/FileTreeBuilder pattern) with local, unpersisted fold/expand state.
  • New dual-pane 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 pure HeraDetailPaneResolver, also unit-tested in ArgusKit.
  • Retired the old toolbar toggle and HeraTab.swift now that the sidebar mode fully subsumes it; its still-needed roster-row components were relocated to HeraRosterComponents.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.md and gotchas/macos-app.md covering the package extraction, the REST-vs-TUI live-state-source distinction, and the mac app's pure-decision-logic pattern; README's /api/hera reference 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_usd recursive-rollup gap, and web SPA parity for the new REST fields.

OpenSpec change add-mac-hera-rail-toggle archived 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 issues
  • make mac-build: clean
  • make mac-test: 161/161 passing (includes new HeraTreeBuilder, HeraDetailPaneResolver suites)
  • openspec validate --all --strict: 61 passed, 0 failed
  • Every stage independently reviewed (spec compliance + code quality) by a fresh agent before merge into this branch

🤖 Generated with Claude Code

anutron and others added 18 commits August 21, 2026 11:52
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)
@github-actions

Copy link
Copy Markdown

Merging this branch changes the coverage (2 decrease, 2 increase)

Impacted Packages Coverage Δ 🤖
github.com/drn/argus/internal/api 87.00% (+0.01%) 👍
github.com/drn/argus/internal/hera/model 94.43% (+94.43%) 🌟
github.com/drn/argus/internal/tui 83.22% (-0.01%) 👎
github.com/drn/argus/internal/tui/hera 91.86% (-0.64%) 👎

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/drn/argus/internal/api/hera.go 96.23% (+2.29%) 53 (-13) 51 (-11) 2 (-2) 👍
github.com/drn/argus/internal/hera/model/model.go 94.43% (+94.43%) 395 (+395) 373 (+373) 22 (+22) 🌟
github.com/drn/argus/internal/hera/model/reader.go 0.00% (ø) 0 0 0
github.com/drn/argus/internal/tui/hera/adopt.go 76.98% (ø) 126 97 29
github.com/drn/argus/internal/tui/hera/details.go 99.64% (ø) 275 274 1
github.com/drn/argus/internal/tui/hera/eol.go 100.00% (ø) 7 7 0
github.com/drn/argus/internal/tui/hera/model_types.go 0.00% (ø) 0 0 0
github.com/drn/argus/internal/tui/hera/ops.go 77.68% (ø) 112 87 25
github.com/drn/argus/internal/tui/hera/page.go 90.33% (ø) 393 355 38
github.com/drn/argus/internal/tui/hera/panes.go 94.29% (ø) 140 132 8
github.com/drn/argus/internal/tui/hera/plan.go 94.44% (ø) 54 51 3
github.com/drn/argus/internal/tui/hera/rail.go 92.94% (ø) 907 843 64
github.com/drn/argus/internal/tui/heraactions.go 76.17% (ø) 579 441 138

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

  • github.com/drn/argus/internal/api/hera_test.go
  • github.com/drn/argus/internal/hera/model/model_test.go
  • github.com/drn/argus/internal/tui/hera/attentionsummary_test.go
  • github.com/drn/argus/internal/tui/hera/bug024_test.go
  • github.com/drn/argus/internal/tui/hera/bug028_repro_test.go
  • github.com/drn/argus/internal/tui/hera/changegate_test.go
  • github.com/drn/argus/internal/tui/hera/dag_test.go
  • github.com/drn/argus/internal/tui/hera/details_railmut_test.go
  • github.com/drn/argus/internal/tui/hera/details_test.go
  • github.com/drn/argus/internal/tui/hera/eol_test.go
  • github.com/drn/argus/internal/tui/hera/excursion_test.go
  • github.com/drn/argus/internal/tui/hera/filter_test.go
  • github.com/drn/argus/internal/tui/hera/handlers_test.go
  • github.com/drn/argus/internal/tui/hera/jumpneedsinput_test.go
  • github.com/drn/argus/internal/tui/hera/keyset_test.go
  • github.com/drn/argus/internal/tui/hera/model_sustainedactive_test.go
  • github.com/drn/argus/internal/tui/hera/model_test.go
  • github.com/drn/argus/internal/tui/hera/ops_test.go
  • github.com/drn/argus/internal/tui/hera/page_test.go
  • github.com/drn/argus/internal/tui/hera/panes_test.go
  • github.com/drn/argus/internal/tui/hera/pin_nonroot_test.go
  • github.com/drn/argus/internal/tui/hera/plan_test.go
  • github.com/drn/argus/internal/tui/hera/rail_kanban_focus_test.go
  • github.com/drn/argus/internal/tui/hera/rail_reveal_test.go
  • github.com/drn/argus/internal/tui/hera/rail_sticky_reveal_test.go
  • github.com/drn/argus/internal/tui/hera/rail_test.go
  • github.com/drn/argus/internal/tui/hera/selection_test.go

@anutron
anutron merged commit 2b1d325 into master Aug 23, 2026
1 check passed
@anutron
anutron deleted the argus/mac-hera-rail-toggle branch August 23, 2026 03:53
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