Skip to content

refactor(ui) 3/5: retire "harness" as a noun for anything you interact with - #183

Merged
senamakel merged 2 commits into
split/2-tree-uifrom
split/3-vocabulary
Aug 5, 2026
Merged

refactor(ui) 3/5: retire "harness" as a noun for anything you interact with#183
senamakel merged 2 commits into
split/2-tree-uifrom
split/3-vocabulary

Conversation

@sanil-23

@sanil-23 sanil-23 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Stack 3/5 — base split/2-tree-ui. Depends on: 1/5, 2/5.

# PR Branch Base Layer
1 #181 split/1-agent-model main session origin/name + agent declarations (model only)
2 #182 split/2-tree-ui split/1-agent-model Hosts and Agents tabs become one Host → Agent → Session tree
3 #183 split/3-vocabulary split/2-tree-ui rename: "harness" is a type, not an entity
4 #184 split/4-wire split/3-vocabulary hub advert: hosts[], per-agent hostId/maxSessions, result sessionId
5 #185 split/5-control split/4-wire dispatch candidacy, hold/hand-back, host-wide cap removed

Each PR's diff shows only its own layer. split/5-control's tree is byte-identical to
bcc61e63, the merge commit on #180.


Third of five stacked PRs replacing #180. Review 1/5 and 2/5 first.

What this layer does

One commit, and it is almost entirely mechanical: retire "harness" as a noun for
anything you interact with.

A harness is a type — the value on an agent that says which coding CLI its sessions run
— not an entity. Everything the operator touches is an agent or one of its sessions, and
the old vocabulary was the route by which the host ≡ worker ≡ agent collapse kept
leaking back in.

Types and fields: HarnessControlSessionControl, LocalHarnesses
LocalSessions, HarnessPickerAgentPicker, App.harnesseslocal_sessions,
harness_pane_sessionpane_session, selected_harness_sessionrail_session,
own_harness_linesown_session_lines, harnesses_waitingsessions_waiting,
Cmd::{HoldHarness, HandOffHarness}{HoldSession, HandOffSession},
BaselineSource::HarnessLaunchSessionLaunch.

Files follow their contents: ui/app/harness_control.rssession_control.rs,
render/harness_modals.rssession_modals.rs, plus four integration-test files.

Copy: every status line, prompt, modal, pane title, help line and toast. /harness
becomes /session (old spelling kept as an alias, with a test pinning it), and Routing's
Harnesses page becomes Harness Types.

What a reviewer should look at closely

This is 106 files of rename, so the interesting part is what is not a rename:

  • /harness keeps working as an alias — there is a test pinning it. Losing that
    would be a silent break for anyone with muscle memory.
  • The rail title now counts agents on the tree rather than the lanes traffic happened
    to fold, so a quiet machine with three declared agents no longer reads Agents · 0.
  • Names that stayed: anything describing the typeHarnessProvider,
    harness_hooks, Harness Types — is intentionally untouched. Flag any spot where the
    rename went too far and renamed a type-level concept.
  • Merge reconciliation. main shipped + New harness+ New session and a
    ── your sessions ── divider (feat(tui): default to primary red and rename harness rail labels to sessions #179) while this branch was open. This stack keeps its
    own vocabulary instead: + New agent at machine level and a per-agent + new session,
    and no divider at all. The ⏎ / ^T hint from feat(tui): default to primary red and rename harness rail labels to sessions #179's button is preserved on the
    per-agent row.

Validation

cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo check --locked --all-targets
cargo build --locked --all-targets
cargo test --locked --no-fail-fast

Result on this branch: fmt, clippy -D warnings, check, and build all clean.
test reports 3879 passed, 5 failed, 14 ignored on macOS. All five failures are
pre-existing on main and unrelated to this stack:

  • daemon::providers::acp::tests::execution::a_new_acp_session_is_reported_before_the_task_completes
  • daemon::providers::tests::direct_runs_report_the_session_before_workspace_context
    (both are idle-watchdog timing tests; they pass in isolation)
  • worker::pty::tests::session::a_launch_root_preserves_trailing_whitespace
  • worker::pty::tests::session::a_session_snapshots_head_before_the_harness_can_commit
  • worker::pty::tests::session::an_unborn_repository_records_its_root_without_a_launch_commit
    (the last three are the macOS /var vs /private/var symlink)

Coverage was deliberately not run locally; CI owns that gate.

Summary by CodeRabbit

  • New Features

    • Added /session as the primary command for starting sessions; /harness remains available as an alias.
    • Renamed the Routing subpage to Harness Types for clearer agent setup.
    • Updated session controls, handoff, takeover, workspace selection, and related UI wording.
  • Documentation

    • Standardized terminology around agents, sessions, harness types, and runtime providers across guides and help content.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Too many files changed for review (107 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request replaces harness-oriented terminology with agent, session, harness-type, and SessionControl terminology across documentation, SDK commands, TUI state, PTY handling, rendering, and tests. Existing session and control behavior remains unchanged.

Changes

Agent and session terminology

Layer / File(s) Summary
Documentation and SDK commands
README.md, docs/*, src/sdk/src/ui/command/*
Documentation defines agents, harness types, and sessions. The /session command replaces /harness, while /harness remains an alias.
Session contracts and application wiring
src/tui/src/worker/pty/*, src/tui/src/ui/harness_pane/*, src/tui/src/ui/app/state.rs, src/tui/src/ui/app/types.rs, src/tui/src/event_loop/*
HarnessControl becomes SessionControl, LocalHarnesses becomes LocalSessions, and application wiring uses session-oriented fields and commands.
TUI rendering and interaction
src/tui/src/ui/app/session_control.rs, src/tui/src/ui/app/input/*, src/tui/src/ui/app/render/*, src/tui/src/ui/app/changes/*
Picker, attachment, handback, baseline, rail, overlay, and rendering APIs use session and agent-picker terminology.
Validation and compatibility coverage
src/tui/tests/*, src/tui/src/**/tests/*
Tests update control types, picker labels, routing labels, session state, handoff commands, and E2E fixtures.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

Suggested reviewers: senamakel

Poem

A rabbit hops through sessions bright,
Renaming harnesses left and right.
Agents pick their types with care,
PTYs hand control through air.
/session now leads the way—
Old aliases still obey.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main UI terminology refactor that removes “harness” as an interactable entity.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@sanil-23

sanil-23 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/tui/src/ui/app/types.rs (1)

372-378: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace the stale failure status text.

When Cmd::HoldSession fails, src/tui/src/event_loop/cmd_dispatch/handoff.rs still emits "You have this harness · the orchestrator was not told: {e}". Change harness to session in that status message. A held session is an interactable entity.

Based on PR objective: “harness” is used only for a harness type, not an interactable entity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/src/ui/app/types.rs` around lines 372 - 378, Update the failure
status message in the Cmd::HoldSession dispatch path in handoff.rs, replacing
“harness” with “session” while preserving the existing error details and
surrounding wording.
src/tui/src/event_loop/cmd_dispatch/handoff.rs (1)

55-65: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use session terminology in the hold failure status.

Cmd::HoldSession now represents a session, but Line 64 still reports You have this harness. This text is visible in the status line and conflicts with the terminology migration. Change the message to use session.

Keep Runtime::hold_harness unchanged because the PR retains wire-level harness terminology.

This follows the PR objective that harness is reserved for a harness type.

Proposed fix
-                        "You have this harness · the orchestrator was not told: {e}"
+                        "You have this session · the orchestrator was not told: {e}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/src/event_loop/cmd_dispatch/handoff.rs` around lines 55 - 65, Update
the failure status message in the Cmd::HoldSession branch to say “session”
instead of “harness.” Leave Runtime::hold_harness unchanged, preserving harness
terminology at the wire-level API.
🧹 Nitpick comments (3)
src/tui/src/ui/app/rail/tests.rs (1)

29-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the test helper and bindings to match LocalSessions.

LocalSessions represents interactable sessions, but Line 35 names the helper shell_harnesses, and Lines 154 and 184 bind the value as harnesses. Rename these identifiers to shell_sessions and sessions. Update the helper callers.

This follows the PR objective that harness is reserved for a harness type.

Proposed refactor
-    app.set_local_sessions(shell_harnesses(PtyManager::new()));
+    app.set_local_sessions(shell_sessions(PtyManager::new()));

- pub(in crate::ui::app) fn shell_harnesses(
+ pub(in crate::ui::app) fn shell_sessions(

-    let harnesses = app.local_sessions().expect("hosting").clone();
+    let sessions = app.local_sessions().expect("hosting").clone();

-    let harnesses = app.local_sessions().expect("hosting").clone();
+    let sessions = app.local_sessions().expect("hosting").clone();

Also applies to: 154-161, 184-191

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/src/ui/app/rail/tests.rs` around lines 29 - 44, Rename the
LocalSessions helper shell_harnesses to shell_sessions, update all callers
including app setup, and rename the bindings currently called harnesses at the
referenced test locations to sessions. Keep the existing LocalSessions behavior
unchanged.
src/tui/src/ui/harness_pane/mod.rs (1)

50-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move behavior out of directory-module roots.

Keep each mod.rs limited to documentation and module wiring.

  • src/tui/src/ui/harness_pane/mod.rs#L50-L50: Move the LocalSessions implementation into a dedicated child module and re-export it from mod.rs.
  • src/tui/src/ui/app/render/agents/rail/mod.rs#L55-L76: Move rail-title behavior into a child module.
  • src/tui/src/ui/app/render/agents/rail/mod.rs#L140-L148: Move rail rendering behavior into a child module.
  • src/tui/src/ui/app/render/agents/rail/mod.rs#L314-L314: Keep the call site in the extracted rendering module.
  • src/tui/src/ui/app/render/agents/rail/mod.rs#L362-L362: Keep the fallback rendering path in the extracted rendering module.

As per coding guidelines, "**/mod.rs: Keep mod.rs focused on module documentation, mod/pub use wiring, and glue that fits no more specific submodule."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/src/ui/harness_pane/mod.rs` at line 50, Move the concrete behavior
out of each directory root so the affected mod.rs files stay limited to wiring:
extract the LocalSessions implementation from
src/tui/src/ui/harness_pane/mod.rs#L50-L50 into a child module and re-export it,
and do the same for the rail-title and rail-rendering logic in
src/tui/src/ui/app/render/agents/rail/mod.rs#L55-L76 and `#L140-L148` while
keeping the call site and fallback rendering path in the extracted rendering
module at `#L314-L314` and `#L362-L362`.

Source: Coding guidelines

src/tui/src/ui/app/render/mod.rs (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Move rendering implementations out of mod.rs files.

The module files contain behavior-heavy impl App code. Keep each mod.rs limited to documentation and module wiring.

  • src/tui/src/ui/app/render/mod.rs#L31-L31: Move rendering methods into focused submodules and retain only module declarations and re-exports here.
  • src/tui/src/ui/app/render/routing/harnesses/mod.rs#L170-L170: Move draw_harnesses into a focused child module and retain only module wiring here.

As per coding guidelines: **/mod.rs: “Keep mod.rs focused on module documentation, mod/pub use wiring, and glue that fits no more specific submodule.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/src/ui/app/render/mod.rs` at line 31, Move the behavior-heavy App
rendering implementations from src/tui/src/ui/app/render/mod.rs into focused
child modules, leaving that file only with documentation, module declarations,
and re-exports. Also move draw_harnesses from
src/tui/src/ui/app/render/routing/harnesses/mod.rs into a focused child module,
retaining only module wiring there.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/TERMINOLOGY.md`:
- Line 57: Update the fenced containment diagram in TERMINOLOGY.md to specify
the text language on its opening fence, using the existing diagram content
unchanged so markdownlint MD040 passes.

In `@src/tui/src/ui/app/keys/harness.rs`:
- Around line 137-147: Update the attachment handling around the `took`
calculation to assign `self.took_control_by_attach` from `took` on every
successful attachment, rather than only setting it when `took` is true. Preserve
the existing control transfer in `set_control` and ensure subsequent PTY failure
handling cannot reuse a stale flag from an earlier attachment.

In `@src/tui/src/worker/pty/types.rs`:
- Around line 75-77: Update the SessionControl documentation to describe the
current holder rather than the session’s origin: remove the claim that unmanaged
sessions must have been born User-held, and distinguish current control from
SessionOrigin while preserving the takeover semantics.

---

Outside diff comments:
In `@src/tui/src/event_loop/cmd_dispatch/handoff.rs`:
- Around line 55-65: Update the failure status message in the Cmd::HoldSession
branch to say “session” instead of “harness.” Leave Runtime::hold_harness
unchanged, preserving harness terminology at the wire-level API.

In `@src/tui/src/ui/app/types.rs`:
- Around line 372-378: Update the failure status message in the Cmd::HoldSession
dispatch path in handoff.rs, replacing “harness” with “session” while preserving
the existing error details and surrounding wording.

---

Nitpick comments:
In `@src/tui/src/ui/app/rail/tests.rs`:
- Around line 29-44: Rename the LocalSessions helper shell_harnesses to
shell_sessions, update all callers including app setup, and rename the bindings
currently called harnesses at the referenced test locations to sessions. Keep
the existing LocalSessions behavior unchanged.

In `@src/tui/src/ui/app/render/mod.rs`:
- Line 31: Move the behavior-heavy App rendering implementations from
src/tui/src/ui/app/render/mod.rs into focused child modules, leaving that file
only with documentation, module declarations, and re-exports. Also move
draw_harnesses from src/tui/src/ui/app/render/routing/harnesses/mod.rs into a
focused child module, retaining only module wiring there.

In `@src/tui/src/ui/harness_pane/mod.rs`:
- Line 50: Move the concrete behavior out of each directory root so the affected
mod.rs files stay limited to wiring: extract the LocalSessions implementation
from src/tui/src/ui/harness_pane/mod.rs#L50-L50 into a child module and
re-export it, and do the same for the rail-title and rail-rendering logic in
src/tui/src/ui/app/render/agents/rail/mod.rs#L55-L76 and `#L140-L148` while
keeping the call site and fallback rendering path in the extracted rendering
module at `#L314-L314` and `#L362-L362`.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0430d3c1-de07-48f1-83ff-43a2ae8f5526

📥 Commits

Reviewing files that changed from the base of the PR and between ccbd1b1 and 49fb3ac.

📒 Files selected for processing (106)
  • README.md
  • docs/TERMINOLOGY.md
  • docs/workflows.md
  • src/sdk/src/hub/handoff/types.rs
  • src/sdk/src/sessions/types.rs
  • src/sdk/src/ui/README.md
  • src/sdk/src/ui/command/catalog.rs
  • src/sdk/src/ui/command/mod.rs
  • src/sdk/src/ui/command/tests.rs
  • src/sdk/src/ui/command/types.rs
  • src/sdk/src/ui/decisions/README.md
  • src/sdk/src/ui/decisions/fold.rs
  • src/sdk/src/ui/decisions/mod.rs
  • src/tui/examples/pty_load.rs
  • src/tui/src/app_loop.rs
  • src/tui/src/event_loop/cmd_dispatch/handoff.rs
  • src/tui/src/event_loop/cmd_dispatch/mod.rs
  • src/tui/src/event_loop/mod.rs
  • src/tui/src/event_loop/types.rs
  • src/tui/src/ui/app/agent_control.rs
  • src/tui/src/ui/app/agent_control_tests.rs
  • src/tui/src/ui/app/changes/baseline.rs
  • src/tui/src/ui/app/changes/baseline_tests.rs
  • src/tui/src/ui/app/changes/mod.rs
  • src/tui/src/ui/app/changes/tests.rs
  • src/tui/src/ui/app/changes/types.rs
  • src/tui/src/ui/app/commands/changes.rs
  • src/tui/src/ui/app/commands/dispatch.rs
  • src/tui/src/ui/app/harness_workspace.rs
  • src/tui/src/ui/app/harness_workspace_tests.rs
  • src/tui/src/ui/app/hosts/edit.rs
  • src/tui/src/ui/app/input/mouse.rs
  • src/tui/src/ui/app/input/paste.rs
  • src/tui/src/ui/app/keys/agents.rs
  • src/tui/src/ui/app/keys/harness.rs
  • src/tui/src/ui/app/keys/mod.rs
  • src/tui/src/ui/app/keys/routing/add_host.rs
  • src/tui/src/ui/app/keys/routing/mod.rs
  • src/tui/src/ui/app/mod.rs
  • src/tui/src/ui/app/overlays.rs
  • src/tui/src/ui/app/overlays_tests.rs
  • src/tui/src/ui/app/rail/mod.rs
  • src/tui/src/ui/app/rail/resolve.rs
  • src/tui/src/ui/app/rail/tests.rs
  • src/tui/src/ui/app/render/agents/composer.rs
  • src/tui/src/ui/app/render/agents/harness.rs
  • src/tui/src/ui/app/render/agents/mod.rs
  • src/tui/src/ui/app/render/agents/rail/attention_tests.rs
  • src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs
  • src/tui/src/ui/app/render/agents/rail/mod.rs
  • src/tui/src/ui/app/render/agents/rail/rows.rs
  • src/tui/src/ui/app/render/agents/rail/state.rs
  • src/tui/src/ui/app/render/agents/rail/status_line_tests.rs
  • src/tui/src/ui/app/render/agents/rail/tests.rs
  • src/tui/src/ui/app/render/agents/transcript.rs
  • src/tui/src/ui/app/render/agents/transcript_tests.rs
  • src/tui/src/ui/app/render/agents/types.rs
  • src/tui/src/ui/app/render/changes.rs
  • src/tui/src/ui/app/render/mod.rs
  • src/tui/src/ui/app/render/routing/add_host.rs
  • src/tui/src/ui/app/render/routing/harnesses/mod.rs
  • src/tui/src/ui/app/render/session_modals.rs
  • src/tui/src/ui/app/render/settings/appearance_usage.rs
  • src/tui/src/ui/app/render/settings/help.rs
  • src/tui/src/ui/app/render/settings/status_line.rs
  • src/tui/src/ui/app/render/tests.rs
  • src/tui/src/ui/app/render/workflows/node_preview/kinds.rs
  • src/tui/src/ui/app/session_control.rs
  • src/tui/src/ui/app/session_control_tests.rs
  • src/tui/src/ui/app/settings_edit/mod.rs
  • src/tui/src/ui/app/state.rs
  • src/tui/src/ui/app/tests.rs
  • src/tui/src/ui/app/types.rs
  • src/tui/src/ui/harness_pane/mod.rs
  • src/tui/src/ui/harness_pane/spawn.rs
  • src/tui/src/ui/harness_pane/tests/mod.rs
  • src/tui/src/ui/harness_pane/tests/origin.rs
  • src/tui/src/ui/harness_pane/tests/session.rs
  • src/tui/src/ui/harness_pane/types.rs
  • src/tui/src/worker/app/tests/helpers/mod.rs
  • src/tui/src/worker/executor/run.rs
  • src/tui/src/worker/executor/tests.rs
  • src/tui/src/worker/executor_tests/basic.rs
  • src/tui/src/worker/executor_tests/live.rs
  • src/tui/src/worker/executor_tests/sessions.rs
  • src/tui/src/worker/pty/handle/control.rs
  • src/tui/src/worker/pty/handle/lifecycle.rs
  • src/tui/src/worker/pty/handle/types.rs
  • src/tui/src/worker/pty/manager/session.rs
  • src/tui/src/worker/pty/mod.rs
  • src/tui/src/worker/pty/tests/attention.rs
  • src/tui/src/worker/pty/tests/control.rs
  • src/tui/src/worker/pty/tests/mod.rs
  • src/tui/src/worker/pty/types.rs
  • src/tui/tests/e2e_local_session_pane.rs
  • src/tui/tests/e2e_screen_stream.rs
  • src/tui/tests/e2e_session_takeover.rs
  • src/tui/tests/feature_commands.rs
  • src/tui/tests/feature_demo_fleet.rs
  • src/tui/tests/feature_paste/attached.rs
  • src/tui/tests/feature_paste/picker.rs
  • src/tui/tests/feature_session_control.rs
  • src/tui/tests/feature_session_handoff.rs
  • src/tui/tests/feature_workers/fleet.rs
  • src/tui/tests/feature_workers/list.rs
  • src/tui/tests/feature_workers/routing.rs

Comment thread docs/TERMINOLOGY.md Outdated
Comment thread src/tui/src/ui/app/keys/harness.rs
Comment thread src/tui/src/worker/pty/types.rs Outdated
@sanil-23
sanil-23 force-pushed the split/3-vocabulary branch from 49fb3ac to 9b63ade Compare August 5, 2026 07:48
sanil-23 and others added 2 commits August 5, 2026 14:04
Plan §A6/F1: a harness is a *type* — the value on an agent that says which
coding CLI its sessions run — not an entity. Everything the operator
touches is an agent or one of its sessions, and the old vocabulary was the
route by which the host ≡ worker ≡ agent collapse kept leaking back in.

Types and fields: `HarnessControl` → `SessionControl` (control is
per-session), `LocalHarnesses` → `LocalSessions`, `HarnessPicker` →
`AgentPicker`, `App.harnesses` → `local_sessions`, `harness_pane_session`
→ `pane_session`, `selected_harness_session` → `rail_session`,
`own_harness_lines` → `own_session_lines`, `harnesses_waiting` →
`sessions_waiting`, `Cmd::{HoldHarness,HandOffHarness}` →
`{HoldSession,HandOffSession}`, `BaselineSource::HarnessLaunch` →
`SessionLaunch`, and the take/hand-back/toggle/release entry points with
them. `ui/app/harness_control.rs` and `render/harness_modals.rs` follow
their contents to `session_control.rs` / `session_modals.rs`, as do four
integration-test files.

Copy: every status line, prompt, modal, pane title, help line and toast
that called a session a harness now calls it a session — including the
hand-back and take-control questions, the kill confirmation, and the key
hint bar. The `/harness` command is now `/session` (old spelling kept as
an alias, with a test pinning it), and Routing's `Harnesses` page is
`Harness Types`. The rail title counts the agents on the tree instead of
the lanes traffic happened to fold, so a quiet machine with three declared
agents no longer reads "Agents · 0".

Kept as "harness" where it genuinely means the type or the CLI process:
`HarnessProvider`/`HarnessChoice`, the `harness` field on a declaration,
`[harness]` config, custom presets, `harness_pty` and the pane internals
that talk to the child, and every wire name (`HarnessControlFrame`,
`HarnessHandoff`, `harness_select`) — hub and protocol are untouched
beyond two stale doc cross-references.

Docs: TERMINOLOGY.md now defines Agent as a declaration, Harness as a type
and not a level, Host → Agent → Session as the chain, and Session as the
runtime instance with origin and owner; README and workflows.md follow.

Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-ups on the vocabulary pass (#183).

`took_control_by_attach` was only ever set to `true`, and nothing clears
it on release — so a `true` left by an attachment that really did take a
session survived into the next one. If that next session was already the
operator's, a pty write failure read the stale flag and handed it to the
orchestrator, taking away a session they had been holding before they
focused in. It is now assigned from `took` on every attachment.

`SessionControl`'s doc defined "unmanaged" as a session *born* user-held.
It is not a kind of session: `claim_idle` reads the current holder, so a
dispatched session the operator takes is unmanaged for as long as they
keep it. The doc now describes current control and leaves origin to
`SessionOrigin`.

And the containment diagram in `TERMINOLOGY.md` gets a language on its
fence, for MD040.

Co-Authored-By: Claude <noreply@anthropic.com>
@sanil-23
sanil-23 force-pushed the split/3-vocabulary branch from 9b63ade to a020299 Compare August 5, 2026 08:49
@sanil-23

sanil-23 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai resolve

@sanil-23

sanil-23 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/tui/tests/feature_session_control.rs (1)

479-529: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move attachment and release tests into a behavior submodule.

The file src/tui/tests/feature_session_control.rs has 508 non-comment source lines, which exceeds the 500-line limit defined in the coding guidelines. Move the attachment and release tests (including a_second_attachment_does_not_inherit_the_first_ones_takeover) into src/tui/tests/feature_session_control/mod.rs to comply with the source-line constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/tests/feature_session_control.rs` around lines 479 - 529, Move the
attachment and release tests, including
a_second_attachment_does_not_inherit_the_first_ones_takeover, from
feature_session_control.rs into the feature_session_control behavior submodule
at mod.rs. Preserve their existing setup, assertions, imports, and Unix-only
gating, while reducing the parent file below the 500-line source limit.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/tui/tests/feature_session_control.rs`:
- Around line 479-529: Move the attachment and release tests, including
a_second_attachment_does_not_inherit_the_first_ones_takeover, from
feature_session_control.rs into the feature_session_control behavior submodule
at mod.rs. Preserve their existing setup, assertions, imports, and Unix-only
gating, while reducing the parent file below the 500-line source limit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a625cdf2-9d41-4cf4-9aa6-ac8da5136cee

📥 Commits

Reviewing files that changed from the base of the PR and between f5a0ed6 and a020299.

📒 Files selected for processing (107)
  • README.md
  • docs/TERMINOLOGY.md
  • docs/workflows.md
  • src/sdk/src/hub/handoff/types.rs
  • src/sdk/src/sessions/types.rs
  • src/sdk/src/ui/README.md
  • src/sdk/src/ui/command/catalog.rs
  • src/sdk/src/ui/command/mod.rs
  • src/sdk/src/ui/command/tests.rs
  • src/sdk/src/ui/command/types.rs
  • src/sdk/src/ui/decisions/README.md
  • src/sdk/src/ui/decisions/fold.rs
  • src/sdk/src/ui/decisions/mod.rs
  • src/tui/examples/pty_load.rs
  • src/tui/src/app_loop.rs
  • src/tui/src/event_loop/cmd_dispatch/handoff.rs
  • src/tui/src/event_loop/cmd_dispatch/mod.rs
  • src/tui/src/event_loop/mod.rs
  • src/tui/src/event_loop/types.rs
  • src/tui/src/ui/app/agent_control.rs
  • src/tui/src/ui/app/agent_control_tests.rs
  • src/tui/src/ui/app/changes/baseline.rs
  • src/tui/src/ui/app/changes/baseline_tests.rs
  • src/tui/src/ui/app/changes/mod.rs
  • src/tui/src/ui/app/changes/tests.rs
  • src/tui/src/ui/app/changes/types.rs
  • src/tui/src/ui/app/commands/changes.rs
  • src/tui/src/ui/app/commands/dispatch.rs
  • src/tui/src/ui/app/harness_workspace.rs
  • src/tui/src/ui/app/harness_workspace_tests.rs
  • src/tui/src/ui/app/hosts/edit.rs
  • src/tui/src/ui/app/input/mouse.rs
  • src/tui/src/ui/app/input/paste.rs
  • src/tui/src/ui/app/input/tests.rs
  • src/tui/src/ui/app/keys/agents.rs
  • src/tui/src/ui/app/keys/harness.rs
  • src/tui/src/ui/app/keys/mod.rs
  • src/tui/src/ui/app/keys/routing/add_host.rs
  • src/tui/src/ui/app/keys/routing/mod.rs
  • src/tui/src/ui/app/mod.rs
  • src/tui/src/ui/app/overlays.rs
  • src/tui/src/ui/app/overlays_tests.rs
  • src/tui/src/ui/app/rail/mod.rs
  • src/tui/src/ui/app/rail/resolve.rs
  • src/tui/src/ui/app/rail/tests.rs
  • src/tui/src/ui/app/render/agents/composer.rs
  • src/tui/src/ui/app/render/agents/harness.rs
  • src/tui/src/ui/app/render/agents/mod.rs
  • src/tui/src/ui/app/render/agents/rail/attention_tests.rs
  • src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs
  • src/tui/src/ui/app/render/agents/rail/mod.rs
  • src/tui/src/ui/app/render/agents/rail/rows.rs
  • src/tui/src/ui/app/render/agents/rail/state.rs
  • src/tui/src/ui/app/render/agents/rail/status_line_tests.rs
  • src/tui/src/ui/app/render/agents/rail/tests.rs
  • src/tui/src/ui/app/render/agents/transcript.rs
  • src/tui/src/ui/app/render/agents/transcript_tests.rs
  • src/tui/src/ui/app/render/agents/types.rs
  • src/tui/src/ui/app/render/changes.rs
  • src/tui/src/ui/app/render/mod.rs
  • src/tui/src/ui/app/render/routing/add_host.rs
  • src/tui/src/ui/app/render/routing/harnesses/mod.rs
  • src/tui/src/ui/app/render/session_modals.rs
  • src/tui/src/ui/app/render/settings/appearance_usage.rs
  • src/tui/src/ui/app/render/settings/help.rs
  • src/tui/src/ui/app/render/settings/status_line.rs
  • src/tui/src/ui/app/render/tests.rs
  • src/tui/src/ui/app/render/workflows/node_preview/kinds.rs
  • src/tui/src/ui/app/session_control.rs
  • src/tui/src/ui/app/session_control_tests.rs
  • src/tui/src/ui/app/settings_edit/mod.rs
  • src/tui/src/ui/app/state.rs
  • src/tui/src/ui/app/tests.rs
  • src/tui/src/ui/app/types.rs
  • src/tui/src/ui/harness_pane/mod.rs
  • src/tui/src/ui/harness_pane/spawn.rs
  • src/tui/src/ui/harness_pane/tests/mod.rs
  • src/tui/src/ui/harness_pane/tests/origin.rs
  • src/tui/src/ui/harness_pane/tests/session.rs
  • src/tui/src/ui/harness_pane/types.rs
  • src/tui/src/worker/app/tests/helpers/mod.rs
  • src/tui/src/worker/executor/run.rs
  • src/tui/src/worker/executor/tests.rs
  • src/tui/src/worker/executor_tests/basic.rs
  • src/tui/src/worker/executor_tests/live.rs
  • src/tui/src/worker/executor_tests/sessions.rs
  • src/tui/src/worker/pty/handle/control.rs
  • src/tui/src/worker/pty/handle/lifecycle.rs
  • src/tui/src/worker/pty/handle/types.rs
  • src/tui/src/worker/pty/manager/session.rs
  • src/tui/src/worker/pty/mod.rs
  • src/tui/src/worker/pty/tests/attention.rs
  • src/tui/src/worker/pty/tests/control.rs
  • src/tui/src/worker/pty/tests/mod.rs
  • src/tui/src/worker/pty/types.rs
  • src/tui/tests/e2e_local_session_pane.rs
  • src/tui/tests/e2e_screen_stream.rs
  • src/tui/tests/e2e_session_takeover.rs
  • src/tui/tests/feature_commands.rs
  • src/tui/tests/feature_demo_fleet.rs
  • src/tui/tests/feature_paste/attached.rs
  • src/tui/tests/feature_paste/picker.rs
  • src/tui/tests/feature_session_control.rs
  • src/tui/tests/feature_session_handoff.rs
  • src/tui/tests/feature_workers/fleet.rs
  • src/tui/tests/feature_workers/list.rs
  • src/tui/tests/feature_workers/routing.rs
🚧 Files skipped from review as they are similar to previous changes (104)
  • src/sdk/src/ui/decisions/fold.rs
  • src/tui/examples/pty_load.rs
  • src/tui/src/ui/app/settings_edit/mod.rs
  • src/tui/src/ui/app/changes/tests.rs
  • src/tui/src/ui/app/render/agents/types.rs
  • src/tui/src/event_loop/cmd_dispatch/mod.rs
  • src/sdk/src/ui/command/catalog.rs
  • src/tui/src/ui/app/session_control_tests.rs
  • src/tui/tests/feature_workers/list.rs
  • src/tui/src/ui/app/commands/changes.rs
  • src/sdk/src/hub/handoff/types.rs
  • src/tui/src/app_loop.rs
  • src/sdk/src/ui/decisions/README.md
  • src/tui/src/ui/app/input/paste.rs
  • src/tui/src/event_loop/mod.rs
  • src/tui/src/event_loop/cmd_dispatch/handoff.rs
  • src/tui/src/ui/app/render/agents/composer.rs
  • README.md
  • src/tui/src/ui/app/keys/routing/add_host.rs
  • src/tui/src/ui/app/rail/resolve.rs
  • src/tui/src/worker/pty/handle/types.rs
  • src/tui/src/worker/executor/tests.rs
  • src/tui/src/ui/app/render/routing/add_host.rs
  • src/sdk/src/ui/decisions/mod.rs
  • src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs
  • src/tui/src/worker/pty/mod.rs
  • src/tui/src/ui/app/mod.rs
  • src/tui/src/worker/pty/handle/lifecycle.rs
  • src/sdk/src/ui/README.md
  • src/tui/src/ui/app/render/workflows/node_preview/kinds.rs
  • src/tui/src/worker/executor_tests/sessions.rs
  • src/tui/src/ui/app/keys/routing/mod.rs
  • src/tui/src/ui/app/render/agents/rail/rows.rs
  • src/tui/src/ui/app/changes/mod.rs
  • src/tui/src/ui/app/render/agents/transcript.rs
  • src/sdk/src/ui/command/mod.rs
  • src/tui/src/ui/app/render/agents/transcript_tests.rs
  • src/sdk/src/ui/command/tests.rs
  • src/tui/src/ui/app/render/agents/mod.rs
  • src/tui/src/ui/harness_pane/tests/mod.rs
  • src/tui/src/ui/app/changes/baseline.rs
  • docs/workflows.md
  • src/sdk/src/sessions/types.rs
  • src/tui/src/worker/pty/tests/mod.rs
  • src/tui/src/ui/harness_pane/mod.rs
  • src/tui/src/worker/executor/run.rs
  • src/tui/tests/feature_workers/routing.rs
  • src/tui/src/ui/app/hosts/edit.rs
  • src/tui/src/ui/app/render/routing/harnesses/mod.rs
  • src/tui/tests/e2e_screen_stream.rs
  • src/tui/src/ui/app/render/changes.rs
  • src/tui/src/ui/app/rail/tests.rs
  • src/tui/src/ui/app/agent_control.rs
  • src/tui/src/ui/app/input/mouse.rs
  • src/tui/tests/e2e_session_takeover.rs
  • src/tui/src/ui/app/harness_workspace.rs
  • src/tui/src/ui/harness_pane/tests/origin.rs
  • src/tui/src/ui/app/render/settings/appearance_usage.rs
  • src/tui/src/ui/app/render/agents/rail/attention_tests.rs
  • src/tui/src/ui/app/changes/baseline_tests.rs
  • src/tui/src/worker/executor_tests/basic.rs
  • src/tui/src/ui/app/render/tests.rs
  • src/tui/src/ui/app/render/agents/rail/state.rs
  • src/tui/src/worker/app/tests/helpers/mod.rs
  • src/tui/src/ui/app/keys/mod.rs
  • src/tui/src/ui/app/overlays.rs
  • src/tui/src/ui/app/render/agents/rail/tests.rs
  • src/tui/src/ui/app/commands/dispatch.rs
  • src/tui/src/worker/pty/tests/control.rs
  • src/tui/src/ui/app/render/agents/harness.rs
  • src/tui/src/ui/app/render/settings/status_line.rs
  • docs/TERMINOLOGY.md
  • src/tui/src/ui/app/tests.rs
  • src/tui/src/event_loop/types.rs
  • src/tui/tests/feature_workers/fleet.rs
  • src/tui/tests/feature_paste/picker.rs
  • src/tui/tests/e2e_local_session_pane.rs
  • src/tui/src/worker/pty/tests/attention.rs
  • src/sdk/src/ui/command/types.rs
  • src/tui/src/ui/app/render/mod.rs
  • src/tui/src/worker/pty/handle/control.rs
  • src/tui/src/ui/harness_pane/tests/session.rs
  • src/tui/src/ui/app/render/agents/rail/mod.rs
  • src/tui/src/ui/app/rail/mod.rs
  • src/tui/src/worker/pty/types.rs
  • src/tui/src/worker/executor_tests/live.rs
  • src/tui/src/ui/app/overlays_tests.rs
  • src/tui/src/ui/harness_pane/types.rs
  • src/tui/tests/feature_demo_fleet.rs
  • src/tui/src/ui/app/render/session_modals.rs
  • src/tui/src/ui/app/render/agents/rail/status_line_tests.rs
  • src/tui/src/ui/harness_pane/spawn.rs
  • src/tui/src/ui/app/keys/agents.rs
  • src/tui/tests/feature_paste/attached.rs
  • src/tui/src/ui/app/agent_control_tests.rs
  • src/tui/tests/feature_session_handoff.rs
  • src/tui/src/worker/pty/manager/session.rs
  • src/tui/src/ui/app/state.rs
  • src/tui/src/ui/app/render/settings/help.rs
  • src/tui/src/ui/app/changes/types.rs
  • src/tui/src/ui/app/harness_workspace_tests.rs
  • src/tui/tests/feature_commands.rs
  • src/tui/src/ui/app/types.rs
  • src/tui/src/ui/app/session_control.rs

@senamakel
senamakel merged commit a6b429c into split/2-tree-ui Aug 5, 2026
6 of 11 checks passed
sanil-23 added a commit that referenced this pull request Aug 5, 2026
Review follow-ups on the vocabulary pass (#183).

`took_control_by_attach` was only ever set to `true`, and nothing clears
it on release — so a `true` left by an attachment that really did take a
session survived into the next one. If that next session was already the
operator's, a pty write failure read the stale flag and handed it to the
orchestrator, taking away a session they had been holding before they
focused in. It is now assigned from `took` on every attachment.

`SessionControl`'s doc defined "unmanaged" as a session *born* user-held.
It is not a kind of session: `claim_idle` reads the current holder, so a
dispatched session the operator takes is unmanaged for as long as they
keep it. The doc now describes current control and leaves origin to
`SessionOrigin`.

And the containment diagram in `TERMINOLOGY.md` gets a language on its
fence, for MD040.

Co-Authored-By: Claude <noreply@anthropic.com>
sanil-23 added a commit that referenced this pull request Aug 5, 2026
… and control (#189)

* feat(hosts): render the Hosts tab as Host → Agents with persisted roles

The page rendered `Runtime::workers()` flat and called each row a host — its
own comment admitted the collapse. That was true only while a machine
advertised one worker; a machine now declares one agent per
`harness × workspace`, so the list was agents with the host level taken out of
it, and a fleet you cannot see the shape of is one you cannot manage.

The tab is now the topology the advert is a projection of (spec §2.4):

- **The hosts this machine runs come first, always.** They are resolved from
  config, not from what started, so a host that is declared but idle is still
  listed — that is the state where the operator most needs to see it. A running
  primary overrides its identity from the live observation, since
  `[host].workspace` is usually blank and only it has resolved that.
- **Each host carries its agents**, from two sources that are deliberately not
  merged: declarations for a local host (an agent exists because it is written
  down), and the roster for a remote one — because the host link does not
  exchange declared agent lists yet (plan §D1). A remote host says so on its own
  row and again in its preview rather than passing this hub's roster off as that
  machine's declarations.
- **The cursor walks both levels.** A host row previews the machine — capacity,
  readiness, budgets, read from whichever entry probed it — and an agent row
  previews the thing a dispatch targets, with the role toggles.

**Roles are assigned per agent and persisted.** `set_roles` moves the roster
this process holds, and the roster is rebuilt from the declarations at every
launch — so assigning a role that way was a change the operator watched take
effect and then lost. Every toggle now writes the declaration through
`declare_agent` *first* and moves the live roster second; a failed write makes
no live change either, because a UI showing a role the file does not have is
worse than one that refused. An agent the roster knows but nothing declared —
the migration seed — is declared here from what it reports, since a role
assigned to something nobody wrote down has nowhere to persist to. Renaming and
removing follow the same rule.

**The v1 capability split is enforced in the UI**: the local host offers agent
creation (`n`) and role editing; a remote host is operator-read-only, because
its agents are declared on that machine. Orchestrator dispatch to a remote agent
is untouched — this is only about affordances in this terminal.

Shared, so the list and the binder cannot disagree about which address a
`[[hosts]]` section will bind: `medulla::config::local_hosts` now owns the
device-local host resolution the TUI binary derived privately, and
`medulla::ui::hosts` owns the tree itself.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(ui): make the Agents tab the Host → Agent → Session tree

The rail concatenated two lists — lanes folded from task events, and the
operator's own harnesses under a `── your harnesses ──` divider — which is
exactly the split the agent/session redefinition removes. A task *is* an
agent session; they differ only by origin. And lanes come from traffic, so
an agent nobody had dispatched to had no row at all.

- A3: `RailRow::{Agent(Lane), Agent(Sub), NewHarness, HarnessSeparator,
  Harness}` becomes `RailRow::{Host, Agent, Session, NewAgent, Lane}`.
  Agents come from `[fleet].agentDeclarations`, so a declared agent with
  zero sessions still renders; a lane nothing declares still gets a row.
  Sessions are one row type under their agent whatever started them, and
  the divider is gone. Host rows appear only once a remote host exists.
  `Lane` is the residue for what is not an agent — the orchestrator's own
  conversation, the functions divider, a `+N more` counter.
- A2: `+ New agent` replaces `+ New harness`. It reuses the harness picker
  (now carrying a `PickerPurpose`) for harness type × workspace dir, then a
  name prompt, and persists through `declare_agent`. Starting a session in
  an undeclared directory offers the same flow inline.
- A4: `Ctrl-T` on a row that names an agent opens a session *of that agent*
  — its declared harness in its declared workspace, named by the operator,
  `SessionOrigin::User`, with the managed/unmanaged choice kept.
- A7: the orchestrator's conversation carries a "sessions started" block;
  clicking an entry moves focus to that session and the rail follows.
  `Ctrl-O` returns — scoped to "not already on the orchestrator" so the
  chord keeps releasing the mouse where drag-select is actually wanted.

Session → agent resolves by the id the hub already filed the task under for
a dispatch, and by `harness × workspace` for an operator-started PTY. A
session in an undeclared directory stays listed rather than hidden.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ui): source both tabs' Host → Agent tree from one projection

The Agents rail was written before the Hosts tab grew the shared
`Host → Agent` projection, so it built its own host and agent levels out
of declarations plus the event fold. Two derivations of one tree is two
answers to "what exists", and the rail left a follow-up note saying so.

The rail's host and agent levels now come from `medulla::ui::hosts::host_rows`
— the same call the Hosts tab renders — with the folded lanes placed onto
the agents it produces. A lane the tree does not know (a backend-side
roster agent, a peer session) still keeps a row, so nothing that used to
be visible disappears. Session rows are unchanged: they are the rail's own
level, dispatched ones resolved by the roster id the hub filed them under
and operator-started ones by harness × workspace.

Progressive disclosure now reads off the projection: host headers appear
once it holds more than one host, which is also when a registered remote
peer first becomes a machine of its own rather than a bare lane.

Two fixes fall out of making the projection load-bearing for both tabs:
an agent declared with no host id is claimed by the machine looking at it
(it was rendered by the rail and dropped by the Hosts tab), and a peer
that reached the registry twice is one agent, not two.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ui): a rail row shows its own content, offers its own session

Three things an operator hit on the Agents tab.

The pane leaked the orchestrator's stream onto rows that are not it.
`Selection::lane_index` defaulted to lane 0 — the orchestrator's — for
every row with no lane of its own, so selecting `+ New agent`, a host
header, or an agent nothing had been dispatched to showed the
orchestrator thinking, attributed to a row that had not thought
anything. The fallback is gone: `lane_index` is an `Option`, `lane()`
answers `None`, and the new `summary` module renders what each of those
rows actually is — the agent's identity, harness, workspace, roles and
session count; the host's reach and agent count; what each action row
will do.

`+ New session` was unreachable. `open_new_session` shipped with the
tree bound only to `^T`, so only an operator who already knew the chord
could find it. `RailRow::NewSession` closes each agent's group, under
its sessions, for the agents this machine declares — Enter and a click
open the same named, user-owned flow.

Spawned sessions were listed in one block for the whole conversation.
They are attributed to the user turn that caused them — the event stream
already carries both halves, a `User` opening a turn and every
`TaskStart` until the next one belonging to it — and drawn under that
turn. Sessions this stream cannot account for keep a trailing group
rather than vanishing. The click-through is unchanged in kind: still
keyed by task id through `focus_session_for_task`, but the hit map is
now one slot per drawn row, since the entries are no longer contiguous.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ui): gate the pty-backed rail tests to unix

Three tests stand a session up by pointing the codex bin at /bin/sh, which
Windows has no equivalent of — CreateProcessW cannot find it, so they fail
there while passing everywhere else. This module's sibling pty tests already
carry the same guard for the same reason; these were written without it.

The row model they exercise is portable; only this way of standing a
session up is not.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ui): scope a session to the host that declares its agent

Review follow-ups on the Host → Agent → Session tree (#182).

The two that change what an operator can do:

- `^T` acted on any selected agent row, including one declared on
  another machine, so it started a *local* process for a remote agent —
  which the rail then listed as an orphan, because it resolves sessions
  against the local declarations. `declaration_for` now filters by host,
  and the refusal names the machine to open it on instead.
- A session started from a custom preset was compared to declarations by
  the CLI underneath it (`claude`) rather than by the preset id the
  declaration records (`deepseek`), so every preset-backed session was
  filed under no agent. The preset id is carried through the launch and
  `SessionRow::harness_id` is what the rail matches on.

The rest:

- `on_orchestrator_lane` matches a lane's own row explicitly. `RailRow::
  Lane` also wraps the overflow control and the `── functions ──`
  divider, and the divider names no lane at all — it fell through to the
  "no lanes yet ⇒ the orchestrator is all there is" answer and would have
  claimed the orchestrator's composer.
- One rule for the no-config-file path: an edit applies for this run, in
  the declaration list as well as the roster, and says how long it lasts.
  Roles updated only the roster (so the row redrew with the old ones), a
  rename was silent, and undeclaring refused. A seed with no workspace is
  now refused like one with no harness — an agent is `harness ×
  workspace`, and a declaration missing half of it is one no session can
  be opened from.
- `local_hosts` returns unique addresses; two sections that resolve to
  one cannot both bind, so listing both drew a host that will not be there.
- A remote host's detail row picks a *probed* entry rather than the first.
- `mod.rs` keeps the wiring; the host projection moves to `projection.rs`.
- Clicking either action row retargets the watch, so a click arriving
  from a task row stops that stream.
- `d` on the Hosts page clears the role focus before reshaping the tree.
- The role list never draws past the rows the pane gave it.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ui): retire "harness" as a noun for anything you interact with

Plan §A6/F1: a harness is a *type* — the value on an agent that says which
coding CLI its sessions run — not an entity. Everything the operator
touches is an agent or one of its sessions, and the old vocabulary was the
route by which the host ≡ worker ≡ agent collapse kept leaking back in.

Types and fields: `HarnessControl` → `SessionControl` (control is
per-session), `LocalHarnesses` → `LocalSessions`, `HarnessPicker` →
`AgentPicker`, `App.harnesses` → `local_sessions`, `harness_pane_session`
→ `pane_session`, `selected_harness_session` → `rail_session`,
`own_harness_lines` → `own_session_lines`, `harnesses_waiting` →
`sessions_waiting`, `Cmd::{HoldHarness,HandOffHarness}` →
`{HoldSession,HandOffSession}`, `BaselineSource::HarnessLaunch` →
`SessionLaunch`, and the take/hand-back/toggle/release entry points with
them. `ui/app/harness_control.rs` and `render/harness_modals.rs` follow
their contents to `session_control.rs` / `session_modals.rs`, as do four
integration-test files.

Copy: every status line, prompt, modal, pane title, help line and toast
that called a session a harness now calls it a session — including the
hand-back and take-control questions, the kill confirmation, and the key
hint bar. The `/harness` command is now `/session` (old spelling kept as
an alias, with a test pinning it), and Routing's `Harnesses` page is
`Harness Types`. The rail title counts the agents on the tree instead of
the lanes traffic happened to fold, so a quiet machine with three declared
agents no longer reads "Agents · 0".

Kept as "harness" where it genuinely means the type or the CLI process:
`HarnessProvider`/`HarnessChoice`, the `harness` field on a declaration,
`[harness]` config, custom presets, `harness_pty` and the pane internals
that talk to the child, and every wire name (`HarnessControlFrame`,
`HarnessHandoff`, `harness_select`) — hub and protocol are untouched
beyond two stale doc cross-references.

Docs: TERMINOLOGY.md now defines Agent as a declaration, Harness as a type
and not a level, Host → Agent → Session as the chain, and Session as the
runtime instance with origin and owner; README and workflows.md follow.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ui): read takeover from the attachment that is happening

Review follow-ups on the vocabulary pass (#183).

`took_control_by_attach` was only ever set to `true`, and nothing clears
it on release — so a `true` left by an attachment that really did take a
session survived into the next one. If that next session was already the
operator's, a pty write failure read the stale flag and handed it to the
orchestrator, taking away a session they had been holding before they
focused in. It is now assigned from `took` on every attachment.

`SessionControl`'s doc defined "unmanaged" as a session *born* user-held.
It is not a kind of session: `claim_idle` reads the current holder, so a
dispatched session the operator takes is unmanaged for as long as they
keep it. The doc now describes current control and leaves origin to
`SessionOrigin`.

And the containment diagram in `TERMINOLOGY.md` gets a language on its
fence, for MD040.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(hub): advertise real host topology and report the session that served a task

register_agents gains a hosts[] block (one entry per host this hub fronts,
kind derived from the locally declared hosts, address/name/resources only
where real values exist) and each agent now carries its own hostId plus
maxSessions. Task results report the sessionId that served them, which the
backend's ManagerTaskEntry.agentSessionId already expects.

metadata.workspace deliberately stays a bare path string: the backend parses
both shapes, so {path,type} buys nothing today and ships with phase D.
Control and handoff keys are byte-identical, pinned by assertions — the
backend's control folds depend on them exactly as they are.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(hub): stop advertising hostId on workspace-backed agents

The library reserves AgentDescriptor.hostId for a LOCAL agent — 'only
meaningful when workspaceId is absent' and 'must NEVER be set on a
harness-backed agent', whose host is derived by walking up from its
workspace. Setting it on every agent made the server take its 'a supplied
workspaceId or hostId always wins' early return and skip synthesizing a
workspaceId from metadata.workspace, orphaning every agent from the
agent -> workspace -> harness -> host chain: host_list still rendered the
topology, but placement answered 'no agent inside <host> is available
(none declared there)' and no task could be dispatched.

The host reaches the wire once, in the hosts[] block. An agent with no
workspace keeps hostId — it has nothing to walk up from, and that is the
case the field exists for.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(control): a person in a session no longer fails a dispatch

Phase E of the agent topology: dispatch candidacy and the control
semantics around a session an operator is holding. Everything here is
exercisable on one machine — declare two agents, dispatch, take a
session, watch the work route around it, hand back, see the result.

Candidacy (E1) is now two independent rules that only coincide today.
A hold is on a *session*: `claim_idle` already refuses to hand a
user-owned session to the orchestrator, so reuse is consulted first and
a person working never makes a dispatch fail. Serialization is on the
*checkout*: under `strategy: checkout` an agent's sessions share one
working tree, so a fresh session cannot start beside the writer that is
there — it queues. `PtyManager::operator_hold(cwd)` ("is this workspace
held") is replaced by `sessions_in(cwd)`, a neutral query, with the
policy stated where the strategy lives. Under `worktree` (§G) rule 2
simply stops applying, with no control logic to revisit. F3 still owns
serializing two *orchestrator* sessions in one checkout, which main
allows and which is a scheduling change, not a control one.

A mid-turn takeover suspends instead of discarding (E3): the fold, its
events, usage and workspace context are retained, everything already
written is folded out first, and the task stays open. Held time does not
accrue against the worker's idle ceiling, and the worker announces the
hold so the hub's no-progress window pauses too (E4) — gated exactly
like link liveness, so a worker that dies mid-hold is still reaped once
the session comes back. On hand-back the runtime runs a *fresh* turn in
that same session (E5), prompted with the original instruction plus a
directive to review the session history and workspace state and either
report the finished work or complete it; its answer is emitted as the
pending task's result, under the same task id, because it is the same
call. The blanket `harnessHeld` refusal is retired (E6): the only path
left to it is a queue that outlived the caller's budget, which keeps its
frame byte-identical so the backend's retry stays a retry. Taking
control is gated to local sessions and says so by name (E7).

Control state is no longer advertised at all: `control`, `controlReason`,
`controlSince` and the handback brief are per-agent keys describing a
per-session fact, and a backend folding them by `agentId` would mark
every task on an agent as held when a person took one session. The local
state and every behaviour it drives are unchanged; only the wire is
quieter. That makes the hand-back turn the *only* way a held in-flight
task reaches a result, so a dispatch that meets a person now always ends
in a real result or a real error — never in silence.

Co-Authored-By: Claude <noreply@anthropic.com>

* perf(daemon): lift the host-wide task cap so work stops queueing for a slot

Both the networked and embedded daemons capped concurrent tasks at 2, so a
third dispatch waited on a semaphore permit even when it targeted a
different agent in a different workspace — where nothing could collide.
That cap predates declared agents: it was the only guard when a machine
was one worker with one implicit session.

The limits that own the real hazard now sit at its grain: per-agent
max_sessions derived from the workspace strategy, and the checkout
serialization that keeps a second writer out of a tree someone is in. A
host-wide count knows about neither, so it could only delay work that was
already safe.

The semaphore stays as the accounting behind active_count, and an operator
can still set concurrency to impose a real cap on a small machine.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ui): trim join keys consistently, keep mod.rs to wiring, and collapse the hand-back instruction

- projection.rs deduped undeclared workers with raw ids while the loop above
  claimed them with trimmed ones, so a padded agent_id listed the same worker
  twice — the duplicate that loop exists to prevent.
- SessionRow::harness_id() is a join key; it now trims once at the source
  rather than leaving every caller to trim defensively.
- render/agents/mod.rs and render/mod.rs held behaviour; moved to child
  modules, per the repo's mod.rs rule.
- the hand-back prompt is one line with text after {instruction}, so a
  newline in it dropped the 'do not redo it' tail entirely — collapsed now.
- covers the render-driven remote-session lifecycle.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ui): assert the hand-back question in the vocabulary it now speaks

#187's pointer tests were written against the pre-sweep prompt, which said
"You still have this harness". The sweep renamed the thing the operator holds,
so the question now reads "...this session" and four of these assertions were
looking for a string nothing prints.

One of them was worse than a failure: the same literal appears under a `!`, so
the test asserting that clicking a session's *own* row asks nothing was passing
because the text it searched for could never be found — it would have passed just
as well if the question had been asked every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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